From 40fa0f365ccbc52b82099973bfb5ab28ce0d959a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:08:46 +0900 Subject: [PATCH 001/199] chore(deps): bump the github-actions-dependencies group across 1 directory with 2 updates (#34261) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/api-tests.yml | 8 ++++---- .github/workflows/autofix.yml | 2 +- .github/workflows/db-migration-test.yml | 4 ++-- .github/workflows/pyrefly-diff.yml | 2 +- .github/workflows/style.yml | 2 +- .github/workflows/vdb-tests.yml | 2 +- .github/workflows/web-e2e.yml | 2 +- .github/workflows/web-tests.yml | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index 7bce056970..cd967b76cf 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -35,7 +35,7 @@ jobs: persist-credentials: false - name: Setup UV and Python - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true python-version: ${{ matrix.python-version }} @@ -84,7 +84,7 @@ jobs: persist-credentials: false - name: Setup UV and Python - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true python-version: ${{ matrix.python-version }} @@ -156,7 +156,7 @@ jobs: persist-credentials: false - name: Setup UV and Python - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true python-version: "3.12" @@ -203,7 +203,7 @@ jobs: - name: Report coverage if: ${{ env.CODECOV_TOKEN != '' }} - uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: files: ./coverage.xml disable_search: true diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index d8a53c9594..0f89d32fdb 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -52,7 +52,7 @@ jobs: python-version: "3.11" - if: github.event_name != 'merge_group' - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - name: Generate Docker Compose if: github.event_name != 'merge_group' && steps.docker-compose-changes.outputs.any_changed == 'true' diff --git a/.github/workflows/db-migration-test.yml b/.github/workflows/db-migration-test.yml index ffb9734e48..5991abe3ba 100644 --- a/.github/workflows/db-migration-test.yml +++ b/.github/workflows/db-migration-test.yml @@ -19,7 +19,7 @@ jobs: persist-credentials: false - name: Setup UV and Python - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true python-version: "3.12" @@ -69,7 +69,7 @@ jobs: persist-credentials: false - name: Setup UV and Python - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true python-version: "3.12" diff --git a/.github/workflows/pyrefly-diff.yml b/.github/workflows/pyrefly-diff.yml index a00f469bbe..0b2a7b8e9e 100644 --- a/.github/workflows/pyrefly-diff.yml +++ b/.github/workflows/pyrefly-diff.yml @@ -22,7 +22,7 @@ jobs: fetch-depth: 0 - name: Setup Python & UV - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index 7b269ccf4e..a7432ae167 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -33,7 +33,7 @@ jobs: - name: Setup UV and Python if: steps.changed-files.outputs.any_changed == 'true' - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: false python-version: "3.12" diff --git a/.github/workflows/vdb-tests.yml b/.github/workflows/vdb-tests.yml index 7c4cd0ba8c..026ff0fe57 100644 --- a/.github/workflows/vdb-tests.yml +++ b/.github/workflows/vdb-tests.yml @@ -30,7 +30,7 @@ jobs: remove_tool_cache: true - name: Setup UV and Python - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/web-e2e.yml b/.github/workflows/web-e2e.yml index 8035d1ef8e..ec8e1ce4e3 100644 --- a/.github/workflows/web-e2e.yml +++ b/.github/workflows/web-e2e.yml @@ -32,7 +32,7 @@ jobs: run: vp install --frozen-lockfile - name: Setup UV and Python - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true python-version: "3.12" diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml index 8110a16355..9ce1156d87 100644 --- a/.github/workflows/web-tests.yml +++ b/.github/workflows/web-tests.yml @@ -83,7 +83,7 @@ jobs: - name: Report coverage if: ${{ env.CODECOV_TOKEN != '' }} - uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: directory: web/coverage flags: web From 456684dfc38cb2b8e48fa59110783123eded064c Mon Sep 17 00:00:00 2001 From: Renzo <170978465+RenzoMXD@users.noreply.github.com> Date: Mon, 30 Mar 2026 10:09:49 +0200 Subject: [PATCH 002/199] refactor: core/rag docstore, datasource, embedding, rerank, retrieval (#34203) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Asuka Minato --- .../rag/datasource/keyword/jieba/jieba.py | 4 +- api/core/rag/datasource/retrieval_service.py | 9 +-- .../tidb_on_qdrant/tidb_on_qdrant_vector.py | 5 +- api/core/rag/datasource/vdb/vector_factory.py | 2 +- api/core/rag/docstore/dataset_docstore.py | 22 +++---- api/core/rag/embedding/cached_embedding.py | 29 ++++----- api/core/rag/extractor/notion_extractor.py | 9 ++- .../index_processor/index_processor_base.py | 5 +- .../processor/paragraph_index_processor.py | 17 +++--- .../processor/parent_child_index_processor.py | 27 +++++---- api/core/rag/rerank/rerank_model.py | 6 +- api/core/rag/retrieval/dataset_retrieval.py | 4 +- .../datasource/keyword/jieba/test_jieba.py | 24 ++++---- .../datasource/test_datasource_retrieval.py | 8 +-- .../rag/datasource/vdb/test_vector_factory.py | 8 +-- .../rag/docstore/test_dataset_docstore.py | 14 ++--- .../rag/embedding/test_cached_embedding.py | 27 +++------ .../rag/embedding/test_embedding_service.py | 60 ++++++++----------- .../rag/extractor/test_notion_extractor.py | 19 +++--- .../test_paragraph_index_processor.py | 18 +++--- .../test_parent_child_index_processor.py | 17 ++---- .../rag/indexing/test_index_processor_base.py | 20 +++---- .../core/rag/rerank/test_reranker.py | 17 ++---- .../rag/retrieval/test_dataset_retrieval.py | 13 ++-- 24 files changed, 170 insertions(+), 214 deletions(-) diff --git a/api/core/rag/datasource/keyword/jieba/jieba.py b/api/core/rag/datasource/keyword/jieba/jieba.py index b07dc108be..b8d5db7a43 100644 --- a/api/core/rag/datasource/keyword/jieba/jieba.py +++ b/api/core/rag/datasource/keyword/jieba/jieba.py @@ -97,13 +97,13 @@ class Jieba(BaseKeyword): documents = [] - segment_query_stmt = db.session.query(DocumentSegment).where( + segment_query_stmt = select(DocumentSegment).where( DocumentSegment.dataset_id == self.dataset.id, DocumentSegment.index_node_id.in_(sorted_chunk_indices) ) if document_ids_filter: segment_query_stmt = segment_query_stmt.where(DocumentSegment.document_id.in_(document_ids_filter)) - segments = db.session.execute(segment_query_stmt).scalars().all() + segments = db.session.scalars(segment_query_stmt).all() segment_map = {segment.index_node_id: segment for segment in segments} for chunk_index in sorted_chunk_indices: segment = segment_map.get(chunk_index) diff --git a/api/core/rag/datasource/retrieval_service.py b/api/core/rag/datasource/retrieval_service.py index cc6ec12c75..203a8588d6 100644 --- a/api/core/rag/datasource/retrieval_service.py +++ b/api/core/rag/datasource/retrieval_service.py @@ -432,10 +432,11 @@ class RetrievalService: # Batch query dataset documents dataset_documents = { doc.id: doc - for doc in db.session.query(DatasetDocument) - .where(DatasetDocument.id.in_(document_ids)) - .options(load_only(DatasetDocument.id, DatasetDocument.doc_form, DatasetDocument.dataset_id)) - .all() + for doc in db.session.scalars( + select(DatasetDocument) + .where(DatasetDocument.id.in_(document_ids)) + .options(load_only(DatasetDocument.id, DatasetDocument.doc_form, DatasetDocument.dataset_id)) + ).all() } valid_dataset_documents = {} diff --git a/api/core/rag/datasource/vdb/tidb_on_qdrant/tidb_on_qdrant_vector.py b/api/core/rag/datasource/vdb/tidb_on_qdrant/tidb_on_qdrant_vector.py index 3c1d5e015f..69c81d521c 100644 --- a/api/core/rag/datasource/vdb/tidb_on_qdrant/tidb_on_qdrant_vector.py +++ b/api/core/rag/datasource/vdb/tidb_on_qdrant/tidb_on_qdrant_vector.py @@ -426,11 +426,10 @@ class TidbOnQdrantVectorFactory(AbstractVectorFactory): TIDB_ON_QDRANT_API_KEY = f"{tidb_auth_binding.account}:{tidb_auth_binding.password}" else: - idle_tidb_auth_binding = ( - db.session.query(TidbAuthBinding) + idle_tidb_auth_binding = db.session.scalar( + select(TidbAuthBinding) .where(TidbAuthBinding.active == False, TidbAuthBinding.status == "ACTIVE") .limit(1) - .one_or_none() ) if idle_tidb_auth_binding: idle_tidb_auth_binding.active = True diff --git a/api/core/rag/datasource/vdb/vector_factory.py b/api/core/rag/datasource/vdb/vector_factory.py index 5a8d3a2f3f..26531eab88 100644 --- a/api/core/rag/datasource/vdb/vector_factory.py +++ b/api/core/rag/datasource/vdb/vector_factory.py @@ -277,7 +277,7 @@ class Vector: return self._vector_processor.search_by_vector(query_vector, **kwargs) def search_by_file(self, file_id: str, **kwargs: Any) -> list[Document]: - upload_file: UploadFile | None = db.session.query(UploadFile).where(UploadFile.id == file_id).first() + upload_file: UploadFile | None = db.session.get(UploadFile, file_id) if not upload_file: return [] diff --git a/api/core/rag/docstore/dataset_docstore.py b/api/core/rag/docstore/dataset_docstore.py index e5b794f80d..40f45953af 100644 --- a/api/core/rag/docstore/dataset_docstore.py +++ b/api/core/rag/docstore/dataset_docstore.py @@ -4,7 +4,7 @@ from collections.abc import Sequence from typing import Any from graphon.model_runtime.entities.model_entities import ModelType -from sqlalchemy import func, select +from sqlalchemy import delete, func, select from core.model_manager import ModelManager from core.rag.index_processor.constant.index_type import IndexTechniqueType @@ -63,10 +63,8 @@ class DatasetDocumentStore: return output def add_documents(self, docs: Sequence[Document], allow_update: bool = True, save_child: bool = False): - max_position = ( - db.session.query(func.max(DocumentSegment.position)) - .where(DocumentSegment.document_id == self._document_id) - .scalar() + max_position = db.session.scalar( + select(func.max(DocumentSegment.position)).where(DocumentSegment.document_id == self._document_id) ) if max_position is None: @@ -155,12 +153,14 @@ class DatasetDocumentStore: ) if save_child and doc.children: # delete the existing child chunks - db.session.query(ChildChunk).where( - ChildChunk.tenant_id == self._dataset.tenant_id, - ChildChunk.dataset_id == self._dataset.id, - ChildChunk.document_id == self._document_id, - ChildChunk.segment_id == segment_document.id, - ).delete() + db.session.execute( + delete(ChildChunk).where( + ChildChunk.tenant_id == self._dataset.tenant_id, + ChildChunk.dataset_id == self._dataset.id, + ChildChunk.document_id == self._document_id, + ChildChunk.segment_id == segment_document.id, + ) + ) # add new child chunks for position, child in enumerate(doc.children, start=1): child_segment = ChildChunk( diff --git a/api/core/rag/embedding/cached_embedding.py b/api/core/rag/embedding/cached_embedding.py index 3bdad00712..8d1c0da392 100644 --- a/api/core/rag/embedding/cached_embedding.py +++ b/api/core/rag/embedding/cached_embedding.py @@ -6,6 +6,7 @@ from typing import Any, cast import numpy as np from graphon.model_runtime.entities.model_entities import ModelPropertyKey from graphon.model_runtime.model_providers.__base.text_embedding_model import TextEmbeddingModel +from sqlalchemy import select from sqlalchemy.exc import IntegrityError from configs import dify_config @@ -31,14 +32,14 @@ class CacheEmbedding(Embeddings): embedding_queue_indices = [] for i, text in enumerate(texts): hash = helper.generate_text_hash(text) - embedding = ( - db.session.query(Embedding) - .filter_by( - model_name=self._model_instance.model_name, - hash=hash, - provider_name=self._model_instance.provider, + embedding = db.session.scalar( + select(Embedding) + .where( + Embedding.model_name == self._model_instance.model_name, + Embedding.hash == hash, + Embedding.provider_name == self._model_instance.provider, ) - .first() + .limit(1) ) if embedding: text_embeddings[i] = embedding.get_embedding() @@ -112,14 +113,14 @@ class CacheEmbedding(Embeddings): embedding_queue_indices = [] for i, multimodel_document in enumerate(multimodel_documents): file_id = multimodel_document["file_id"] - embedding = ( - db.session.query(Embedding) - .filter_by( - model_name=self._model_instance.model_name, - hash=file_id, - provider_name=self._model_instance.provider, + embedding = db.session.scalar( + select(Embedding) + .where( + Embedding.model_name == self._model_instance.model_name, + Embedding.hash == file_id, + Embedding.provider_name == self._model_instance.provider, ) - .first() + .limit(1) ) if embedding: multimodel_embeddings[i] = embedding.get_embedding() diff --git a/api/core/rag/extractor/notion_extractor.py b/api/core/rag/extractor/notion_extractor.py index 372af8fd94..aa36160711 100644 --- a/api/core/rag/extractor/notion_extractor.py +++ b/api/core/rag/extractor/notion_extractor.py @@ -4,6 +4,7 @@ import operator from typing import Any, cast import httpx +from sqlalchemy import update from configs import dify_config from core.rag.extractor.extractor_base import BaseExtractor @@ -346,9 +347,11 @@ class NotionExtractor(BaseExtractor): if data_source_info: data_source_info["last_edited_time"] = last_edited_time - db.session.query(DocumentModel).filter_by(id=document_model.id).update( - {DocumentModel.data_source_info: json.dumps(data_source_info)} - ) # type: ignore + db.session.execute( + update(DocumentModel) + .where(DocumentModel.id == document_model.id) + .values(data_source_info=json.dumps(data_source_info)) + ) db.session.commit() def get_notion_last_edited_time(self) -> str: diff --git a/api/core/rag/index_processor/index_processor_base.py b/api/core/rag/index_processor/index_processor_base.py index a435dfc46a..7d504fdb35 100644 --- a/api/core/rag/index_processor/index_processor_base.py +++ b/api/core/rag/index_processor/index_processor_base.py @@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Any, NotRequired, Optional from urllib.parse import unquote, urlparse import httpx +from sqlalchemy import select from typing_extensions import TypedDict from configs import dify_config @@ -200,7 +201,7 @@ class BaseIndexProcessor(ABC): # Get unique IDs for database query unique_upload_file_ids = list(set(upload_file_id_list)) - upload_files = db.session.query(UploadFile).where(UploadFile.id.in_(unique_upload_file_ids)).all() + upload_files = db.session.scalars(select(UploadFile).where(UploadFile.id.in_(unique_upload_file_ids))).all() # Create a mapping from ID to UploadFile for quick lookup upload_file_map = {upload_file.id: upload_file for upload_file in upload_files} @@ -312,7 +313,7 @@ class BaseIndexProcessor(ABC): """ from services.file_service import FileService - tool_file = db.session.query(ToolFile).where(ToolFile.id == tool_file_id).first() + tool_file = db.session.get(ToolFile, tool_file_id) if not tool_file: return None blob = storage.load_once(tool_file.file_key) diff --git a/api/core/rag/index_processor/processor/paragraph_index_processor.py b/api/core/rag/index_processor/processor/paragraph_index_processor.py index 5c10ffbf2d..22ab492cbf 100644 --- a/api/core/rag/index_processor/processor/paragraph_index_processor.py +++ b/api/core/rag/index_processor/processor/paragraph_index_processor.py @@ -18,6 +18,7 @@ from graphon.model_runtime.entities.message_entities import ( UserPromptMessage, ) from graphon.model_runtime.entities.model_entities import ModelFeature, ModelType +from sqlalchemy import select from core.app.file_access import DatabaseFileAccessController from core.app.llm import deduct_llm_quota @@ -145,14 +146,12 @@ class ParagraphIndexProcessor(BaseIndexProcessor): if delete_summaries: if node_ids: # Find segments by index_node_id - segments = ( - db.session.query(DocumentSegment) - .filter( + segments = db.session.scalars( + select(DocumentSegment).where( DocumentSegment.dataset_id == dataset.id, DocumentSegment.index_node_id.in_(node_ids), ) - .all() - ) + ).all() segment_ids = [segment.id for segment in segments] if segment_ids: SummaryIndexService.delete_summaries_for_segments(dataset, segment_ids) @@ -537,11 +536,9 @@ class ParagraphIndexProcessor(BaseIndexProcessor): # Get unique IDs for database query unique_upload_file_ids = list(set(upload_file_id_list)) - upload_files = ( - db.session.query(UploadFile) - .where(UploadFile.id.in_(unique_upload_file_ids), UploadFile.tenant_id == tenant_id) - .all() - ) + upload_files = db.session.scalars( + select(UploadFile).where(UploadFile.id.in_(unique_upload_file_ids), UploadFile.tenant_id == tenant_id) + ).all() # Create File objects from UploadFile records file_objects = [] diff --git a/api/core/rag/index_processor/processor/parent_child_index_processor.py b/api/core/rag/index_processor/processor/parent_child_index_processor.py index 70504e6e50..1c5e02e9c8 100644 --- a/api/core/rag/index_processor/processor/parent_child_index_processor.py +++ b/api/core/rag/index_processor/processor/parent_child_index_processor.py @@ -6,6 +6,8 @@ import uuid from collections.abc import Mapping from typing import Any +from sqlalchemy import delete, select + from configs import dify_config from core.db.session_factory import session_factory from core.entities.knowledge_entities import PreviewDetail @@ -177,17 +179,16 @@ class ParentChildIndexProcessor(BaseIndexProcessor): child_node_ids = precomputed_child_node_ids else: # Fallback to original query (may fail if segments are already deleted) - child_node_ids = ( - db.session.query(ChildChunk.index_node_id) + rows = db.session.execute( + select(ChildChunk.index_node_id) .join(DocumentSegment, ChildChunk.segment_id == DocumentSegment.id) .where( DocumentSegment.dataset_id == dataset.id, DocumentSegment.index_node_id.in_(node_ids), ChildChunk.dataset_id == dataset.id, ) - .all() - ) - child_node_ids = [child_node_id[0] for child_node_id in child_node_ids if child_node_id[0]] + ).all() + child_node_ids = [row[0] for row in rows if row[0]] # Delete from vector index if child_node_ids: @@ -195,18 +196,22 @@ class ParentChildIndexProcessor(BaseIndexProcessor): # Delete from database if delete_child_chunks and child_node_ids: - db.session.query(ChildChunk).where( - ChildChunk.dataset_id == dataset.id, ChildChunk.index_node_id.in_(child_node_ids) - ).delete(synchronize_session=False) + db.session.execute( + delete(ChildChunk).where( + ChildChunk.dataset_id == dataset.id, ChildChunk.index_node_id.in_(child_node_ids) + ) + ) db.session.commit() else: vector.delete() if delete_child_chunks: # Use existing compound index: (tenant_id, dataset_id, ...) - db.session.query(ChildChunk).where( - ChildChunk.tenant_id == dataset.tenant_id, ChildChunk.dataset_id == dataset.id - ).delete(synchronize_session=False) + db.session.execute( + delete(ChildChunk).where( + ChildChunk.tenant_id == dataset.tenant_id, ChildChunk.dataset_id == dataset.id + ) + ) db.session.commit() def retrieve( diff --git a/api/core/rag/rerank/rerank_model.py b/api/core/rag/rerank/rerank_model.py index 211a9f5c5c..8283be19f9 100644 --- a/api/core/rag/rerank/rerank_model.py +++ b/api/core/rag/rerank/rerank_model.py @@ -134,9 +134,7 @@ class RerankModelRunner(BaseRerankRunner): ): if document.metadata.get("doc_type") == DocType.IMAGE: # Query file info within db.session context to ensure thread-safe access - upload_file = ( - db.session.query(UploadFile).where(UploadFile.id == document.metadata["doc_id"]).first() - ) + upload_file = db.session.get(UploadFile, document.metadata["doc_id"]) if upload_file: blob = storage.load_once(upload_file.key) document_file_base64 = base64.b64encode(blob).decode() @@ -169,7 +167,7 @@ class RerankModelRunner(BaseRerankRunner): return rerank_result, unique_documents elif query_type == QueryType.IMAGE_QUERY: # Query file info within db.session context to ensure thread-safe access - upload_file = db.session.query(UploadFile).where(UploadFile.id == query).first() + upload_file = db.session.get(UploadFile, query) if upload_file: blob = storage.load_once(upload_file.key) file_query = base64.b64encode(blob).decode() diff --git a/api/core/rag/retrieval/dataset_retrieval.py b/api/core/rag/retrieval/dataset_retrieval.py index 1abea6639e..593e1f1420 100644 --- a/api/core/rag/retrieval/dataset_retrieval.py +++ b/api/core/rag/retrieval/dataset_retrieval.py @@ -1340,7 +1340,7 @@ class DatasetRetrieval: metadata_filtering_conditions: MetadataFilteringCondition | None, inputs: dict, ) -> tuple[dict[str, list[str]] | None, MetadataCondition | None]: - document_query = db.session.query(DatasetDocument).where( + document_query = select(DatasetDocument).where( DatasetDocument.dataset_id.in_(dataset_ids), DatasetDocument.indexing_status == "completed", DatasetDocument.enabled == True, @@ -1411,7 +1411,7 @@ class DatasetRetrieval: document_query = document_query.where(and_(*filters)) else: document_query = document_query.where(or_(*filters)) - documents = document_query.all() + documents = db.session.scalars(document_query).all() # group by dataset_id metadata_filter_document_ids = defaultdict(list) if documents else None # type: ignore for document in documents: diff --git a/api/tests/unit_tests/core/rag/datasource/keyword/jieba/test_jieba.py b/api/tests/unit_tests/core/rag/datasource/keyword/jieba/test_jieba.py index 795a325a6b..bbdd476914 100644 --- a/api/tests/unit_tests/core/rag/datasource/keyword/jieba/test_jieba.py +++ b/api/tests/unit_tests/core/rag/datasource/keyword/jieba/test_jieba.py @@ -201,27 +201,23 @@ def test_search_returns_documents_in_rank_order_and_applies_filter(monkeypatch, document_id = _Field("document_id") keyword = Jieba(_dataset(_dataset_keyword_table())) - query_stmt = _FakeQuery() - patched_runtime.session.query.return_value = query_stmt - patched_runtime.session.execute.return_value = _FakeExecuteResult( - [ - SimpleNamespace( - index_node_id="node-2", - content="segment-content", - index_node_hash="hash-2", - document_id="doc-2", - dataset_id="dataset-1", - ) - ] - ) + patched_runtime.session.scalars.return_value.all.return_value = [ + SimpleNamespace( + index_node_id="node-2", + content="segment-content", + index_node_hash="hash-2", + document_id="doc-2", + dataset_id="dataset-1", + ) + ] monkeypatch.setattr(jieba_module, "DocumentSegment", _FakeDocumentSegment) + monkeypatch.setattr(jieba_module, "select", lambda *_: _FakeSelect()) monkeypatch.setattr(keyword, "_get_dataset_keyword_table", MagicMock(return_value={"k": {"node-1", "node-2"}})) monkeypatch.setattr(keyword, "_retrieve_ids_by_query", MagicMock(return_value=["node-1", "node-2"])) documents = keyword.search("query", top_k=2, document_ids_filter=["doc-2"]) - assert len(query_stmt.where_calls) == 2 assert len(documents) == 1 assert documents[0].page_content == "segment-content" assert documents[0].metadata["doc_id"] == "node-2" diff --git a/api/tests/unit_tests/core/rag/datasource/test_datasource_retrieval.py b/api/tests/unit_tests/core/rag/datasource/test_datasource_retrieval.py index 63de4b8af2..5dbd62580a 100644 --- a/api/tests/unit_tests/core/rag/datasource/test_datasource_retrieval.py +++ b/api/tests/unit_tests/core/rag/datasource/test_datasource_retrieval.py @@ -714,13 +714,13 @@ class TestRetrievalServiceInternals: dataset_id="dataset-id", ) - dataset_query = Mock() - dataset_query.where.return_value.options.return_value.all.return_value = [ + scalars_result = Mock() + scalars_result.all.return_value = [ dataset_doc_parent, dataset_doc_text, dataset_doc_parent_summary, ] - monkeypatch.setattr(retrieval_service_module.db.session, "query", Mock(return_value=dataset_query)) + monkeypatch.setattr(retrieval_service_module.db.session, "scalars", Mock(return_value=scalars_result)) monkeypatch.setattr(retrieval_service_module, "RetrievalChildChunk", _SimpleRetrievalChildChunk) monkeypatch.setattr(retrieval_service_module, "RetrievalSegments", _SimpleRetrievalSegment) @@ -882,7 +882,7 @@ class TestRetrievalServiceInternals: def test_format_retrieval_documents_rolls_back_and_raises_when_db_fails(self, monkeypatch): rollback = Mock() monkeypatch.setattr(retrieval_service_module.db.session, "rollback", rollback) - monkeypatch.setattr(retrieval_service_module.db.session, "query", Mock(side_effect=RuntimeError("db error"))) + monkeypatch.setattr(retrieval_service_module.db.session, "scalars", Mock(side_effect=RuntimeError("db error"))) documents = [Document(page_content="content", metadata={"document_id": "doc-1"}, provider="dify")] diff --git a/api/tests/unit_tests/core/rag/datasource/vdb/test_vector_factory.py b/api/tests/unit_tests/core/rag/datasource/vdb/test_vector_factory.py index 54ad6d330b..4e9ceddda9 100644 --- a/api/tests/unit_tests/core/rag/datasource/vdb/test_vector_factory.py +++ b/api/tests/unit_tests/core/rag/datasource/vdb/test_vector_factory.py @@ -340,15 +340,13 @@ def test_search_by_file_handles_missing_and_existing_upload(vector_factory_modul vector._embeddings = MagicMock() vector._vector_processor = MagicMock() + mock_session = SimpleNamespace(get=lambda _model, _id: None) monkeypatch.setattr(vector_factory_module, "UploadFile", SimpleNamespace(id=_Field())) - monkeypatch.setattr( - vector_factory_module, "db", SimpleNamespace(session=SimpleNamespace(query=lambda _model: upload_query)) - ) + monkeypatch.setattr(vector_factory_module, "db", SimpleNamespace(session=mock_session)) - upload_query.first.return_value = None assert vector.search_by_file("file-1") == [] - upload_query.first.return_value = SimpleNamespace(key="blob-key") + mock_session.get = lambda _model, _id: SimpleNamespace(key="blob-key") monkeypatch.setattr(vector_factory_module.storage, "load_once", MagicMock(return_value=b"file-bytes")) vector._embeddings.embed_multimodal_query.return_value = [0.3, 0.4] vector._vector_processor.search_by_vector.return_value = ["hit"] diff --git a/api/tests/unit_tests/core/rag/docstore/test_dataset_docstore.py b/api/tests/unit_tests/core/rag/docstore/test_dataset_docstore.py index 3ba0628fe2..a7b7c1595b 100644 --- a/api/tests/unit_tests/core/rag/docstore/test_dataset_docstore.py +++ b/api/tests/unit_tests/core/rag/docstore/test_dataset_docstore.py @@ -167,7 +167,7 @@ class TestDatasetDocumentStoreAddDocuments: ): mock_session = MagicMock() mock_db.session = mock_session - mock_db.session.query.return_value.where.return_value.scalar.return_value = None + mock_db.session.scalar.return_value = None mock_manager = MagicMock() mock_manager.get_model_instance.return_value = mock_model_instance @@ -211,7 +211,7 @@ class TestDatasetDocumentStoreAddDocuments: with patch("core.rag.docstore.dataset_docstore.db") as mock_db: mock_session = MagicMock() mock_db.session = mock_session - mock_db.session.query.return_value.where.return_value.scalar.return_value = 5 + mock_db.session.scalar.return_value = 5 with patch.object(DatasetDocumentStore, "get_document_segment", return_value=mock_existing_segment): with patch.object(DatasetDocumentStore, "add_multimodel_documents_binding"): @@ -276,7 +276,7 @@ class TestDatasetDocumentStoreAddDocuments: with patch("core.rag.docstore.dataset_docstore.db") as mock_db: mock_session = MagicMock() mock_db.session = mock_session - mock_db.session.query.return_value.where.return_value.scalar.return_value = None + mock_db.session.scalar.return_value = None with patch.object(DatasetDocumentStore, "get_document_segment", return_value=None): with patch.object(DatasetDocumentStore, "add_multimodel_documents_binding"): @@ -353,7 +353,7 @@ class TestDatasetDocumentStoreAddDocuments: with patch("core.rag.docstore.dataset_docstore.db") as mock_db: mock_session = MagicMock() mock_db.session = mock_session - mock_db.session.query.return_value.where.return_value.scalar.return_value = None + mock_db.session.scalar.return_value = None with patch.object(DatasetDocumentStore, "get_document_segment", return_value=None): with patch.object(DatasetDocumentStore, "add_multimodel_documents_binding"): @@ -755,7 +755,7 @@ class TestDatasetDocumentStoreAddDocumentsUpdateChild: with patch("core.rag.docstore.dataset_docstore.db") as mock_db: mock_session = MagicMock() mock_db.session = mock_session - mock_db.session.query.return_value.where.return_value.scalar.return_value = 5 + mock_db.session.scalar.return_value = 5 with patch.object(DatasetDocumentStore, "get_document_segment", return_value=mock_existing_segment): with patch.object(DatasetDocumentStore, "add_multimodel_documents_binding"): @@ -767,7 +767,7 @@ class TestDatasetDocumentStoreAddDocumentsUpdateChild: store.add_documents([mock_doc], save_child=True) - mock_db.session.query.return_value.where.return_value.delete.assert_called() + mock_db.session.execute.assert_called() mock_db.session.commit.assert_called() @@ -798,7 +798,7 @@ class TestDatasetDocumentStoreAddDocumentsUpdateAnswer: with patch("core.rag.docstore.dataset_docstore.db") as mock_db: mock_session = MagicMock() mock_db.session = mock_session - mock_db.session.query.return_value.where.return_value.scalar.return_value = 5 + mock_db.session.scalar.return_value = 5 with patch.object(DatasetDocumentStore, "get_document_segment", return_value=mock_existing_segment): with patch.object(DatasetDocumentStore, "add_multimodel_documents_binding"): diff --git a/api/tests/unit_tests/core/rag/embedding/test_cached_embedding.py b/api/tests/unit_tests/core/rag/embedding/test_cached_embedding.py index 6fd44be4d4..3563186186 100644 --- a/api/tests/unit_tests/core/rag/embedding/test_cached_embedding.py +++ b/api/tests/unit_tests/core/rag/embedding/test_cached_embedding.py @@ -69,7 +69,7 @@ class TestCacheEmbeddingMultimodalDocuments: documents = [{"file_id": "file123", "content": "test content"}] with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None mock_model_instance.invoke_multimodal_embedding.return_value = sample_multimodal_result result = cache_embedding.embed_multimodal_documents(documents) @@ -114,7 +114,7 @@ class TestCacheEmbeddingMultimodalDocuments: ) with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None mock_model_instance.invoke_multimodal_embedding.return_value = embedding_result result = cache_embedding.embed_multimodal_documents(documents) @@ -134,7 +134,7 @@ class TestCacheEmbeddingMultimodalDocuments: mock_cached_embedding.get_embedding.return_value = normalized_cached with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = mock_cached_embedding + mock_session.scalar.return_value = mock_cached_embedding result = cache_embedding.embed_multimodal_documents(documents) @@ -180,18 +180,7 @@ class TestCacheEmbeddingMultimodalDocuments: ) with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - call_count = [0] - - def mock_filter_by(**kwargs): - call_count[0] += 1 - mock_query = Mock() - if call_count[0] == 1: - mock_query.first.return_value = mock_cached_embedding - else: - mock_query.first.return_value = None - return mock_query - - mock_session.query.return_value.filter_by = mock_filter_by + mock_session.scalar.side_effect = [mock_cached_embedding, None, None] mock_model_instance.invoke_multimodal_embedding.return_value = embedding_result result = cache_embedding.embed_multimodal_documents(documents) @@ -224,7 +213,7 @@ class TestCacheEmbeddingMultimodalDocuments: ) with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None mock_model_instance.invoke_multimodal_embedding.return_value = embedding_result with patch("core.rag.embedding.cached_embedding.logger") as mock_logger: @@ -265,7 +254,7 @@ class TestCacheEmbeddingMultimodalDocuments: ) with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None batch_results = [create_batch_result(10), create_batch_result(10), create_batch_result(5)] mock_model_instance.invoke_multimodal_embedding.side_effect = batch_results @@ -281,7 +270,7 @@ class TestCacheEmbeddingMultimodalDocuments: documents = [{"file_id": "file123"}] with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None mock_model_instance.invoke_multimodal_embedding.side_effect = Exception("API Error") with pytest.raises(Exception) as exc_info: @@ -298,7 +287,7 @@ class TestCacheEmbeddingMultimodalDocuments: documents = [{"file_id": "file123"}] with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None mock_model_instance.invoke_multimodal_embedding.return_value = sample_multimodal_result mock_session.commit.side_effect = IntegrityError("Duplicate key", None, None) diff --git a/api/tests/unit_tests/core/rag/embedding/test_embedding_service.py b/api/tests/unit_tests/core/rag/embedding/test_embedding_service.py index d7ba944e58..408cf14a51 100644 --- a/api/tests/unit_tests/core/rag/embedding/test_embedding_service.py +++ b/api/tests/unit_tests/core/rag/embedding/test_embedding_service.py @@ -139,7 +139,7 @@ class TestCacheEmbeddingDocuments: # Mock database query to return no cached embedding (cache miss) with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None # Mock model invocation mock_model_instance.invoke_text_embedding.return_value = sample_embedding_result @@ -203,7 +203,7 @@ class TestCacheEmbeddingDocuments: ) with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None mock_model_instance.invoke_text_embedding.return_value = embedding_result # Act @@ -240,7 +240,7 @@ class TestCacheEmbeddingDocuments: with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: # Mock database to return cached embedding (cache hit) - mock_session.query.return_value.filter_by.return_value.first.return_value = mock_cached_embedding + mock_session.scalar.return_value = mock_cached_embedding # Act result = cache_embedding.embed_documents(texts) @@ -313,19 +313,7 @@ class TestCacheEmbeddingDocuments: mock_hash.side_effect = generate_hash # Mock database to return cached embedding only for first text (hash_1) - call_count = [0] - - def mock_filter_by(**kwargs): - call_count[0] += 1 - mock_query = Mock() - # First call (hash_1) returns cached, others return None - if call_count[0] == 1: - mock_query.first.return_value = mock_cached_embedding - else: - mock_query.first.return_value = None - return mock_query - - mock_session.query.return_value.filter_by = mock_filter_by + mock_session.scalar.side_effect = [mock_cached_embedding, None, None] mock_model_instance.invoke_text_embedding.return_value = embedding_result # Act @@ -392,7 +380,7 @@ class TestCacheEmbeddingDocuments: ) with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None # Mock model to return appropriate batch results batch_results = [ @@ -455,7 +443,7 @@ class TestCacheEmbeddingDocuments: ) with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None mock_model_instance.invoke_text_embedding.return_value = embedding_result with patch("core.rag.embedding.cached_embedding.logger") as mock_logger: @@ -489,7 +477,7 @@ class TestCacheEmbeddingDocuments: texts = ["Test text"] with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None # Mock model to raise connection error mock_model_instance.invoke_text_embedding.side_effect = InvokeConnectionError("Failed to connect to API") @@ -515,7 +503,7 @@ class TestCacheEmbeddingDocuments: texts = ["Test text"] with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None # Mock model to raise rate limit error mock_model_instance.invoke_text_embedding.side_effect = InvokeRateLimitError("Rate limit exceeded") @@ -539,7 +527,7 @@ class TestCacheEmbeddingDocuments: texts = ["Test text"] with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None # Mock model to raise authorization error mock_model_instance.invoke_text_embedding.side_effect = InvokeAuthorizationError("Invalid API key") @@ -564,7 +552,7 @@ class TestCacheEmbeddingDocuments: texts = ["Test text"] with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None mock_model_instance.invoke_text_embedding.return_value = sample_embedding_result # Mock database commit to raise IntegrityError @@ -884,7 +872,7 @@ class TestEmbeddingModelSwitching: ) with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None model_instance_ada.invoke_text_embedding.return_value = result_ada model_instance_3_small.invoke_text_embedding.return_value = result_3_small @@ -1047,7 +1035,7 @@ class TestEmbeddingDimensionValidation: ) with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None mock_model_instance.invoke_text_embedding.return_value = embedding_result # Act @@ -1100,7 +1088,7 @@ class TestEmbeddingDimensionValidation: ) with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None mock_model_instance.invoke_text_embedding.return_value = embedding_result # Act @@ -1186,7 +1174,7 @@ class TestEmbeddingDimensionValidation: ) with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None model_instance_ada.invoke_text_embedding.return_value = result_ada model_instance_cohere.invoke_text_embedding.return_value = result_cohere @@ -1284,7 +1272,7 @@ class TestEmbeddingEdgeCases: ) with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None mock_model_instance.invoke_text_embedding.return_value = embedding_result # Act @@ -1327,7 +1315,7 @@ class TestEmbeddingEdgeCases: ) with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None mock_model_instance.invoke_text_embedding.return_value = embedding_result # Act @@ -1375,7 +1363,7 @@ class TestEmbeddingEdgeCases: ) with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None mock_model_instance.invoke_text_embedding.return_value = embedding_result # Act @@ -1427,7 +1415,7 @@ class TestEmbeddingEdgeCases: ) with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None mock_model_instance.invoke_text_embedding.return_value = embedding_result # Act @@ -1483,7 +1471,7 @@ class TestEmbeddingEdgeCases: ) with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None mock_model_instance.invoke_text_embedding.return_value = embedding_result # Act @@ -1551,7 +1539,7 @@ class TestEmbeddingEdgeCases: ) with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None mock_model_instance.invoke_text_embedding.return_value = embedding_result # Act @@ -1649,7 +1637,7 @@ class TestEmbeddingEdgeCases: ) with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None mock_model_instance.invoke_text_embedding.return_value = embedding_result # Act @@ -1728,7 +1716,7 @@ class TestEmbeddingCachePerformance: with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: # First call: cache miss - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None usage = EmbeddingUsage( tokens=5, @@ -1756,7 +1744,7 @@ class TestEmbeddingCachePerformance: assert len(result1) == 1 # Arrange - Second call: cache hit - mock_session.query.return_value.filter_by.return_value.first.return_value = mock_cached_embedding + mock_session.scalar.return_value = mock_cached_embedding # Act - Second call (cache hit) result2 = cache_embedding.embed_documents([text]) @@ -1816,7 +1804,7 @@ class TestEmbeddingCachePerformance: ) with patch("core.rag.embedding.cached_embedding.db.session") as mock_session: - mock_session.query.return_value.filter_by.return_value.first.return_value = None + mock_session.scalar.return_value = None # Mock model to return appropriate batch results batch_results = [ diff --git a/api/tests/unit_tests/core/rag/extractor/test_notion_extractor.py b/api/tests/unit_tests/core/rag/extractor/test_notion_extractor.py index 6daee11f8f..808e41867e 100644 --- a/api/tests/unit_tests/core/rag/extractor/test_notion_extractor.py +++ b/api/tests/unit_tests/core/rag/extractor/test_notion_extractor.py @@ -405,35 +405,36 @@ class TestNotionMetadataAndCredentialMethods: class FakeDocumentModel: data_source_info = "data_source_info" + id = "id" - update_calls = [] + execute_calls = [] - class FakeQuery: - def filter_by(self, **kwargs): + class FakeUpdateStmt: + def where(self, *args): return self - def update(self, payload): - update_calls.append(payload) + def values(self, **kwargs): + return self class FakeSession: committed = False - def query(self, model): - assert model is FakeDocumentModel - return FakeQuery() + def execute(self, stmt): + execute_calls.append(stmt) def commit(self): self.committed = True fake_db = SimpleNamespace(session=FakeSession()) monkeypatch.setattr(notion_extractor, "DocumentModel", FakeDocumentModel) + monkeypatch.setattr(notion_extractor, "update", lambda model: FakeUpdateStmt()) monkeypatch.setattr(notion_extractor, "db", fake_db) monkeypatch.setattr(extractor, "get_notion_last_edited_time", lambda: "2026-01-01T00:00:00.000Z") doc_model = SimpleNamespace(id="doc-1", data_source_info_dict={"source": "notion"}) extractor.update_last_edited_time(doc_model) - assert update_calls + assert execute_calls assert fake_db.session.committed is True def test_get_notion_last_edited_time_uses_page_and_database_urls(self, mocker: MockerFixture): diff --git a/api/tests/unit_tests/core/rag/indexing/processor/test_paragraph_index_processor.py b/api/tests/unit_tests/core/rag/indexing/processor/test_paragraph_index_processor.py index cc2873dd3f..d4b987c832 100644 --- a/api/tests/unit_tests/core/rag/indexing/processor/test_paragraph_index_processor.py +++ b/api/tests/unit_tests/core/rag/indexing/processor/test_paragraph_index_processor.py @@ -188,10 +188,10 @@ class TestParagraphIndexProcessor: mock_keyword_cls.return_value.add_texts.assert_called_once_with(docs) def test_clean_deletes_summaries_and_vector(self, processor: ParagraphIndexProcessor, dataset: Mock) -> None: - segment_query = Mock() - segment_query.filter.return_value.all.return_value = [SimpleNamespace(id="seg-1")] + scalars_result = Mock() + scalars_result.all.return_value = [SimpleNamespace(id="seg-1")] session = Mock() - session.query.return_value = segment_query + session.scalars.return_value = scalars_result with ( patch("core.rag.index_processor.processor.paragraph_index_processor.db.session", session), @@ -531,10 +531,10 @@ class TestParagraphIndexProcessor: size=1, key="key", ) - query = Mock() - query.where.return_value.all.return_value = [image_upload, non_image_upload] + scalars_result = Mock() + scalars_result.all.return_value = [image_upload, non_image_upload] session = Mock() - session.query.return_value = query + session.scalars.return_value = scalars_result with ( patch("core.rag.index_processor.processor.paragraph_index_processor.db.session", session), @@ -565,10 +565,10 @@ class TestParagraphIndexProcessor: size=1, key="key", ) - query = Mock() - query.where.return_value.all.return_value = [image_upload] + scalars_result = Mock() + scalars_result.all.return_value = [image_upload] session = Mock() - session.query.return_value = query + session.scalars.return_value = scalars_result with ( patch("core.rag.index_processor.processor.paragraph_index_processor.db.session", session), diff --git a/api/tests/unit_tests/core/rag/indexing/processor/test_parent_child_index_processor.py b/api/tests/unit_tests/core/rag/indexing/processor/test_parent_child_index_processor.py index b1ed735ee7..d363a0804d 100644 --- a/api/tests/unit_tests/core/rag/indexing/processor/test_parent_child_index_processor.py +++ b/api/tests/unit_tests/core/rag/indexing/processor/test_parent_child_index_processor.py @@ -208,11 +208,7 @@ class TestParentChildIndexProcessor: vector.create_multimodal.assert_called_once_with(multimodal_docs) def test_clean_with_precomputed_child_ids(self, processor: ParentChildIndexProcessor, dataset: Mock) -> None: - delete_query = Mock() - where_query = Mock() - where_query.delete.return_value = 2 session = Mock() - session.query.return_value.where.return_value = where_query with ( patch("core.rag.index_processor.processor.parent_child_index_processor.Vector") as mock_vector_cls, @@ -227,16 +223,16 @@ class TestParentChildIndexProcessor: ) vector.delete_by_ids.assert_called_once_with(["child-1", "child-2"]) - where_query.delete.assert_called_once_with(synchronize_session=False) + session.execute.assert_called() session.commit.assert_called_once() def test_clean_queries_child_ids_when_not_precomputed( self, processor: ParentChildIndexProcessor, dataset: Mock ) -> None: - child_query = Mock() - child_query.join.return_value.where.return_value.all.return_value = [("child-1",), (None,), ("child-2",)] + execute_result = Mock() + execute_result.all.return_value = [("child-1",), (None,), ("child-2",)] session = Mock() - session.query.return_value = child_query + session.execute.return_value = execute_result with ( patch("core.rag.index_processor.processor.parent_child_index_processor.Vector") as mock_vector_cls, @@ -248,10 +244,7 @@ class TestParentChildIndexProcessor: vector.delete_by_ids.assert_called_once_with(["child-1", "child-2"]) def test_clean_dataset_wide_cleanup(self, processor: ParentChildIndexProcessor, dataset: Mock) -> None: - where_query = Mock() - where_query.delete.return_value = 3 session = Mock() - session.query.return_value.where.return_value = where_query with ( patch("core.rag.index_processor.processor.parent_child_index_processor.Vector") as mock_vector_cls, @@ -261,7 +254,7 @@ class TestParentChildIndexProcessor: processor.clean(dataset, None, delete_child_chunks=True) vector.delete.assert_called_once() - where_query.delete.assert_called_once_with(synchronize_session=False) + session.execute.assert_called() session.commit.assert_called_once() def test_clean_deletes_summaries_when_requested(self, processor: ParentChildIndexProcessor, dataset: Mock) -> None: diff --git a/api/tests/unit_tests/core/rag/indexing/test_index_processor_base.py b/api/tests/unit_tests/core/rag/indexing/test_index_processor_base.py index b31bb6eea7..12c5238f5e 100644 --- a/api/tests/unit_tests/core/rag/indexing/test_index_processor_base.py +++ b/api/tests/unit_tests/core/rag/indexing/test_index_processor_base.py @@ -133,10 +133,10 @@ class TestBaseIndexProcessor: upload_b = SimpleNamespace(id="bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", name="b.png") upload_tool = SimpleNamespace(id="tool-upload-id", name="tool.png") upload_remote = SimpleNamespace(id="remote-upload-id", name="remote.png") - db_query = Mock() - db_query.where.return_value.all.return_value = [upload_a, upload_b, upload_tool, upload_remote] + scalars_result = Mock() + scalars_result.all.return_value = [upload_a, upload_b, upload_tool, upload_remote] db_session = Mock() - db_session.query.return_value = db_query + db_session.scalars.return_value = scalars_result with ( patch.object(processor, "_extract_markdown_images", return_value=images), @@ -170,10 +170,10 @@ class TestBaseIndexProcessor: def test_get_content_files_ignores_missing_upload_records(self, processor: _ForwardingBaseIndexProcessor) -> None: document = Document(page_content="ignored", metadata={"document_id": "doc-1", "dataset_id": "ds-1"}) images = ["/files/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/image-preview"] - db_query = Mock() - db_query.where.return_value.all.return_value = [] + scalars_result = Mock() + scalars_result.all.return_value = [] db_session = Mock() - db_session.query.return_value = db_query + db_session.scalars.return_value = scalars_result with ( patch.object(processor, "_extract_markdown_images", return_value=images), @@ -259,20 +259,16 @@ class TestBaseIndexProcessor: assert processor._download_image("https://example.com/image.png", current_user=Mock()) is None def test_download_tool_file_returns_none_when_not_found(self, processor: _ForwardingBaseIndexProcessor) -> None: - db_query = Mock() - db_query.where.return_value.first.return_value = None db_session = Mock() - db_session.query.return_value = db_query + db_session.get.return_value = None with patch("core.rag.index_processor.index_processor_base.db.session", db_session): assert processor._download_tool_file("tool-id", current_user=Mock()) is None def test_download_tool_file_uploads_file_when_found(self, processor: _ForwardingBaseIndexProcessor) -> None: tool_file = SimpleNamespace(file_key="k1", name="tool.png", mimetype="image/png") - db_query = Mock() - db_query.where.return_value.first.return_value = tool_file db_session = Mock() - db_session.query.return_value = db_query + db_session.get.return_value = tool_file mock_db = Mock() mock_db.session = db_session mock_db.engine = Mock() diff --git a/api/tests/unit_tests/core/rag/rerank/test_reranker.py b/api/tests/unit_tests/core/rag/rerank/test_reranker.py index 2ec7f0498e..c279b00d3b 100644 --- a/api/tests/unit_tests/core/rag/rerank/test_reranker.py +++ b/api/tests/unit_tests/core/rag/rerank/test_reranker.py @@ -473,12 +473,10 @@ class TestRerankModelRunnerMultimodal: metadata={}, provider="external", ) - query = Mock() - query.where.return_value.first.return_value = SimpleNamespace(key="image-key") rerank_result = RerankResult(model="rerank-model", docs=[]) with ( - patch("core.rag.rerank.rerank_model.db.session.query", return_value=query), + patch("core.rag.rerank.rerank_model.db.session.get", return_value=SimpleNamespace(key="image-key")), patch("core.rag.rerank.rerank_model.storage.load_once", return_value=b"image-bytes") as mock_load_once, patch.object( rerank_runner, @@ -504,12 +502,10 @@ class TestRerankModelRunnerMultimodal: metadata={"doc_id": "img-missing", "doc_type": DocType.IMAGE}, provider="dify", ) - query = Mock() - query.where.return_value.first.return_value = None rerank_result = RerankResult(model="rerank-model", docs=[]) with ( - patch("core.rag.rerank.rerank_model.db.session.query", return_value=query), + patch("core.rag.rerank.rerank_model.db.session.get", return_value=None), patch.object( rerank_runner, "fetch_text_rerank", @@ -533,8 +529,6 @@ class TestRerankModelRunnerMultimodal: metadata={"doc_id": "txt-1", "doc_type": DocType.TEXT}, provider="dify", ) - query_chain = Mock() - query_chain.where.return_value.first.return_value = SimpleNamespace(key="query-image-key") rerank_result = RerankResult( model="rerank-model", docs=[RerankDocument(index=0, text="text-content", score=0.77)], @@ -542,7 +536,7 @@ class TestRerankModelRunnerMultimodal: mock_model_instance.invoke_multimodal_rerank.return_value = rerank_result session = MagicMock() - session.query.return_value = query_chain + session.get.return_value = SimpleNamespace(key="query-image-key") with ( patch("core.rag.rerank.rerank_model.db.session", session), patch("core.rag.rerank.rerank_model.storage.load_once", return_value=b"query-image-bytes"), @@ -563,10 +557,7 @@ class TestRerankModelRunnerMultimodal: assert "user" not in invoke_kwargs def test_fetch_multimodal_rerank_raises_when_query_image_not_found(self, rerank_runner): - query_chain = Mock() - query_chain.where.return_value.first.return_value = None - - with patch("core.rag.rerank.rerank_model.db.session.query", return_value=query_chain): + with patch("core.rag.rerank.rerank_model.db.session.get", return_value=None): with pytest.raises(ValueError, match="Upload file not found for query"): rerank_runner.fetch_multimodal_rerank( query="missing-upload-id", diff --git a/api/tests/unit_tests/core/rag/retrieval/test_dataset_retrieval.py b/api/tests/unit_tests/core/rag/retrieval/test_dataset_retrieval.py index c11426163e..fee7b168ad 100644 --- a/api/tests/unit_tests/core/rag/retrieval/test_dataset_retrieval.py +++ b/api/tests/unit_tests/core/rag/retrieval/test_dataset_retrieval.py @@ -3971,11 +3971,10 @@ class TestDatasetRetrievalAdditionalHelpers: ) def test_get_metadata_filter_condition(self, retrieval: DatasetRetrieval) -> None: - db_query = Mock() - db_query.where.return_value = db_query - db_query.all.return_value = [SimpleNamespace(dataset_id="d1", id="doc-1")] + scalars_result = Mock() + scalars_result.all.return_value = [SimpleNamespace(dataset_id="d1", id="doc-1")] - with patch("core.rag.retrieval.dataset_retrieval.db.session.query", return_value=db_query): + with patch("core.rag.retrieval.dataset_retrieval.db.session.scalars", return_value=scalars_result): mapping, condition = retrieval.get_metadata_filter_condition( dataset_ids=["d1"], query="python", @@ -3991,7 +3990,7 @@ class TestDatasetRetrievalAdditionalHelpers: automatic_filters = [{"condition": "contains", "metadata_name": "author", "value": "Alice"}] with ( - patch("core.rag.retrieval.dataset_retrieval.db.session.query", return_value=db_query), + patch("core.rag.retrieval.dataset_retrieval.db.session.scalars", return_value=scalars_result), patch.object(retrieval, "_automatic_metadata_filter_func", return_value=automatic_filters), ): mapping, condition = retrieval.get_metadata_filter_condition( @@ -4012,7 +4011,7 @@ class TestDatasetRetrievalAdditionalHelpers: logical_operator="and", conditions=[AppCondition(name="author", comparison_operator="contains", value="{{name}}")], ) - with patch("core.rag.retrieval.dataset_retrieval.db.session.query", return_value=db_query): + with patch("core.rag.retrieval.dataset_retrieval.db.session.scalars", return_value=scalars_result): mapping, condition = retrieval.get_metadata_filter_condition( dataset_ids=["d1"], query="python", @@ -4027,7 +4026,7 @@ class TestDatasetRetrievalAdditionalHelpers: assert condition is not None assert condition.conditions[0].value == "Alice" - with patch("core.rag.retrieval.dataset_retrieval.db.session.query", return_value=db_query): + with patch("core.rag.retrieval.dataset_retrieval.db.session.scalars", return_value=scalars_result): with pytest.raises(ValueError, match="Invalid metadata filtering mode"): retrieval.get_metadata_filter_condition( dataset_ids=["d1"], From 944db46d4fc622c9cbbc6e2fb0ceb34c69911391 Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka928@users.noreply.github.com> Date: Mon, 30 Mar 2026 04:22:29 -0400 Subject: [PATCH 003/199] refactor(api): replace json.loads with Pydantic validation in services layer (#33704) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Asuka Minato --- .../datasets/rag_pipeline/datasource_auth.py | 7 ++-- .../console/workspace/tool_providers.py | 3 +- .../console/workspace/trigger_providers.py | 6 +-- api/services/account_service.py | 16 ++++++-- api/services/message_service.py | 10 +++-- api/services/model_load_balancing_service.py | 11 +++--- api/services/plugin/plugin_migration.py | 37 ++++++++++++------- .../restore_archived_workflow_run.py | 22 +++++++++-- api/services/tools/tools_transform_service.py | 10 +++-- .../trigger_subscription_builder_service.py | 18 ++++++--- api/services/workflow_service.py | 2 +- .../services/plugin/test_oauth_service.py | 17 +++++++++ .../test_restore_archived_workflow_run.py | 3 +- 13 files changed, 114 insertions(+), 48 deletions(-) diff --git a/api/controllers/console/datasets/rag_pipeline/datasource_auth.py b/api/controllers/console/datasets/rag_pipeline/datasource_auth.py index 1976a6bc8a..bdf83b991e 100644 --- a/api/controllers/console/datasets/rag_pipeline/datasource_auth.py +++ b/api/controllers/console/datasets/rag_pipeline/datasource_auth.py @@ -120,7 +120,8 @@ class DatasourceOAuthCallback(Resource): if context is None: raise Forbidden("Invalid context_id") - user_id, tenant_id = context.get("user_id"), context.get("tenant_id") + user_id: str = context["user_id"] + tenant_id: str = context["tenant_id"] datasource_provider_id = DatasourceProviderID(provider_id) plugin_id = datasource_provider_id.plugin_id datasource_provider_service = DatasourceProviderService() @@ -141,7 +142,7 @@ class DatasourceOAuthCallback(Resource): system_credentials=oauth_client_params, request=request, ) - credential_id = context.get("credential_id") + credential_id: str | None = context.get("credential_id") if credential_id: datasource_provider_service.reauthorize_datasource_oauth_provider( tenant_id=tenant_id, @@ -150,7 +151,7 @@ class DatasourceOAuthCallback(Resource): name=oauth_response.metadata.get("name") or None, expire_at=oauth_response.expires_at, credentials=dict(oauth_response.credentials), - credential_id=context.get("credential_id"), + credential_id=credential_id, ) else: datasource_provider_service.add_datasource_oauth_provider( diff --git a/api/controllers/console/workspace/tool_providers.py b/api/controllers/console/workspace/tool_providers.py index 02eb0adc94..80216915cd 100644 --- a/api/controllers/console/workspace/tool_providers.py +++ b/api/controllers/console/workspace/tool_providers.py @@ -832,7 +832,8 @@ class ToolOAuthCallback(Resource): tool_provider = ToolProviderID(provider) plugin_id = tool_provider.plugin_id provider_name = tool_provider.provider_name - user_id, tenant_id = context.get("user_id"), context.get("tenant_id") + user_id: str = context["user_id"] + tenant_id: str = context["tenant_id"] oauth_handler = OAuthHandler() oauth_client_params = BuiltinToolManageService.get_oauth_client(tenant_id, provider) diff --git a/api/controllers/console/workspace/trigger_providers.py b/api/controllers/console/workspace/trigger_providers.py index 265b6ecd9a..76d64cb97c 100644 --- a/api/controllers/console/workspace/trigger_providers.py +++ b/api/controllers/console/workspace/trigger_providers.py @@ -499,9 +499,9 @@ class TriggerOAuthCallbackApi(Resource): provider_id = TriggerProviderID(provider) plugin_id = provider_id.plugin_id provider_name = provider_id.provider_name - user_id = context.get("user_id") - tenant_id = context.get("tenant_id") - subscription_builder_id = context.get("subscription_builder_id") + user_id: str = context["user_id"] + tenant_id: str = context["tenant_id"] + subscription_builder_id: str = context["subscription_builder_id"] # Get OAuth client configuration oauth_client_params = TriggerProviderService.get_oauth_client( diff --git a/api/services/account_service.py b/api/services/account_service.py index bd520f54cf..cc8ef08857 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -7,9 +7,19 @@ from datetime import UTC, datetime, timedelta from hashlib import sha256 from typing import Any, cast -from pydantic import BaseModel +from pydantic import BaseModel, TypeAdapter from sqlalchemy import func, select from sqlalchemy.orm import Session +from typing_extensions import TypedDict + + +class InvitationData(TypedDict): + account_id: str + email: str + workspace_id: str + + +_invitation_adapter: TypeAdapter[InvitationData] = TypeAdapter(InvitationData) from werkzeug.exceptions import Unauthorized from configs import dify_config @@ -1571,7 +1581,7 @@ class RegisterService: @classmethod def get_invitation_by_token( cls, token: str, workspace_id: str | None = None, email: str | None = None - ) -> dict[str, str] | None: + ) -> InvitationData | None: if workspace_id is not None and email is not None: email_hash = sha256(email.encode()).hexdigest() cache_key = f"member_invite_token:{workspace_id}, {email_hash}:{token}" @@ -1590,7 +1600,7 @@ class RegisterService: if not data: return None - invitation: dict = json.loads(data) + invitation = _invitation_adapter.validate_json(data) return invitation @classmethod diff --git a/api/services/message_service.py b/api/services/message_service.py index e5389ef659..a04f9cbe01 100644 --- a/api/services/message_service.py +++ b/api/services/message_service.py @@ -1,8 +1,8 @@ -import json from collections.abc import Sequence from typing import Union from graphon.model_runtime.entities.model_entities import ModelType +from pydantic import TypeAdapter from sqlalchemy.orm import sessionmaker from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfigManager @@ -17,7 +17,7 @@ from extensions.ext_database import db from libs.infinite_scroll_pagination import InfiniteScrollPagination from models import Account from models.enums import FeedbackFromSource, FeedbackRating -from models.model import App, AppMode, AppModelConfig, EndUser, Message, MessageFeedback +from models.model import App, AppMode, AppModelConfig, AppModelConfigDict, EndUser, Message, MessageFeedback from repositories.execution_extra_content_repository import ExecutionExtraContentRepository from repositories.sqlalchemy_execution_extra_content_repository import ( SQLAlchemyExecutionExtraContentRepository, @@ -31,6 +31,8 @@ from services.errors.message import ( ) from services.workflow_service import WorkflowService +_app_model_config_adapter: TypeAdapter[AppModelConfigDict] = TypeAdapter(AppModelConfigDict) + def _create_execution_extra_content_repository() -> ExecutionExtraContentRepository: session_maker = sessionmaker(bind=db.engine, expire_on_commit=False) @@ -286,7 +288,9 @@ class MessageService: .first() ) else: - conversation_override_model_configs = json.loads(conversation.override_model_configs) + conversation_override_model_configs = _app_model_config_adapter.validate_json( + conversation.override_model_configs + ) app_model_config = AppModelConfig( app_id=app_model.id, ) diff --git a/api/services/model_load_balancing_service.py b/api/services/model_load_balancing_service.py index 91cca5cb6d..25de411e43 100644 --- a/api/services/model_load_balancing_service.py +++ b/api/services/model_load_balancing_service.py @@ -1,7 +1,6 @@ import json import logging -from json import JSONDecodeError -from typing import Union +from typing import Any, Union from graphon.model_runtime.entities.model_entities import ModelType from graphon.model_runtime.entities.provider_entities import ( @@ -168,10 +167,10 @@ class ModelLoadBalancingService: try: if load_balancing_config.encrypted_config: - credentials: dict[str, object] = json.loads(load_balancing_config.encrypted_config) + credentials: dict[str, Any] = json.loads(load_balancing_config.encrypted_config) else: credentials = {} - except JSONDecodeError: + except (json.JSONDecodeError, ValueError): credentials = {} # Get provider credential secret variables @@ -256,7 +255,7 @@ class ModelLoadBalancingService: credentials = json.loads(load_balancing_model_config.encrypted_config) else: credentials = {} - except JSONDecodeError: + except (json.JSONDecodeError, ValueError): credentials = {} # Get credential form schemas from model credential schema or provider credential schema @@ -575,7 +574,7 @@ class ModelLoadBalancingService: original_credentials = json.loads(load_balancing_model_config.encrypted_config) else: original_credentials = {} - except JSONDecodeError: + except (json.JSONDecodeError, ValueError): original_credentials = {} # encrypt credentials diff --git a/api/services/plugin/plugin_migration.py b/api/services/plugin/plugin_migration.py index df5fa3e233..1562d4e696 100644 --- a/api/services/plugin/plugin_migration.py +++ b/api/services/plugin/plugin_migration.py @@ -12,7 +12,9 @@ import click import sqlalchemy as sa import tqdm from flask import Flask, current_app +from pydantic import TypeAdapter from sqlalchemy.orm import Session +from typing_extensions import TypedDict from core.agent.entities import AgentToolEntity from core.helper import marketplace @@ -33,6 +35,14 @@ logger = logging.getLogger(__name__) excluded_providers = ["time", "audio", "code", "webscraper"] +class _TenantPluginRecord(TypedDict): + tenant_id: str + plugins: list[str] + + +_tenant_plugin_adapter: TypeAdapter[_TenantPluginRecord] = TypeAdapter(_TenantPluginRecord) + + class PluginMigration: @classmethod def extract_plugins(cls, filepath: str, workers: int): @@ -308,9 +318,8 @@ class PluginMigration: logger.info("Extracting unique plugins from %s", extracted_plugins) with open(extracted_plugins) as f: for line in f: - data = json.loads(line) - new_plugin_ids = data.get("plugins", []) - for plugin_id in new_plugin_ids: + data = _tenant_plugin_adapter.validate_json(line) + for plugin_id in data["plugins"]: if plugin_id not in plugin_ids: plugin_ids.append(plugin_id) @@ -381,21 +390,23 @@ class PluginMigration: Read line by line, and install plugins for each tenant. """ for line in f: - data = json.loads(line) - tenant_id = data.get("tenant_id") - plugin_ids = data.get("plugins", []) - current_not_installed = { - "tenant_id": tenant_id, - "plugin_not_exist": [], - } + data = _tenant_plugin_adapter.validate_json(line) + tenant_id = data["tenant_id"] + plugin_ids = data["plugins"] + plugin_not_exist: list[str] = [] # get plugin unique identifier for plugin_id in plugin_ids: unique_identifier = plugins.get(plugin_id) if unique_identifier: - current_not_installed["plugin_not_exist"].append(plugin_id) + plugin_not_exist.append(plugin_id) - if current_not_installed["plugin_not_exist"]: - not_installed.append(current_not_installed) + if plugin_not_exist: + not_installed.append( + { + "tenant_id": tenant_id, + "plugin_not_exist": plugin_not_exist, + } + ) thread_pool.submit(install, tenant_id, plugin_ids) diff --git a/api/services/retention/workflow_run/restore_archived_workflow_run.py b/api/services/retention/workflow_run/restore_archived_workflow_run.py index 64dad7ba52..c8362738ee 100644 --- a/api/services/retention/workflow_run/restore_archived_workflow_run.py +++ b/api/services/retention/workflow_run/restore_archived_workflow_run.py @@ -6,7 +6,6 @@ back to the database. """ import io -import json import logging import time import zipfile @@ -17,8 +16,23 @@ from datetime import datetime from typing import Any, cast import click +from pydantic import TypeAdapter from sqlalchemy.dialects.postgresql import insert as pg_insert from sqlalchemy.engine import CursorResult +from typing_extensions import TypedDict + + +class _TableInfo(TypedDict, total=False): + row_count: int + + +class ArchiveManifest(TypedDict, total=False): + tables: dict[str, _TableInfo] + schema_version: str + + +_manifest_adapter: TypeAdapter[ArchiveManifest] = TypeAdapter(ArchiveManifest) + from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker from extensions.ext_database import db @@ -239,12 +253,12 @@ class WorkflowRunRestore: return self.workflow_run_repo @staticmethod - def _load_manifest_from_zip(archive: zipfile.ZipFile) -> dict[str, Any]: + def _load_manifest_from_zip(archive: zipfile.ZipFile) -> ArchiveManifest: try: data = archive.read("manifest.json") except KeyError as e: raise ValueError("manifest.json missing from archive bundle") from e - return json.loads(data.decode("utf-8")) + return _manifest_adapter.validate_json(data) def _restore_table_records( self, @@ -332,7 +346,7 @@ class WorkflowRunRestore: return result - def _get_schema_version(self, manifest: dict[str, Any]) -> str: + def _get_schema_version(self, manifest: ArchiveManifest) -> str: schema_version = manifest.get("schema_version") if not schema_version: logger.warning("Manifest missing schema_version; defaulting to 1.0") diff --git a/api/services/tools/tools_transform_service.py b/api/services/tools/tools_transform_service.py index b276146066..7cd61e3162 100644 --- a/api/services/tools/tools_transform_service.py +++ b/api/services/tools/tools_transform_service.py @@ -3,7 +3,7 @@ import logging from collections.abc import Mapping from typing import Any, Union -from pydantic import ValidationError +from pydantic import TypeAdapter, ValidationError from yarl import URL from configs import dify_config @@ -31,6 +31,8 @@ from services.plugin.plugin_service import PluginService logger = logging.getLogger(__name__) +_mcp_tools_adapter: TypeAdapter[list[MCPTool]] = TypeAdapter(list[MCPTool]) + class ToolTransformService: _MCP_SCHEMA_TYPE_RESOLUTION_MAX_DEPTH = 10 @@ -53,7 +55,7 @@ class ToolTransformService: if isinstance(icon, str): return json.loads(icon) return icon - except Exception: + except (json.JSONDecodeError, ValueError): return {"background": "#252525", "content": "\ud83d\ude01"} elif provider_type == ToolProviderType.MCP: return icon @@ -247,8 +249,8 @@ class ToolTransformService: response = provider_entity.to_api_response(user_name=user_name, include_sensitive=include_sensitive) try: - mcp_tools = [MCPTool(**tool) for tool in json.loads(db_provider.tools)] - except (ValidationError, json.JSONDecodeError): + mcp_tools = _mcp_tools_adapter.validate_json(db_provider.tools) + except (ValidationError, ValueError): mcp_tools = [] # Add additional fields specific to the transform response["id"] = db_provider.server_identifier if not for_list else db_provider.id diff --git a/api/services/trigger/trigger_subscription_builder_service.py b/api/services/trigger/trigger_subscription_builder_service.py index 37f852da3e..889717df72 100644 --- a/api/services/trigger/trigger_subscription_builder_service.py +++ b/api/services/trigger/trigger_subscription_builder_service.py @@ -1,4 +1,3 @@ -import json import logging import uuid from collections.abc import Mapping @@ -7,6 +6,7 @@ from datetime import datetime from typing import Any from flask import Request, Response +from pydantic import TypeAdapter from core.plugin.entities.plugin_daemon import CredentialType from core.plugin.entities.request import TriggerDispatchResponse @@ -29,6 +29,8 @@ from services.trigger.trigger_provider_service import TriggerProviderService logger = logging.getLogger(__name__) +_request_logs_adapter: TypeAdapter[list[RequestLog]] = TypeAdapter(list[RequestLog]) + class TriggerSubscriptionBuilderService: """Service for managing trigger providers and credentials""" @@ -398,7 +400,7 @@ class TriggerSubscriptionBuilderService: cache_key = cls.encode_cache_key(endpoint_id) subscription_cache = redis_client.get(cache_key) if subscription_cache: - return SubscriptionBuilder.model_validate(json.loads(subscription_cache)) + return SubscriptionBuilder.model_validate_json(subscription_cache) return None @@ -423,12 +425,16 @@ class TriggerSubscriptionBuilderService: ) key = f"trigger:subscription:builder:logs:{endpoint_id}" - logs = json.loads(redis_client.get(key) or "[]") - logs.append(log.model_dump(mode="json")) + logs = _request_logs_adapter.validate_json(redis_client.get(key) or b"[]") + logs.append(log) # Keep last N logs logs = logs[-cls.__VALIDATION_REQUEST_CACHE_COUNT__ :] - redis_client.setex(key, cls.__VALIDATION_REQUEST_CACHE_EXPIRE_SECONDS__, json.dumps(logs, default=str)) + redis_client.setex( + key, + cls.__VALIDATION_REQUEST_CACHE_EXPIRE_SECONDS__, + _request_logs_adapter.dump_json(logs), + ) @classmethod def list_logs(cls, endpoint_id: str) -> list[RequestLog]: @@ -437,7 +443,7 @@ class TriggerSubscriptionBuilderService: logs_json = redis_client.get(key) if not logs_json: return [] - return [RequestLog.model_validate(log) for log in json.loads(logs_json)] + return _request_logs_adapter.validate_json(logs_json) @classmethod def process_builder_validation_endpoint(cls, endpoint_id: str, request: Request) -> Response | None: diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index b555676704..3b3ee6dd92 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -1118,7 +1118,7 @@ class WorkflowService: continue try: payload = json.loads(recipient.recipient_payload) - except Exception: + except (json.JSONDecodeError, ValueError): logger.exception("Failed to parse human input recipient payload for delivery test.") continue email = payload.get("email") diff --git a/api/tests/unit_tests/services/plugin/test_oauth_service.py b/api/tests/unit_tests/services/plugin/test_oauth_service.py index 6511385000..eee65b3a18 100644 --- a/api/tests/unit_tests/services/plugin/test_oauth_service.py +++ b/api/tests/unit_tests/services/plugin/test_oauth_service.py @@ -93,3 +93,20 @@ class TestUseProxyContext: assert result == stored expected_key = "oauth_proxy_context:valid-id" redis_client.delete.assert_called_once_with(expected_key) + + def test_returns_context_with_credential_id(self): + from extensions.ext_redis import redis_client + + stored = { + "user_id": "u1", + "tenant_id": "t1", + "plugin_id": "p1", + "provider": "github", + "credential_id": "cred-42", + } + redis_client.get.return_value = json.dumps(stored).encode() + + result = OAuthProxyService.use_proxy_context("ctx-with-cred") + + assert result["credential_id"] == "cred-42" + assert result["tenant_id"] == "t1" diff --git a/api/tests/unit_tests/services/retention/workflow_run/test_restore_archived_workflow_run.py b/api/tests/unit_tests/services/retention/workflow_run/test_restore_archived_workflow_run.py index 4bfdba87a0..628e4e594d 100644 --- a/api/tests/unit_tests/services/retention/workflow_run/test_restore_archived_workflow_run.py +++ b/api/tests/unit_tests/services/retention/workflow_run/test_restore_archived_workflow_run.py @@ -13,6 +13,7 @@ from datetime import datetime from unittest.mock import Mock, create_autospec, patch import pytest +from pydantic import ValidationError from sqlalchemy import Column, Integer, MetaData, String, Table from libs.archive_storage import ArchiveStorageNotConfiguredError @@ -292,7 +293,7 @@ class TestLoadManifestFromZip: zip_buffer.seek(0) with zipfile.ZipFile(zip_buffer, "r") as archive: - with pytest.raises(json.JSONDecodeError): + with pytest.raises(ValidationError): WorkflowRunRestore._load_manifest_from_zip(archive) From 1aaba80211d85cc801f1d4dc2043df9034555359 Mon Sep 17 00:00:00 2001 From: jigangz <115519042+jigangz@users.noreply.github.com> Date: Mon, 30 Mar 2026 03:09:50 -0700 Subject: [PATCH 004/199] fix: enrich Service API segment responses with summary content (#34221) Co-authored-by: jigangz Co-authored-by: FFXN <31929997+FFXN@users.noreply.github.com> --- .../service_api/dataset/segment.py | 33 ++++++++-- .../dataset/test_dataset_segment.py | 65 ++++++++++++++++++- 2 files changed, 92 insertions(+), 6 deletions(-) diff --git a/api/controllers/service_api/dataset/segment.py b/api/controllers/service_api/dataset/segment.py index b4cc9874b6..5b16da81e0 100644 --- a/api/controllers/service_api/dataset/segment.py +++ b/api/controllers/service_api/dataset/segment.py @@ -29,6 +29,31 @@ from services.entities.knowledge_entities.knowledge_entities import SegmentUpdat from services.errors.chunk import ChildChunkDeleteIndexError, ChildChunkIndexingError from services.errors.chunk import ChildChunkDeleteIndexError as ChildChunkDeleteIndexServiceError from services.errors.chunk import ChildChunkIndexingError as ChildChunkIndexingServiceError +from services.summary_index_service import SummaryIndexService + + +def _marshal_segment_with_summary(segment, dataset_id: str) -> dict: + """Marshal a single segment and enrich it with summary content.""" + segment_dict = dict(marshal(segment, segment_fields)) # type: ignore[arg-type] + summary = SummaryIndexService.get_segment_summary(segment_id=segment.id, dataset_id=dataset_id) + segment_dict["summary"] = summary.summary_content if summary else None + return segment_dict + + +def _marshal_segments_with_summary(segments, dataset_id: str) -> list[dict]: + """Marshal multiple segments and enrich them with summary content (batch query).""" + segment_ids = [segment.id for segment in segments] + summaries: dict = {} + if segment_ids: + summary_records = SummaryIndexService.get_segments_summaries(segment_ids=segment_ids, dataset_id=dataset_id) + summaries = {chunk_id: record.summary_content for chunk_id, record in summary_records.items()} + + result = [] + for segment in segments: + segment_dict = dict(marshal(segment, segment_fields)) # type: ignore[arg-type] + segment_dict["summary"] = summaries.get(segment.id) + result.append(segment_dict) + return result class SegmentCreatePayload(BaseModel): @@ -132,7 +157,7 @@ class SegmentApi(DatasetApiResource): for args_item in payload.segments: SegmentService.segment_create_args_validate(args_item, document) segments = SegmentService.multi_create_segment(payload.segments, document, dataset) - return {"data": marshal(segments, segment_fields), "doc_form": document.doc_form}, 200 + return {"data": _marshal_segments_with_summary(segments, dataset_id), "doc_form": document.doc_form}, 200 else: return {"error": "Segments is required"}, 400 @@ -196,7 +221,7 @@ class SegmentApi(DatasetApiResource): ) response = { - "data": marshal(segments, segment_fields), + "data": _marshal_segments_with_summary(segments, dataset_id), "doc_form": document.doc_form, "total": total, "has_more": len(segments) == limit, @@ -296,7 +321,7 @@ class DatasetSegmentApi(DatasetApiResource): payload = SegmentUpdatePayload.model_validate(service_api_ns.payload or {}) updated_segment = SegmentService.update_segment(payload.segment, segment, document, dataset) - return {"data": marshal(updated_segment, segment_fields), "doc_form": document.doc_form}, 200 + return {"data": _marshal_segment_with_summary(updated_segment, dataset_id), "doc_form": document.doc_form}, 200 @service_api_ns.doc("get_segment") @service_api_ns.doc(description="Get a specific segment by ID") @@ -326,7 +351,7 @@ class DatasetSegmentApi(DatasetApiResource): if not segment: raise NotFound("Segment not found.") - return {"data": marshal(segment, segment_fields), "doc_form": document.doc_form}, 200 + return {"data": _marshal_segment_with_summary(segment, dataset_id), "doc_form": document.doc_form}, 200 @service_api_ns.route( diff --git a/api/tests/unit_tests/controllers/service_api/dataset/test_dataset_segment.py b/api/tests/unit_tests/controllers/service_api/dataset/test_dataset_segment.py index 7f5d6b0839..e9c3e6d376 100644 --- a/api/tests/unit_tests/controllers/service_api/dataset/test_dataset_segment.py +++ b/api/tests/unit_tests/controllers/service_api/dataset/test_dataset_segment.py @@ -768,6 +768,7 @@ class TestSegmentApiGet: ``current_account_with_tenant()`` and ``marshal``. """ + @patch("controllers.service_api.dataset.segment.SummaryIndexService") @patch("controllers.service_api.dataset.segment.marshal") @patch("controllers.service_api.dataset.segment.SegmentService") @patch("controllers.service_api.dataset.segment.DocumentService") @@ -780,6 +781,7 @@ class TestSegmentApiGet: mock_doc_svc, mock_seg_svc, mock_marshal, + mock_summary_svc, app, mock_tenant, mock_dataset, @@ -791,7 +793,8 @@ class TestSegmentApiGet: mock_db.session.scalar.return_value = mock_dataset mock_doc_svc.get_document.return_value = Mock(doc_form=IndexStructureType.PARAGRAPH_INDEX) mock_seg_svc.get_segments.return_value = ([mock_segment], 1) - mock_marshal.return_value = [{"id": mock_segment.id}] + mock_marshal.return_value = {"id": mock_segment.id} + mock_summary_svc.get_segments_summaries.return_value = {} # Act with app.test_request_context( @@ -872,6 +875,7 @@ class TestSegmentApiPost: mock_rate_limit.enabled = False mock_feature_svc.get_knowledge_rate_limit.return_value = mock_rate_limit + @patch("controllers.service_api.dataset.segment.SummaryIndexService") @patch("controllers.service_api.dataset.segment.marshal") @patch("controllers.service_api.dataset.segment.SegmentService") @patch("controllers.service_api.dataset.segment.DocumentService") @@ -888,6 +892,7 @@ class TestSegmentApiPost: mock_doc_svc, mock_seg_svc, mock_marshal, + mock_summary_svc, app, mock_tenant, mock_dataset, @@ -909,7 +914,8 @@ class TestSegmentApiPost: mock_seg_svc.segment_create_args_validate.return_value = None mock_seg_svc.multi_create_segment.return_value = [mock_segment] - mock_marshal.return_value = [{"id": mock_segment.id}] + mock_marshal.return_value = {"id": mock_segment.id} + mock_summary_svc.get_segments_summaries.return_value = {} segments_data = [{"content": "Test segment content", "answer": "Test answer"}] @@ -1206,6 +1212,7 @@ class TestDatasetSegmentApiUpdate: mock_rate_limit.enabled = False mock_feature_svc.get_knowledge_rate_limit.return_value = mock_rate_limit + @patch("controllers.service_api.dataset.segment.SummaryIndexService") @patch("controllers.service_api.dataset.segment.marshal") @patch("controllers.service_api.dataset.segment.SegmentService") @patch("controllers.service_api.dataset.segment.DocumentService") @@ -1224,6 +1231,7 @@ class TestDatasetSegmentApiUpdate: mock_doc_svc, mock_seg_svc, mock_marshal, + mock_summary_svc, app, mock_tenant, mock_dataset, @@ -1240,6 +1248,7 @@ class TestDatasetSegmentApiUpdate: updated = Mock() mock_seg_svc.update_segment.return_value = updated mock_marshal.return_value = {"id": mock_segment.id} + mock_summary_svc.get_segment_summary.return_value = None with app.test_request_context( f"/datasets/{mock_dataset.id}/documents/doc-id/segments/{mock_segment.id}", @@ -1349,6 +1358,7 @@ class TestDatasetSegmentApiGetSingle: ``current_account_with_tenant()`` and ``marshal``. """ + @patch("controllers.service_api.dataset.segment.SummaryIndexService") @patch("controllers.service_api.dataset.segment.marshal") @patch("controllers.service_api.dataset.segment.SegmentService") @patch("controllers.service_api.dataset.segment.DocumentService") @@ -1363,6 +1373,7 @@ class TestDatasetSegmentApiGetSingle: mock_doc_svc, mock_seg_svc, mock_marshal, + mock_summary_svc, app, mock_tenant, mock_dataset, @@ -1376,6 +1387,7 @@ class TestDatasetSegmentApiGetSingle: mock_doc_svc.get_document.return_value = mock_doc mock_seg_svc.get_segment_by_id.return_value = mock_segment mock_marshal.return_value = {"id": mock_segment.id} + mock_summary_svc.get_segment_summary.return_value = None with app.test_request_context( f"/datasets/{mock_dataset.id}/documents/doc-id/segments/{mock_segment.id}", @@ -1393,6 +1405,55 @@ class TestDatasetSegmentApiGetSingle: assert "data" in response assert response["doc_form"] == IndexStructureType.PARAGRAPH_INDEX + @patch("controllers.service_api.dataset.segment.SummaryIndexService") + @patch("controllers.service_api.dataset.segment.marshal") + @patch("controllers.service_api.dataset.segment.SegmentService") + @patch("controllers.service_api.dataset.segment.DocumentService") + @patch("controllers.service_api.dataset.segment.DatasetService") + @patch("controllers.service_api.dataset.segment.current_account_with_tenant") + @patch("controllers.service_api.dataset.segment.db") + def test_get_single_segment_includes_summary( + self, + mock_db, + mock_account_fn, + mock_dataset_svc, + mock_doc_svc, + mock_seg_svc, + mock_marshal, + mock_summary_svc, + app, + mock_tenant, + mock_dataset, + mock_segment, + ): + """Test that single segment response includes summary content from SummaryIndexService.""" + mock_account_fn.return_value = (Mock(), mock_tenant.id) + mock_db.session.scalar.return_value = mock_dataset + mock_dataset_svc.check_dataset_model_setting.return_value = None + mock_doc = Mock(doc_form=IndexStructureType.PARAGRAPH_INDEX) + mock_doc_svc.get_document.return_value = mock_doc + mock_seg_svc.get_segment_by_id.return_value = mock_segment + mock_marshal.return_value = {"id": mock_segment.id, "summary": None} + + mock_summary_record = Mock() + mock_summary_record.summary_content = "This is the segment summary" + mock_summary_svc.get_segment_summary.return_value = mock_summary_record + + with app.test_request_context( + f"/datasets/{mock_dataset.id}/documents/doc-id/segments/{mock_segment.id}", + method="GET", + ): + api = DatasetSegmentApi() + response, status = api.get( + tenant_id=mock_tenant.id, + dataset_id=mock_dataset.id, + document_id="doc-id", + segment_id=mock_segment.id, + ) + + assert status == 200 + assert response["data"]["summary"] == "This is the segment summary" + @patch("controllers.service_api.dataset.segment.current_account_with_tenant") @patch("controllers.service_api.dataset.segment.db") def test_get_single_segment_dataset_not_found( From 52a4bea88f45a30bc8c5a9fa3c031cb00a57be5a Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Mon, 30 Mar 2026 18:34:50 +0800 Subject: [PATCH 005/199] refactor: introduce pnpm workspace (#34241) Co-authored-by: yyh Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .github/actions/setup-web/action.yml | 1 - .github/workflows/autofix.yml | 4 + .github/workflows/build-push.yml | 30 +- .github/workflows/docker-build.yml | 30 +- .github/workflows/main-ci.yml | 8 + .github/workflows/style.yml | 8 +- .github/workflows/tool-test-sdks.yaml | 3 + .github/workflows/web-e2e.yml | 4 - .github/workflows/web-tests.yml | 31 - .gitignore | 3 +- web/.nvmrc => .nvmrc | 0 Makefile | 6 +- api/README.md | 2 + dev/setup | 3 +- dev/start-web | 4 +- e2e/AGENTS.md | 23 +- e2e/package.json | 22 +- e2e/pnpm-lock.yaml | 2632 --------- package.json | 11 + web/pnpm-lock.yaml => pnpm-lock.yaml | 4861 +++++++++++------ pnpm-workspace.yaml | 257 + sdks/nodejs-client/README.md | 4 + sdks/nodejs-client/package.json | 27 +- sdks/nodejs-client/pnpm-lock.yaml | 2255 -------- sdks/nodejs-client/pnpm-workspace.yaml | 2 - sdks/nodejs-client/scripts/publish.sh | 83 +- web/taze.config.js => taze.config.js | 1 + web/.dockerignore | 32 - web/Dockerfile | 24 +- web/Dockerfile.dockerignore | 34 + web/README.md | 34 +- .../config/config-audio.spec.tsx | 3 +- .../config/config-document.spec.tsx | 3 +- .../app/configuration/config/index.spec.tsx | 9 +- .../hooks/__tests__/use-dsl-drag-drop.spec.ts | 5 +- .../base/carousel/__tests__/index.spec.tsx | 38 +- .../upgrade-btn/__tests__/index.spec.tsx | 9 +- web/eslint.config.mjs | 1 + web/next.config.ts | 1 - web/package.json | 435 +- web/scripts/copy-and-start.mjs | 56 +- web/vitest.setup.ts | 3 + 42 files changed, 4060 insertions(+), 6942 deletions(-) rename web/.nvmrc => .nvmrc (100%) delete mode 100644 e2e/pnpm-lock.yaml create mode 100644 package.json rename web/pnpm-lock.yaml => pnpm-lock.yaml (77%) create mode 100644 pnpm-workspace.yaml delete mode 100644 sdks/nodejs-client/pnpm-lock.yaml delete mode 100644 sdks/nodejs-client/pnpm-workspace.yaml rename web/taze.config.js => taze.config.js (95%) delete mode 100644 web/.dockerignore create mode 100644 web/Dockerfile.dockerignore diff --git a/.github/actions/setup-web/action.yml b/.github/actions/setup-web/action.yml index 24af948732..673155bcf7 100644 --- a/.github/actions/setup-web/action.yml +++ b/.github/actions/setup-web/action.yml @@ -6,7 +6,6 @@ runs: - name: Setup Vite+ uses: voidzero-dev/setup-vp@20553a7a7429c429a74894104a2835d7fed28a72 # v1.3.0 with: - working-directory: web node-version-file: .nvmrc cache: true run-install: true diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 0f89d32fdb..9648c34274 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -39,6 +39,10 @@ jobs: with: files: | web/** + package.json + pnpm-lock.yaml + pnpm-workspace.yaml + .nvmrc - name: Check api inputs if: github.event_name != 'merge_group' id: api-changes diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index 1ae8d44482..a23edc70e5 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -24,27 +24,39 @@ env: jobs: build: - runs-on: ${{ matrix.platform == 'linux/arm64' && 'arm64_runner' || 'ubuntu-latest' }} + runs-on: ${{ matrix.runs_on }} if: github.repository == 'langgenius/dify' strategy: matrix: include: - service_name: "build-api-amd64" image_name_env: "DIFY_API_IMAGE_NAME" - context: "api" + artifact_context: "api" + build_context: "{{defaultContext}}:api" + file: "Dockerfile" platform: linux/amd64 + runs_on: ubuntu-latest - service_name: "build-api-arm64" image_name_env: "DIFY_API_IMAGE_NAME" - context: "api" + artifact_context: "api" + build_context: "{{defaultContext}}:api" + file: "Dockerfile" platform: linux/arm64 + runs_on: ubuntu-24.04-arm - service_name: "build-web-amd64" image_name_env: "DIFY_WEB_IMAGE_NAME" - context: "web" + artifact_context: "web" + build_context: "{{defaultContext}}" + file: "web/Dockerfile" platform: linux/amd64 + runs_on: ubuntu-latest - service_name: "build-web-arm64" image_name_env: "DIFY_WEB_IMAGE_NAME" - context: "web" + artifact_context: "web" + build_context: "{{defaultContext}}" + file: "web/Dockerfile" platform: linux/arm64 + runs_on: ubuntu-24.04-arm steps: - name: Prepare @@ -58,9 +70,6 @@ jobs: username: ${{ env.DOCKERHUB_USER }} password: ${{ env.DOCKERHUB_TOKEN }} - - name: Set up QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 @@ -74,7 +83,8 @@ jobs: id: build uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: - context: "{{defaultContext}}:${{ matrix.context }}" + context: ${{ matrix.build_context }} + file: ${{ matrix.file }} platforms: ${{ matrix.platform }} build-args: COMMIT_SHA=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }} labels: ${{ steps.meta.outputs.labels }} @@ -93,7 +103,7 @@ jobs: - name: Upload digest uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: - name: digests-${{ matrix.context }}-${{ env.PLATFORM_PAIR }} + name: digests-${{ matrix.artifact_context }}-${{ env.PLATFORM_PAIR }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 340b380dc9..cbeb1a3bb1 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -6,7 +6,12 @@ on: - "main" paths: - api/Dockerfile + - web/docker/** - web/Dockerfile + - package.json + - pnpm-lock.yaml + - pnpm-workspace.yaml + - .nvmrc concurrency: group: docker-build-${{ github.head_ref || github.run_id }} @@ -14,26 +19,31 @@ concurrency: jobs: build-docker: - runs-on: ubuntu-latest + runs-on: ${{ matrix.runs_on }} strategy: matrix: include: - service_name: "api-amd64" platform: linux/amd64 - context: "api" + runs_on: ubuntu-latest + context: "{{defaultContext}}:api" + file: "Dockerfile" - service_name: "api-arm64" platform: linux/arm64 - context: "api" + runs_on: ubuntu-24.04-arm + context: "{{defaultContext}}:api" + file: "Dockerfile" - service_name: "web-amd64" platform: linux/amd64 - context: "web" + runs_on: ubuntu-latest + context: "{{defaultContext}}" + file: "web/Dockerfile" - service_name: "web-arm64" platform: linux/arm64 - context: "web" + runs_on: ubuntu-24.04-arm + context: "{{defaultContext}}" + file: "web/Dockerfile" steps: - - name: Set up QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 @@ -41,8 +51,8 @@ jobs: uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: push: false - context: "{{defaultContext}}:${{ matrix.context }}" - file: "${{ matrix.file }}" + context: ${{ matrix.context }} + file: ${{ matrix.file }} platforms: ${{ matrix.platform }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index 2d96dae4da..104368d192 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -65,6 +65,10 @@ jobs: - 'docker/volumes/sandbox/conf/**' web: - 'web/**' + - 'package.json' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' + - '.nvmrc' - '.github/workflows/web-tests.yml' - '.github/actions/setup-web/**' e2e: @@ -73,6 +77,10 @@ jobs: - 'api/uv.lock' - 'e2e/**' - 'web/**' + - 'package.json' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' + - '.nvmrc' - 'docker/docker-compose.middleware.yaml' - 'docker/middleware.env.example' - '.github/workflows/web-e2e.yml' diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index a7432ae167..9bc4ceaa93 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -77,6 +77,10 @@ jobs: with: files: | web/** + package.json + pnpm-lock.yaml + pnpm-workspace.yaml + .nvmrc .github/workflows/style.yml .github/actions/setup-web/** @@ -90,9 +94,9 @@ jobs: uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: web/.eslintcache - key: ${{ runner.os }}-web-eslint-${{ hashFiles('web/package.json', 'web/pnpm-lock.yaml', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-${{ github.sha }} + key: ${{ runner.os }}-web-eslint-${{ hashFiles('web/package.json', 'pnpm-lock.yaml', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-${{ github.sha }} restore-keys: | - ${{ runner.os }}-web-eslint-${{ hashFiles('web/package.json', 'web/pnpm-lock.yaml', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}- + ${{ runner.os }}-web-eslint-${{ hashFiles('web/package.json', 'pnpm-lock.yaml', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}- - name: Web style check if: steps.changed-files.outputs.any_changed == 'true' diff --git a/.github/workflows/tool-test-sdks.yaml b/.github/workflows/tool-test-sdks.yaml index 3fc351c0c2..536a52b560 100644 --- a/.github/workflows/tool-test-sdks.yaml +++ b/.github/workflows/tool-test-sdks.yaml @@ -6,6 +6,9 @@ on: - main paths: - sdks/** + - package.json + - pnpm-lock.yaml + - pnpm-workspace.yaml concurrency: group: sdk-tests-${{ github.head_ref || github.run_id }} diff --git a/.github/workflows/web-e2e.yml b/.github/workflows/web-e2e.yml index ec8e1ce4e3..eb752619be 100644 --- a/.github/workflows/web-e2e.yml +++ b/.github/workflows/web-e2e.yml @@ -27,10 +27,6 @@ jobs: - name: Setup web dependencies uses: ./.github/actions/setup-web - - name: Install E2E package dependencies - working-directory: ./e2e - run: vp install --frozen-lockfile - - name: Setup UV and Python uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml index 9ce1156d87..3c36335e79 100644 --- a/.github/workflows/web-tests.yml +++ b/.github/workflows/web-tests.yml @@ -89,34 +89,3 @@ jobs: flags: web env: CODECOV_TOKEN: ${{ env.CODECOV_TOKEN }} - - web-build: - name: Web Build - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./web - - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Check changed files - id: changed-files - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 - with: - files: | - web/** - .github/workflows/web-tests.yml - .github/actions/setup-web/** - - - name: Setup web environment - if: steps.changed-files.outputs.any_changed == 'true' - uses: ./.github/actions/setup-web - - - name: Web build check - if: steps.changed-files.outputs.any_changed == 'true' - working-directory: ./web - run: vp run build diff --git a/.gitignore b/.gitignore index aaca9f2b0a..d7698fe3fd 100644 --- a/.gitignore +++ b/.gitignore @@ -212,6 +212,7 @@ api/.vscode # pnpm /.pnpm-store +/node_modules # plugin migrate plugins.jsonl @@ -239,4 +240,4 @@ scripts/stress-test/reports/ *.local.md # Code Agent Folder -.qoder/* \ No newline at end of file +.qoder/* diff --git a/web/.nvmrc b/.nvmrc similarity index 100% rename from web/.nvmrc rename to .nvmrc diff --git a/Makefile b/Makefile index c377b7c671..d8c9df5208 100644 --- a/Makefile +++ b/Makefile @@ -24,8 +24,8 @@ prepare-docker: # Step 2: Prepare web environment prepare-web: @echo "🌐 Setting up web environment..." - @cp -n web/.env.example web/.env 2>/dev/null || echo "Web .env already exists" - @cd web && pnpm install + @cp -n web/.env.example web/.env.local 2>/dev/null || echo "Web .env.local already exists" + @pnpm install @echo "✅ Web environment prepared (not started)" # Step 3: Prepare API environment @@ -93,7 +93,7 @@ test: # Build Docker images build-web: @echo "Building web Docker image: $(WEB_IMAGE):$(VERSION)..." - docker build -t $(WEB_IMAGE):$(VERSION) ./web + docker build -f web/Dockerfile -t $(WEB_IMAGE):$(VERSION) . @echo "Web Docker image built successfully: $(WEB_IMAGE):$(VERSION)" build-api: diff --git a/api/README.md b/api/README.md index b647367046..00562f3f78 100644 --- a/api/README.md +++ b/api/README.md @@ -40,6 +40,8 @@ The scripts resolve paths relative to their location, so you can run them from a ./dev/start-web ``` + `./dev/setup` and `./dev/start-web` install JavaScript dependencies through the repository root workspace, so you do not need a separate `cd web && pnpm install` step. + 1. Set up your application by visiting `http://localhost:3000`. 1. Start the worker service (async and scheduler tasks, runs from `api`). diff --git a/dev/setup b/dev/setup index 399c8f28a5..4236ff7fa7 100755 --- a/dev/setup +++ b/dev/setup @@ -24,5 +24,4 @@ cp "$MIDDLEWARE_ENV_EXAMPLE" "$MIDDLEWARE_ENV" cd "$ROOT/api" uv sync --group dev -cd "$ROOT/web" -pnpm install +pnpm --dir "$ROOT" install diff --git a/dev/start-web b/dev/start-web index f853f4a895..baf008274b 100755 --- a/dev/start-web +++ b/dev/start-web @@ -3,6 +3,6 @@ set -x SCRIPT_DIR="$(dirname "$(realpath "$0")")" -cd "$SCRIPT_DIR/../web" +ROOT_DIR="$(dirname "$SCRIPT_DIR")" -pnpm install && pnpm dev:inspect +pnpm --dir "$ROOT_DIR" install && pnpm --dir "$ROOT_DIR/web" dev:inspect diff --git a/e2e/AGENTS.md b/e2e/AGENTS.md index 245c9863d4..ae642768f5 100644 --- a/e2e/AGENTS.md +++ b/e2e/AGENTS.md @@ -19,15 +19,18 @@ It tests: - `uv` - Docker +Run the following commands from the repository root. + Install Playwright browsers once: ```bash -cd e2e pnpm install -pnpm e2e:install -pnpm check +pnpm -C e2e e2e:install +pnpm -C e2e check ``` +`pnpm install` is resolved through the repository workspace and uses the shared root lockfile plus `pnpm-workspace.yaml`. + Use `pnpm check` as the default local verification step after editing E2E TypeScript, Cucumber support code, or feature glue. It runs formatting, linting, and type checks for this package. Common commands: @@ -35,20 +38,20 @@ Common commands: ```bash # authenticated-only regression (default excludes @fresh) # expects backend API, frontend artifact, and middleware stack to already be running -pnpm e2e +pnpm -C e2e e2e # full reset + fresh install + authenticated scenarios # starts required middleware/dependencies for you -pnpm e2e:full +pnpm -C e2e e2e:full # run a tagged subset -pnpm e2e -- --tags @smoke +pnpm -C e2e e2e -- --tags @smoke # headed browser -pnpm e2e:headed -- --tags @smoke +pnpm -C e2e e2e:headed -- --tags @smoke # slow down browser actions for local debugging -E2E_SLOW_MO=500 pnpm e2e:headed -- --tags @smoke +E2E_SLOW_MO=500 pnpm -C e2e e2e:headed -- --tags @smoke ``` Frontend artifact behavior: @@ -101,7 +104,7 @@ Because of that, the `@fresh` install scenario only runs in the `pnpm e2e:full*` Reset all persisted E2E state: ```bash -pnpm e2e:reset +pnpm -C e2e e2e:reset ``` This removes: @@ -117,7 +120,7 @@ This removes: Start the full middleware stack: ```bash -pnpm e2e:middleware:up +pnpm -C e2e e2e:middleware:up ``` Stop the full middleware stack: diff --git a/e2e/package.json b/e2e/package.json index 9b8a1f873f..0ee2afff7f 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -14,21 +14,11 @@ "e2e:reset": "tsx ./scripts/setup.ts reset" }, "devDependencies": { - "@cucumber/cucumber": "12.7.0", - "@playwright/test": "1.51.1", - "@types/node": "25.5.0", - "tsx": "4.21.0", - "typescript": "5.9.3", - "vite-plus": "latest" - }, - "engines": { - "node": "^22.22.1" - }, - "packageManager": "pnpm@10.32.1", - "pnpm": { - "overrides": { - "vite": "npm:@voidzero-dev/vite-plus-core@latest", - "vitest": "npm:@voidzero-dev/vite-plus-test@latest" - } + "@cucumber/cucumber": "catalog:", + "@playwright/test": "catalog:", + "@types/node": "catalog:", + "tsx": "catalog:", + "typescript": "catalog:", + "vite-plus": "catalog:" } } diff --git a/e2e/pnpm-lock.yaml b/e2e/pnpm-lock.yaml deleted file mode 100644 index b63458ad4a..0000000000 --- a/e2e/pnpm-lock.yaml +++ /dev/null @@ -1,2632 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -overrides: - vite: npm:@voidzero-dev/vite-plus-core@latest - vitest: npm:@voidzero-dev/vite-plus-test@latest - -importers: - - .: - devDependencies: - '@cucumber/cucumber': - specifier: 12.7.0 - version: 12.7.0 - '@playwright/test': - specifier: 1.51.1 - version: 1.51.1 - '@types/node': - specifier: 25.5.0 - version: 25.5.0 - tsx: - specifier: 4.21.0 - version: 4.21.0 - typescript: - specifier: 5.9.3 - version: 5.9.3 - vite-plus: - specifier: latest - version: 0.1.14(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) - -packages: - - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} - engines: {node: '>=6.9.0'} - - '@colors/colors@1.5.0': - resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} - engines: {node: '>=0.1.90'} - - '@cucumber/ci-environment@13.0.0': - resolution: {integrity: sha512-cs+3NzfNkGbcmHPddjEv4TKFiBpZRQ6WJEEufB9mw+ExS22V/4R/zpDSEG+fsJ/iSNCd6A2sATdY8PFOyY3YnA==} - - '@cucumber/cucumber-expressions@19.0.0': - resolution: {integrity: sha512-4FKoOQh2Uf6F6/Ln+1OxuK8LkTg6PyAqekhf2Ix8zqV2M54sH+m7XNJNLhOFOAW/t9nxzRbw2CcvXbCLjcvHZg==} - - '@cucumber/cucumber@12.7.0': - resolution: {integrity: sha512-7A/9CJpJDxv1SQ7hAZU0zPn2yRxx6XMR+LO4T94Enm3cYNWsEEj+RGX38NLX4INT+H6w5raX3Csb/qs4vUBsOA==} - engines: {node: 20 || 22 || >=24} - hasBin: true - - '@cucumber/gherkin-streams@6.0.0': - resolution: {integrity: sha512-HLSHMmdDH0vCr7vsVEURcDA4WwnRLdjkhqr6a4HQ3i4RFK1wiDGPjBGVdGJLyuXuRdJpJbFc6QxHvT8pU4t6jw==} - hasBin: true - peerDependencies: - '@cucumber/gherkin': '>=22.0.0' - '@cucumber/message-streams': '>=4.0.0' - '@cucumber/messages': '>=17.1.1' - - '@cucumber/gherkin-utils@11.0.0': - resolution: {integrity: sha512-LJ+s4+TepHTgdKWDR4zbPyT7rQjmYIcukTwNbwNwgqr6i8Gjcmzf6NmtbYDA19m1ZFg6kWbFsmHnj37ZuX+kZA==} - hasBin: true - - '@cucumber/gherkin@38.0.0': - resolution: {integrity: sha512-duEXK+KDfQUzu3vsSzXjkxQ2tirF5PRsc1Xrts6THKHJO6mjw4RjM8RV+vliuDasmhhrmdLcOcM7d9nurNTJKw==} - - '@cucumber/html-formatter@23.0.0': - resolution: {integrity: sha512-WwcRzdM8Ixy4e53j+Frm3fKM5rNuIyWUfy4HajEN+Xk/YcjA6yW0ACGTFDReB++VDZz/iUtwYdTlPRY36NbqJg==} - peerDependencies: - '@cucumber/messages': '>=18' - - '@cucumber/junit-xml-formatter@0.9.0': - resolution: {integrity: sha512-WF+A7pBaXpKMD1i7K59Nk5519zj4extxY4+4nSgv5XLsGXHDf1gJnb84BkLUzevNtp2o2QzMG0vWLwSm8V5blw==} - peerDependencies: - '@cucumber/messages': '*' - - '@cucumber/message-streams@4.0.1': - resolution: {integrity: sha512-Kxap9uP5jD8tHUZVjTWgzxemi/0uOsbGjd4LBOSxcJoOCRbESFwemUzilJuzNTB8pcTQUh8D5oudUyxfkJOKmA==} - peerDependencies: - '@cucumber/messages': '>=17.1.1' - - '@cucumber/messages@32.0.1': - resolution: {integrity: sha512-1OSoW+GQvFUNAl6tdP2CTBexTXMNJF0094goVUcvugtQeXtJ0K8sCP0xbq7GGoiezs/eJAAOD03+zAPT64orHQ==} - - '@cucumber/pretty-formatter@1.0.1': - resolution: {integrity: sha512-A1lU4VVP0aUWdOTmpdzvXOyEYuPtBDI0xYwYJnmoMDplzxMdhcHk86lyyvYDoMoPzzq6OkOE3isuosvUU4X7IQ==} - peerDependencies: - '@cucumber/cucumber': '>=7.0.0' - '@cucumber/messages': '*' - - '@cucumber/query@14.7.0': - resolution: {integrity: sha512-fiqZ4gMEgYjmbuWproF/YeCdD5y+gD2BqgBIGbpihOsx6UlNsyzoDSfO+Tny0q65DxfK+pHo2UkPyEl7dO7wmQ==} - peerDependencies: - '@cucumber/messages': '*' - - '@cucumber/tag-expressions@9.1.0': - resolution: {integrity: sha512-bvHjcRFZ+J1TqIa9eFNO1wGHqwx4V9ZKV3hYgkuK/VahHx73uiP4rKV3JVrvWSMrwrFvJG6C8aEwnCWSvbyFdQ==} - - '@emnapi/core@1.9.1': - resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} - - '@emnapi/runtime@1.9.1': - resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} - - '@emnapi/wasi-threads@1.2.0': - resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} - - '@esbuild/aix-ppc64@0.27.4': - resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.27.4': - resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.27.4': - resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.27.4': - resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.27.4': - resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.4': - resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.27.4': - resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.4': - resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.27.4': - resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.27.4': - resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.27.4': - resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.27.4': - resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.27.4': - resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.27.4': - resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.4': - resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.27.4': - resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.27.4': - resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.27.4': - resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.4': - resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.27.4': - resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.4': - resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.27.4': - resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.27.4': - resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.27.4': - resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.27.4': - resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.27.4': - resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@napi-rs/wasm-runtime@1.1.1': - resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} - - '@oxc-project/runtime@0.121.0': - resolution: {integrity: sha512-p0bQukD8OEHxzY4T9OlANBbEFGnOnjo1CYi50HES7OD36UO2yPh6T+uOJKLtlg06eclxroipRCpQGMpeH8EJ/g==} - engines: {node: ^20.19.0 || >=22.12.0} - - '@oxc-project/types@0.122.0': - resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} - - '@oxfmt/binding-android-arm-eabi@0.42.0': - resolution: {integrity: sha512-dsqPTYsozeokRjlrt/b4E7Pj0z3eS3Eg74TWQuuKbjY4VttBmA88rB7d50Xrd+TZ986qdXCNeZRPEzZHAe+jow==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [android] - - '@oxfmt/binding-android-arm64@0.42.0': - resolution: {integrity: sha512-t+aAjHxcr5eOBphFHdg1ouQU9qmZZoRxnX7UOJSaTwSoKsb6TYezNKO0YbWytGXCECObRqNcUxPoPr0KaraAIg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@oxfmt/binding-darwin-arm64@0.42.0': - resolution: {integrity: sha512-ulpSEYMKg61C5bRMZinFHrKJYRoKGVbvMEXA5zM1puX3O9T6Q4XXDbft20yrDijpYWeuG59z3Nabt+npeTsM1A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@oxfmt/binding-darwin-x64@0.42.0': - resolution: {integrity: sha512-ttxLKhQYPdFiM8I/Ri37cvqChE4Xa562nNOsZFcv1CKTVLeEozXjKuYClNvxkXmNlcF55nzM80P+CQkdFBu+uQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@oxfmt/binding-freebsd-x64@0.42.0': - resolution: {integrity: sha512-Og7QS3yI3tdIKYZ58SXik0rADxIk2jmd+/YvuHRyKULWpG4V2fR5V4hvKm624Mc0cQET35waPXiCQWvjQEjwYQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@oxfmt/binding-linux-arm-gnueabihf@0.42.0': - resolution: {integrity: sha512-jwLOw/3CW4H6Vxcry4/buQHk7zm9Ne2YsidzTL1kpiMe4qqrRCwev3dkyWe2YkFmP+iZCQ7zku4KwjcLRoh8ew==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@oxfmt/binding-linux-arm-musleabihf@0.42.0': - resolution: {integrity: sha512-XwXu2vkMtiq2h7tfvN+WA/9/5/1IoGAVCFPiiQUvcAuG3efR97KNcRGM8BetmbYouFotQ2bDal3yyjUx6IPsTg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@oxfmt/binding-linux-arm64-gnu@0.42.0': - resolution: {integrity: sha512-ea7s/XUJoT7ENAtUQDudFe3nkSM3e3Qpz4nJFRdzO2wbgXEcjnchKLEsV3+t4ev3r8nWxIYr9NRjPWtnyIFJVA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@oxfmt/binding-linux-arm64-musl@0.42.0': - resolution: {integrity: sha512-+JA0YMlSdDqmacygGi2REp57c3fN+tzARD8nwsukx9pkCHK+6DkbAA9ojS4lNKsiBjIW8WWa0pBrBWhdZEqfuw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@oxfmt/binding-linux-ppc64-gnu@0.42.0': - resolution: {integrity: sha512-VfnET0j4Y5mdfCzh5gBt0NK28lgn5DKx+8WgSMLYYeSooHhohdbzwAStLki9pNuGy51y4I7IoW8bqwAaCMiJQg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@oxfmt/binding-linux-riscv64-gnu@0.42.0': - resolution: {integrity: sha512-gVlCbmBkB0fxBWbhBj9rcxezPydsQHf4MFKeHoTSPicOQ+8oGeTQgQ8EeesSybWeiFPVRx3bgdt4IJnH6nOjAA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@oxfmt/binding-linux-riscv64-musl@0.42.0': - resolution: {integrity: sha512-zN5OfstL0avgt/IgvRu0zjQzVh/EPkcLzs33E9LMAzpqlLWiPWeMDZyMGFlSRGOdDjuNmlZBCgj0pFnK5u32TQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@oxfmt/binding-linux-s390x-gnu@0.42.0': - resolution: {integrity: sha512-9X6+H2L0qMc2sCAgO9HS03bkGLMKvOFjmEdchaFlany3vNZOjnVui//D8k/xZAtQv2vaCs1reD5KAgPoIU4msA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@oxfmt/binding-linux-x64-gnu@0.42.0': - resolution: {integrity: sha512-BajxJ6KQvMMdpXGPWhBGyjb2Jvx4uec0w+wi6TJZ6Tv7+MzPwe0pO8g5h1U0jyFgoaF7mDl6yKPW3ykWcbUJRw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@oxfmt/binding-linux-x64-musl@0.42.0': - resolution: {integrity: sha512-0wV284I6vc5f0AqAhgAbHU2935B4bVpncPoe5n/WzVZY/KnHgqxC8iSFGeSyLWEgstFboIcWkOPck7tqbdHkzA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@oxfmt/binding-openharmony-arm64@0.42.0': - resolution: {integrity: sha512-p4BG6HpGnhfgHk1rzZfyR6zcWkE7iLrWxyehHfXUy4Qa5j3e0roglFOdP/Nj5cJJ58MA3isQ5dlfkW2nNEpolw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@oxfmt/binding-win32-arm64-msvc@0.42.0': - resolution: {integrity: sha512-mn//WV60A+IetORDxYieYGAoQso4KnVRRjORDewMcod4irlRe0OSC7YPhhwaexYNPQz/GCFk+v9iUcZ2W22yxQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@oxfmt/binding-win32-ia32-msvc@0.42.0': - resolution: {integrity: sha512-3gWltUrvuz4LPJXWivoAxZ28Of2O4N7OGuM5/X3ubPXCEV8hmgECLZzjz7UYvSDUS3grfdccQwmjynm+51EFpw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ia32] - os: [win32] - - '@oxfmt/binding-win32-x64-msvc@0.42.0': - resolution: {integrity: sha512-Wg4TMAfQRL9J9AZevJ/ZNy3uyyDztDYQtGr4P8UyyzIhLhFrdSmz1J/9JT+rv0fiCDLaFOBQnj3f3K3+a5PzDQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@oxlint-tsgolint/darwin-arm64@0.17.3': - resolution: {integrity: sha512-5aDl4mxXWs+Bj02pNrX6YY6v9KMZjLIytXoqolLEo0dfBNVeZUonZgJAa/w0aUmijwIRrBhxEzb42oLuUtfkGw==} - cpu: [arm64] - os: [darwin] - - '@oxlint-tsgolint/darwin-x64@0.17.3': - resolution: {integrity: sha512-gPBy4DS5ueCgXzko20XsNZzDe/Cxde056B+QuPLGvz05CGEAtmRfpImwnyY2lAXXjPL+SmnC/OYexu8zI12yHQ==} - cpu: [x64] - os: [darwin] - - '@oxlint-tsgolint/linux-arm64@0.17.3': - resolution: {integrity: sha512-+pkunvCfB6pB0G9qHVVXUao3nqzXQPo4O3DReIi+5nGa+bOU3J3Srgy+Zb8VyOL+WDsSMJ+U7+r09cKHWhz3hg==} - cpu: [arm64] - os: [linux] - - '@oxlint-tsgolint/linux-x64@0.17.3': - resolution: {integrity: sha512-/kW5oXtBThu4FjmgIBthdmMjWLzT3M1TEDQhxDu7hQU5xDeTd60CDXb2SSwKCbue9xu7MbiFoJu83LN0Z/d38g==} - cpu: [x64] - os: [linux] - - '@oxlint-tsgolint/win32-arm64@0.17.3': - resolution: {integrity: sha512-NMELRvbz4Ed4dxg8WiqZxtu3k4OJEp2B9KInZW+BMfqEqbwZdEJY83tbqz2hD1EjKO2akrqBQ0GpRUJEkd8kKw==} - cpu: [arm64] - os: [win32] - - '@oxlint-tsgolint/win32-x64@0.17.3': - resolution: {integrity: sha512-+pJ7r8J3SLPws5uoidVplZc8R/lpKyKPE6LoPGv9BME00Y1VjT6jWGx/dtUN8PWvcu3iTC6k+8u3ojFSJNmWTg==} - cpu: [x64] - os: [win32] - - '@oxlint/binding-android-arm-eabi@1.57.0': - resolution: {integrity: sha512-C7EiyfAJG4B70496eV543nKiq5cH0o/xIh/ufbjQz3SIvHhlDDsyn+mRFh+aW8KskTyUpyH2LGWL8p2oN6bl1A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [android] - - '@oxlint/binding-android-arm64@1.57.0': - resolution: {integrity: sha512-9i80AresjZ/FZf5xK8tKFbhQnijD4s1eOZw6/FHUwD59HEZbVLRc2C88ADYJfLZrF5XofWDiRX/Ja9KefCLy7w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@oxlint/binding-darwin-arm64@1.57.0': - resolution: {integrity: sha512-0eUfhRz5L2yKa9I8k3qpyl37XK3oBS5BvrgdVIx599WZK63P8sMbg+0s4IuxmIiZuBK68Ek+Z+gcKgeYf0otsg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@oxlint/binding-darwin-x64@1.57.0': - resolution: {integrity: sha512-UvrSuzBaYOue+QMAcuDITe0k/Vhj6KZGjfnI6x+NkxBTke/VoM7ZisaxgNY0LWuBkTnd1OmeQfEQdQ48fRjkQg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@oxlint/binding-freebsd-x64@1.57.0': - resolution: {integrity: sha512-wtQq0dCoiw4bUwlsNVDJJ3pxJA218fOezpgtLKrbQqUtQJcM9yP8z+I9fu14aHg0uyAxIY+99toL6uBa2r7nxA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@oxlint/binding-linux-arm-gnueabihf@1.57.0': - resolution: {integrity: sha512-qxFWl2BBBFcT4djKa+OtMdnLgoHEJXpqjyGwz8OhW35ImoCwR5qtAGqApNYce5260FQqoAHW8S8eZTjiX67Tsg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@oxlint/binding-linux-arm-musleabihf@1.57.0': - resolution: {integrity: sha512-SQoIsBU7J0bDW15/f0/RvxHfY3Y0+eB/caKBQtNFbuerTiA6JCYx9P1MrrFTwY2dTm/lMgTSgskvCEYk2AtG/Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@oxlint/binding-linux-arm64-gnu@1.57.0': - resolution: {integrity: sha512-jqxYd1W6WMeozsCmqe9Rzbu3SRrGTyGDAipRlRggetyYbUksJqJKvUNTQtZR/KFoJPb+grnSm5SHhdWrywv3RQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@oxlint/binding-linux-arm64-musl@1.57.0': - resolution: {integrity: sha512-i66WyEPVEvq9bxRUCJ/MP5EBfnTDN3nhwEdFZFTO5MmLLvzngfWEG3NSdXQzTT3vk5B9i6C2XSIYBh+aG6uqyg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@oxlint/binding-linux-ppc64-gnu@1.57.0': - resolution: {integrity: sha512-oMZDCwz4NobclZU3pH+V1/upVlJZiZvne4jQP+zhJwt+lmio4XXr4qG47CehvrW1Lx2YZiIHuxM2D4YpkG3KVA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@oxlint/binding-linux-riscv64-gnu@1.57.0': - resolution: {integrity: sha512-uoBnjJ3MMEBbfnWC1jSFr7/nSCkcQYa72NYoNtLl1imshDnWSolYCjzb8LVCwYCCfLJXD+0gBLD7fyC14c0+0g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@oxlint/binding-linux-riscv64-musl@1.57.0': - resolution: {integrity: sha512-BdrwD7haPZ8a9KrZhKJRSj6jwCor+Z8tHFZ3PT89Y3Jq5v3LfMfEePeAmD0LOTWpiTmzSzdmyw9ijneapiVHKQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@oxlint/binding-linux-s390x-gnu@1.57.0': - resolution: {integrity: sha512-BNs+7ZNsRstVg2tpNxAXfMX/Iv5oZh204dVyb8Z37+/gCh+yZqNTlg6YwCLIMPSk5wLWIGOaQjT0GUOahKYImw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@oxlint/binding-linux-x64-gnu@1.57.0': - resolution: {integrity: sha512-AghS18w+XcENcAX0+BQGLiqjpqpaxKJa4cWWP0OWNLacs27vHBxu7TYkv9LUSGe5w8lOJHeMxcYfZNOAPqw2bg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@oxlint/binding-linux-x64-musl@1.57.0': - resolution: {integrity: sha512-E/FV3GB8phu/Rpkhz5T96hAiJlGzn91qX5yj5gU754P5cmVGXY1Jw/VSjDSlZBCY3VHjsVLdzgdkJaomEmcNOg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@oxlint/binding-openharmony-arm64@1.57.0': - resolution: {integrity: sha512-xvZ2yZt0nUVfU14iuGv3V25jpr9pov5N0Wr28RXnHFxHCRxNDMtYPHV61gGLhN9IlXM96gI4pyYpLSJC5ClLCQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@oxlint/binding-win32-arm64-msvc@1.57.0': - resolution: {integrity: sha512-Z4D8Pd0AyHBKeazhdIXeUUy5sIS3Mo0veOlzlDECg6PhRRKgEsBJCCV1n+keUZtQ04OP+i7+itS3kOykUyNhDg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@oxlint/binding-win32-ia32-msvc@1.57.0': - resolution: {integrity: sha512-StOZ9nFMVKvevicbQfql6Pouu9pgbeQnu60Fvhz2S6yfMaii+wnueLnqQ5I1JPgNF0Syew4voBlAaHD13wH6tw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ia32] - os: [win32] - - '@oxlint/binding-win32-x64-msvc@1.57.0': - resolution: {integrity: sha512-6PuxhYgth8TuW0+ABPOIkGdBYw+qYGxgIdXPHSVpiCDm+hqTTWCmC739St1Xni0DJBt8HnSHTG67i1y6gr8qrA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@playwright/test@1.51.1': - resolution: {integrity: sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==} - engines: {node: '>=18'} - hasBin: true - - '@polka/url@1.0.0-next.29': - resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - - '@rolldown/binding-android-arm64@1.0.0-rc.12': - resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@rolldown/binding-darwin-arm64@1.0.0-rc.12': - resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.0.0-rc.12': - resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.0.0-rc.12': - resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': - resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': - resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': - resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': - resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': - resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': - resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': - resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@rolldown/pluginutils@1.0.0-rc.12': - resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} - - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - - '@teppeis/multimaps@3.0.0': - resolution: {integrity: sha512-ID7fosbc50TbT0MK0EG12O+gAP3W3Aa/Pz4DaTtQtEvlc9Odaqi0de+xuZ7Li2GtK4HzEX7IuRWS/JmZLksR3Q==} - engines: {node: '>=14'} - - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - - '@types/node@25.5.0': - resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} - - '@types/normalize-package-data@2.4.4': - resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} - - '@voidzero-dev/vite-plus-core@0.1.14': - resolution: {integrity: sha512-CCWzdkfW0fo0cQNlIsYp5fOuH2IwKuPZEb2UY2Z8gXcp5pG74A82H2Pthj0heAuvYTAnfT7kEC6zM+RbiBgQbg==} - engines: {node: ^20.19.0 || >=22.12.0} - peerDependencies: - '@arethetypeswrong/core': ^0.18.1 - '@tsdown/css': 0.21.4 - '@tsdown/exe': 0.21.4 - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.0 - esbuild: ^0.27.0 - jiti: '>=1.21.0' - less: ^4.0.0 - publint: ^0.3.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - typescript: ^5.0.0 - unplugin-unused: ^0.5.0 - yaml: ^2.4.2 - peerDependenciesMeta: - '@arethetypeswrong/core': - optional: true - '@tsdown/css': - optional: true - '@tsdown/exe': - optional: true - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - publint: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - typescript: - optional: true - unplugin-unused: - optional: true - yaml: - optional: true - - '@voidzero-dev/vite-plus-darwin-arm64@0.1.14': - resolution: {integrity: sha512-q2ESUSbapwsxVRe/KevKATahNRraoX5nti3HT9S3266OHT5sMroBY14jaxTv74ekjQc9E6EPhyLGQWuWQuuBRw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@voidzero-dev/vite-plus-darwin-x64@0.1.14': - resolution: {integrity: sha512-UpcDZc9G99E/4HDRoobvYHxMvFOG5uv3RwEcq0HF70u4DsnEMl1z8RaJLeWV7a09LGwj9Q+YWC3Z4INWnTLs8g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.14': - resolution: {integrity: sha512-GIjn35RABUEDB9gHD26nRq7T72Te+Qy2+NIzogwEaUE728PvPkatF5gMCeF4sigCoc8c4qxDwsG+A2A2LYGnDg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.14': - resolution: {integrity: sha512-qo2RToGirG0XCcxZ2AEOuonLM256z6dNbJzDDIo5gWYA+cIKigFQJbkPyr25zsT1tsP2aY0OTxt2038XbVlRkQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.14': - resolution: {integrity: sha512-BsMWKZfdfGcYLxxLyaePpg6NW54xqzzcfq8sFUwKfwby0kgOKQ4WymUXyBvO9nnBb0ZPsJQrV0sx+Onac/LTaw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@voidzero-dev/vite-plus-linux-x64-musl@0.1.14': - resolution: {integrity: sha512-mOrEpj7ntW9RopGbcOYG/L0pOs0qHzUG4Vz7NXbuf4dbOSlY4JjyoMOIWxjKQORQht02Hzuf8YrMGNwa6AjVSQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@voidzero-dev/vite-plus-test@0.1.14': - resolution: {integrity: sha512-rjF+qpYD+5+THOJZ3gbE3+cxsk5sW7nJ0ODK7y6ZKeS4amREUMedEDYykzKBwR7OZDC/WwE90A0iLWCr6qAXhA==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} - peerDependencies: - '@edge-runtime/vm': '*' - '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/ui': 4.1.1 - happy-dom: '*' - jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@opentelemetry/api': - optional: true - '@types/node': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.14': - resolution: {integrity: sha512-7iC+Ig+8D/zACy0IJf7w/vQ7duTjux9Ttmm3KOBdVWH4dl3JihydA7+SQVMhz71a4WiqJ6nPidoG8D6hUP4MVQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.14': - resolution: {integrity: sha512-yRJ/8yAYFluNHx0Ej6Kevx65MIeM3wFKklnxosVZRlz2ZRL1Ea1Qh3tWATr3Ipk1ciRxBv8KJgp6zXqjxtZSoQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - ansi-regex@4.1.1: - resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} - engines: {node: '>=6'} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - - assertion-error-formatter@3.0.0: - resolution: {integrity: sha512-6YyAVLrEze0kQ7CmJfUgrLHb+Y7XghmL2Ie7ijVa2Y9ynP3LV+VDiwFk62Dn0qtqbmY0BT0ss6p1xxpiF2PYbQ==} - - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - - balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} - - brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} - engines: {node: 18 || 20 || >=22} - - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - - cac@7.0.0: - resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} - engines: {node: '>=20.19.0'} - - capital-case@1.0.4: - resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - class-transformer@0.5.1: - resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} - - cli-table3@0.6.5: - resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} - engines: {node: 10.* || >= 12.*} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - commander@14.0.0: - resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==} - engines: {node: '>=20'} - - commander@14.0.2: - resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} - engines: {node: '>=20'} - - commander@14.0.3: - resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} - engines: {node: '>=20'} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - diff@4.0.4: - resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} - engines: {node: '>=0.3.1'} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - error-stack-parser@2.1.4: - resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} - - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - - esbuild@0.27.4: - resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} - engines: {node: '>=18'} - hasBin: true - - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - figures@3.2.0: - resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} - engines: {node: '>=8'} - - find-up-simple@1.0.1: - resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} - engines: {node: '>=18'} - - fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - get-tsconfig@4.13.7: - resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} - - glob@13.0.6: - resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} - engines: {node: 18 || 20 || >=22} - - global-dirs@3.0.1: - resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} - engines: {node: '>=10'} - - has-ansi@4.0.1: - resolution: {integrity: sha512-Qr4RtTm30xvEdqUXbSBVWDu+PrTokJOwe/FU+VdfJPk+MXAPoeOzKpRyrDTnZIJwAkQ4oBLTU53nu0HrkF/Z2A==} - engines: {node: '>=8'} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - hosted-git-info@9.0.2: - resolution: {integrity: sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==} - engines: {node: ^20.17.0 || >=22.9.0} - - indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} - - index-to-position@1.2.0: - resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} - engines: {node: '>=18'} - - ini@2.0.0: - resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} - engines: {node: '>=10'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-installed-globally@0.4.0: - resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} - engines: {node: '>=10'} - - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - knuth-shuffle-seeded@1.0.6: - resolution: {integrity: sha512-9pFH0SplrfyKyojCLxZfMcvkhf5hH0d+UwR9nTVJ/DDQJGuzcXjTwB7TP7sDfehSudlGGaOLblmEWqv04ERVWg==} - - lightningcss-android-arm64@1.32.0: - resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.32.0: - resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.32.0: - resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.32.0: - resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.32.0: - resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-musl@1.32.0: - resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-gnu@1.32.0: - resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-musl@1.32.0: - resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-win32-arm64-msvc@1.32.0: - resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.32.0: - resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.32.0: - resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} - engines: {node: '>= 12.0.0'} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - lodash.mergewith@4.6.2: - resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} - - lodash.sortby@4.7.0: - resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} - - lower-case@2.0.2: - resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} - - lru-cache@11.2.7: - resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} - engines: {node: 20 || >=22} - - luxon@3.7.2: - resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} - engines: {node: '>=12'} - - mime@3.0.0: - resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} - engines: {node: '>=10.0.0'} - hasBin: true - - minimatch@10.2.4: - resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} - engines: {node: 18 || 20 || >=22} - - minipass@7.1.3: - resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} - engines: {node: '>=16 || 14 >=14.17'} - - mkdirp@3.0.1: - resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} - engines: {node: '>=10'} - hasBin: true - - mrmime@2.0.1: - resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} - engines: {node: '>=10'} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - no-case@3.0.4: - resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} - - normalize-package-data@8.0.0: - resolution: {integrity: sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==} - engines: {node: ^20.17.0 || >=22.9.0} - - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - obug@2.1.1: - resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - - oxfmt@0.42.0: - resolution: {integrity: sha512-QhejGErLSMReNuZ6vxgFHDyGoPbjTRNi6uGHjy0cvIjOQFqD6xmr/T+3L41ixR3NIgzcNiJ6ylQKpvShTgDfqg==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - oxlint-tsgolint@0.17.3: - resolution: {integrity: sha512-1eh4bcpOMw0e7+YYVxmhFc2mo/V6hJ2+zfukqf+GprvVn3y94b69M/xNrYLmx5A+VdYe0i/bJ2xOs6Hp/jRmRA==} - hasBin: true - - oxlint@1.57.0: - resolution: {integrity: sha512-DGFsuBX5MFZX9yiDdtKjTrYPq45CZ8Fft6qCltJITYZxfwYjVdGf/6wycGYTACloauwIPxUnYhBVeZbHvleGhw==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - oxlint-tsgolint: '>=0.15.0' - peerDependenciesMeta: - oxlint-tsgolint: - optional: true - - pad-right@0.2.2: - resolution: {integrity: sha512-4cy8M95ioIGolCoMmm2cMntGR1lPLEbOMzOKu8bzjuJP6JpzEMQcDHmh7hHLYGgob+nKe1YHFMaG4V59HQa89g==} - engines: {node: '>=0.10.0'} - - parse-json@8.3.0: - resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} - engines: {node: '>=18'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-scurry@2.0.2: - resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} - engines: {node: 18 || 20 || >=22} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - - pixelmatch@7.1.0: - resolution: {integrity: sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==} - hasBin: true - - playwright-core@1.51.1: - resolution: {integrity: sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==} - engines: {node: '>=18'} - hasBin: true - - playwright@1.51.1: - resolution: {integrity: sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==} - engines: {node: '>=18'} - hasBin: true - - pngjs@7.0.0: - resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} - engines: {node: '>=14.19.0'} - - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} - engines: {node: ^10 || ^12 || >=14} - - progress@2.0.3: - resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} - engines: {node: '>=0.4.0'} - - property-expr@2.0.6: - resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} - - read-package-up@12.0.0: - resolution: {integrity: sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw==} - engines: {node: '>=20'} - - read-pkg@10.1.0: - resolution: {integrity: sha512-I8g2lArQiP78ll51UeMZojewtYgIRCKCWqZEgOO8c/uefTI+XDXvCSXu3+YNUaTNvZzobrL5+SqHjBrByRRTdg==} - engines: {node: '>=20'} - - reflect-metadata@0.2.2: - resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} - - regexp-match-indices@1.0.2: - resolution: {integrity: sha512-DwZuAkt8NF5mKwGGER1EGh2PRqyvhRhhLviH+R8y8dIuaQROlUfXjt4s9ZTXstIsSkptf06BSvwcEmmfheJJWQ==} - - regexp-tree@0.1.27: - resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} - hasBin: true - - repeat-string@1.6.1: - resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} - engines: {node: '>=0.10'} - - resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - - rolldown@1.0.0-rc.12: - resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - seed-random@2.2.0: - resolution: {integrity: sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ==} - - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} - engines: {node: '>=10'} - hasBin: true - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - sirv@3.0.2: - resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} - engines: {node: '>=18'} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - - spdx-correct@3.2.0: - resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} - - spdx-exceptions@2.5.0: - resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} - - spdx-expression-parse@3.0.1: - resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} - - spdx-license-ids@3.0.23: - resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} - - stackframe@1.3.4: - resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} - - std-env@4.0.0: - resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} - - string-argv@0.3.1: - resolution: {integrity: sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==} - engines: {node: '>=0.6.19'} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - - tagged-tag@1.0.0: - resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} - engines: {node: '>=20'} - - thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - - thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - - tiny-case@1.0.3: - resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} - - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - - tinyexec@1.0.4: - resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} - engines: {node: '>=18'} - - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} - - tinypool@2.1.0: - resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} - engines: {node: ^20.0.0 || >=22.0.0} - - toposort@2.0.2: - resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} - - totalist@3.0.1: - resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} - engines: {node: '>=6'} - - ts-dedent@2.2.0: - resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} - engines: {node: '>=6.10'} - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - tsx@4.21.0: - resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} - engines: {node: '>=18.0.0'} - hasBin: true - - type-fest@2.19.0: - resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} - engines: {node: '>=12.20'} - - type-fest@4.41.0: - resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} - engines: {node: '>=16'} - - type-fest@5.5.0: - resolution: {integrity: sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==} - engines: {node: '>=20'} - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} - - unicorn-magic@0.4.0: - resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} - engines: {node: '>=20'} - - upper-case-first@2.0.2: - resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} - - util-arity@1.1.0: - resolution: {integrity: sha512-kkyIsXKwemfSy8ZEoaIz06ApApnWsk5hQO0vLjZS6UkBiGiW++Jsyb8vSBoc0WKlffGoGs5yYy/j5pp8zckrFA==} - - validate-npm-package-license@3.0.4: - resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} - - vite-plus@0.1.14: - resolution: {integrity: sha512-p4pWlpZZNiEsHxPWNdeIU9iuPix3ydm3ficb0dXPggoyIkdotfXtvn2NPX9KwfiQImU72EVEs4+VYBZYNcUYrw==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - vite@8.0.3: - resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.0 - esbuild: ^0.27.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - xmlbuilder@15.1.1: - resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} - engines: {node: '>=8.0'} - - yaml@2.8.3: - resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} - engines: {node: '>= 14.6'} - hasBin: true - - yup@1.7.1: - resolution: {integrity: sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==} - -snapshots: - - '@babel/code-frame@7.29.0': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/helper-validator-identifier@7.28.5': {} - - '@colors/colors@1.5.0': - optional: true - - '@cucumber/ci-environment@13.0.0': {} - - '@cucumber/cucumber-expressions@19.0.0': - dependencies: - regexp-match-indices: 1.0.2 - - '@cucumber/cucumber@12.7.0': - dependencies: - '@cucumber/ci-environment': 13.0.0 - '@cucumber/cucumber-expressions': 19.0.0 - '@cucumber/gherkin': 38.0.0 - '@cucumber/gherkin-streams': 6.0.0(@cucumber/gherkin@38.0.0)(@cucumber/message-streams@4.0.1(@cucumber/messages@32.0.1))(@cucumber/messages@32.0.1) - '@cucumber/gherkin-utils': 11.0.0 - '@cucumber/html-formatter': 23.0.0(@cucumber/messages@32.0.1) - '@cucumber/junit-xml-formatter': 0.9.0(@cucumber/messages@32.0.1) - '@cucumber/message-streams': 4.0.1(@cucumber/messages@32.0.1) - '@cucumber/messages': 32.0.1 - '@cucumber/pretty-formatter': 1.0.1(@cucumber/cucumber@12.7.0)(@cucumber/messages@32.0.1) - '@cucumber/tag-expressions': 9.1.0 - assertion-error-formatter: 3.0.0 - capital-case: 1.0.4 - chalk: 4.1.2 - cli-table3: 0.6.5 - commander: 14.0.3 - debug: 4.4.3(supports-color@8.1.1) - error-stack-parser: 2.1.4 - figures: 3.2.0 - glob: 13.0.6 - has-ansi: 4.0.1 - indent-string: 4.0.0 - is-installed-globally: 0.4.0 - is-stream: 2.0.1 - knuth-shuffle-seeded: 1.0.6 - lodash.merge: 4.6.2 - lodash.mergewith: 4.6.2 - luxon: 3.7.2 - mime: 3.0.0 - mkdirp: 3.0.1 - mz: 2.7.0 - progress: 2.0.3 - read-package-up: 12.0.0 - semver: 7.7.4 - string-argv: 0.3.1 - supports-color: 8.1.1 - type-fest: 4.41.0 - util-arity: 1.1.0 - yaml: 2.8.3 - yup: 1.7.1 - - '@cucumber/gherkin-streams@6.0.0(@cucumber/gherkin@38.0.0)(@cucumber/message-streams@4.0.1(@cucumber/messages@32.0.1))(@cucumber/messages@32.0.1)': - dependencies: - '@cucumber/gherkin': 38.0.0 - '@cucumber/message-streams': 4.0.1(@cucumber/messages@32.0.1) - '@cucumber/messages': 32.0.1 - commander: 14.0.0 - source-map-support: 0.5.21 - - '@cucumber/gherkin-utils@11.0.0': - dependencies: - '@cucumber/gherkin': 38.0.0 - '@cucumber/messages': 32.0.1 - '@teppeis/multimaps': 3.0.0 - commander: 14.0.2 - source-map-support: 0.5.21 - - '@cucumber/gherkin@38.0.0': - dependencies: - '@cucumber/messages': 32.0.1 - - '@cucumber/html-formatter@23.0.0(@cucumber/messages@32.0.1)': - dependencies: - '@cucumber/messages': 32.0.1 - - '@cucumber/junit-xml-formatter@0.9.0(@cucumber/messages@32.0.1)': - dependencies: - '@cucumber/messages': 32.0.1 - '@cucumber/query': 14.7.0(@cucumber/messages@32.0.1) - '@teppeis/multimaps': 3.0.0 - luxon: 3.7.2 - xmlbuilder: 15.1.1 - - '@cucumber/message-streams@4.0.1(@cucumber/messages@32.0.1)': - dependencies: - '@cucumber/messages': 32.0.1 - - '@cucumber/messages@32.0.1': - dependencies: - class-transformer: 0.5.1 - reflect-metadata: 0.2.2 - - '@cucumber/pretty-formatter@1.0.1(@cucumber/cucumber@12.7.0)(@cucumber/messages@32.0.1)': - dependencies: - '@cucumber/cucumber': 12.7.0 - '@cucumber/messages': 32.0.1 - ansi-styles: 5.2.0 - cli-table3: 0.6.5 - figures: 3.2.0 - ts-dedent: 2.2.0 - - '@cucumber/query@14.7.0(@cucumber/messages@32.0.1)': - dependencies: - '@cucumber/messages': 32.0.1 - '@teppeis/multimaps': 3.0.0 - lodash.sortby: 4.7.0 - - '@cucumber/tag-expressions@9.1.0': {} - - '@emnapi/core@1.9.1': - dependencies: - '@emnapi/wasi-threads': 1.2.0 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.9.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@esbuild/aix-ppc64@0.27.4': - optional: true - - '@esbuild/android-arm64@0.27.4': - optional: true - - '@esbuild/android-arm@0.27.4': - optional: true - - '@esbuild/android-x64@0.27.4': - optional: true - - '@esbuild/darwin-arm64@0.27.4': - optional: true - - '@esbuild/darwin-x64@0.27.4': - optional: true - - '@esbuild/freebsd-arm64@0.27.4': - optional: true - - '@esbuild/freebsd-x64@0.27.4': - optional: true - - '@esbuild/linux-arm64@0.27.4': - optional: true - - '@esbuild/linux-arm@0.27.4': - optional: true - - '@esbuild/linux-ia32@0.27.4': - optional: true - - '@esbuild/linux-loong64@0.27.4': - optional: true - - '@esbuild/linux-mips64el@0.27.4': - optional: true - - '@esbuild/linux-ppc64@0.27.4': - optional: true - - '@esbuild/linux-riscv64@0.27.4': - optional: true - - '@esbuild/linux-s390x@0.27.4': - optional: true - - '@esbuild/linux-x64@0.27.4': - optional: true - - '@esbuild/netbsd-arm64@0.27.4': - optional: true - - '@esbuild/netbsd-x64@0.27.4': - optional: true - - '@esbuild/openbsd-arm64@0.27.4': - optional: true - - '@esbuild/openbsd-x64@0.27.4': - optional: true - - '@esbuild/openharmony-arm64@0.27.4': - optional: true - - '@esbuild/sunos-x64@0.27.4': - optional: true - - '@esbuild/win32-arm64@0.27.4': - optional: true - - '@esbuild/win32-ia32@0.27.4': - optional: true - - '@esbuild/win32-x64@0.27.4': - optional: true - - '@napi-rs/wasm-runtime@1.1.1': - dependencies: - '@emnapi/core': 1.9.1 - '@emnapi/runtime': 1.9.1 - '@tybys/wasm-util': 0.10.1 - optional: true - - '@oxc-project/runtime@0.121.0': {} - - '@oxc-project/types@0.122.0': {} - - '@oxfmt/binding-android-arm-eabi@0.42.0': - optional: true - - '@oxfmt/binding-android-arm64@0.42.0': - optional: true - - '@oxfmt/binding-darwin-arm64@0.42.0': - optional: true - - '@oxfmt/binding-darwin-x64@0.42.0': - optional: true - - '@oxfmt/binding-freebsd-x64@0.42.0': - optional: true - - '@oxfmt/binding-linux-arm-gnueabihf@0.42.0': - optional: true - - '@oxfmt/binding-linux-arm-musleabihf@0.42.0': - optional: true - - '@oxfmt/binding-linux-arm64-gnu@0.42.0': - optional: true - - '@oxfmt/binding-linux-arm64-musl@0.42.0': - optional: true - - '@oxfmt/binding-linux-ppc64-gnu@0.42.0': - optional: true - - '@oxfmt/binding-linux-riscv64-gnu@0.42.0': - optional: true - - '@oxfmt/binding-linux-riscv64-musl@0.42.0': - optional: true - - '@oxfmt/binding-linux-s390x-gnu@0.42.0': - optional: true - - '@oxfmt/binding-linux-x64-gnu@0.42.0': - optional: true - - '@oxfmt/binding-linux-x64-musl@0.42.0': - optional: true - - '@oxfmt/binding-openharmony-arm64@0.42.0': - optional: true - - '@oxfmt/binding-win32-arm64-msvc@0.42.0': - optional: true - - '@oxfmt/binding-win32-ia32-msvc@0.42.0': - optional: true - - '@oxfmt/binding-win32-x64-msvc@0.42.0': - optional: true - - '@oxlint-tsgolint/darwin-arm64@0.17.3': - optional: true - - '@oxlint-tsgolint/darwin-x64@0.17.3': - optional: true - - '@oxlint-tsgolint/linux-arm64@0.17.3': - optional: true - - '@oxlint-tsgolint/linux-x64@0.17.3': - optional: true - - '@oxlint-tsgolint/win32-arm64@0.17.3': - optional: true - - '@oxlint-tsgolint/win32-x64@0.17.3': - optional: true - - '@oxlint/binding-android-arm-eabi@1.57.0': - optional: true - - '@oxlint/binding-android-arm64@1.57.0': - optional: true - - '@oxlint/binding-darwin-arm64@1.57.0': - optional: true - - '@oxlint/binding-darwin-x64@1.57.0': - optional: true - - '@oxlint/binding-freebsd-x64@1.57.0': - optional: true - - '@oxlint/binding-linux-arm-gnueabihf@1.57.0': - optional: true - - '@oxlint/binding-linux-arm-musleabihf@1.57.0': - optional: true - - '@oxlint/binding-linux-arm64-gnu@1.57.0': - optional: true - - '@oxlint/binding-linux-arm64-musl@1.57.0': - optional: true - - '@oxlint/binding-linux-ppc64-gnu@1.57.0': - optional: true - - '@oxlint/binding-linux-riscv64-gnu@1.57.0': - optional: true - - '@oxlint/binding-linux-riscv64-musl@1.57.0': - optional: true - - '@oxlint/binding-linux-s390x-gnu@1.57.0': - optional: true - - '@oxlint/binding-linux-x64-gnu@1.57.0': - optional: true - - '@oxlint/binding-linux-x64-musl@1.57.0': - optional: true - - '@oxlint/binding-openharmony-arm64@1.57.0': - optional: true - - '@oxlint/binding-win32-arm64-msvc@1.57.0': - optional: true - - '@oxlint/binding-win32-ia32-msvc@1.57.0': - optional: true - - '@oxlint/binding-win32-x64-msvc@1.57.0': - optional: true - - '@playwright/test@1.51.1': - dependencies: - playwright: 1.51.1 - - '@polka/url@1.0.0-next.29': {} - - '@rolldown/binding-android-arm64@1.0.0-rc.12': - optional: true - - '@rolldown/binding-darwin-arm64@1.0.0-rc.12': - optional: true - - '@rolldown/binding-darwin-x64@1.0.0-rc.12': - optional: true - - '@rolldown/binding-freebsd-x64@1.0.0-rc.12': - optional: true - - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': - optional: true - - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': - optional: true - - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': - optional: true - - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': - optional: true - - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': - optional: true - - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': - optional: true - - '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': - optional: true - - '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': - optional: true - - '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': - dependencies: - '@napi-rs/wasm-runtime': 1.1.1 - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': - optional: true - - '@rolldown/pluginutils@1.0.0-rc.12': {} - - '@standard-schema/spec@1.1.0': {} - - '@teppeis/multimaps@3.0.0': {} - - '@tybys/wasm-util@0.10.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@types/chai@5.2.3': - dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 - - '@types/deep-eql@4.0.2': {} - - '@types/node@25.5.0': - dependencies: - undici-types: 7.18.2 - - '@types/normalize-package-data@2.4.4': {} - - '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': - dependencies: - '@oxc-project/runtime': 0.121.0 - '@oxc-project/types': 0.122.0 - lightningcss: 1.32.0 - postcss: 8.5.8 - optionalDependencies: - '@types/node': 25.5.0 - esbuild: 0.27.4 - fsevents: 2.3.3 - tsx: 4.21.0 - typescript: 5.9.3 - yaml: 2.8.3 - - '@voidzero-dev/vite-plus-darwin-arm64@0.1.14': - optional: true - - '@voidzero-dev/vite-plus-darwin-x64@0.1.14': - optional: true - - '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.14': - optional: true - - '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.14': - optional: true - - '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.14': - optional: true - - '@voidzero-dev/vite-plus-linux-x64-musl@0.1.14': - optional: true - - '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)': - dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@voidzero-dev/vite-plus-core': 0.1.14(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) - es-module-lexer: 1.7.0 - obug: 2.1.1 - pixelmatch: 7.1.0 - pngjs: 7.0.0 - sirv: 3.0.2 - std-env: 4.0.0 - tinybench: 2.9.0 - tinyexec: 1.0.4 - tinyglobby: 0.2.15 - vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(yaml@2.8.3) - ws: 8.20.0 - optionalDependencies: - '@types/node': 25.5.0 - transitivePeerDependencies: - - '@arethetypeswrong/core' - - '@tsdown/css' - - '@tsdown/exe' - - '@vitejs/devtools' - - bufferutil - - esbuild - - jiti - - less - - publint - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - typescript - - unplugin-unused - - utf-8-validate - - yaml - - '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.14': - optional: true - - '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.14': - optional: true - - ansi-regex@4.1.1: {} - - ansi-regex@5.0.1: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@5.2.0: {} - - any-promise@1.3.0: {} - - assertion-error-formatter@3.0.0: - dependencies: - diff: 4.0.4 - pad-right: 0.2.2 - repeat-string: 1.6.1 - - assertion-error@2.0.1: {} - - balanced-match@4.0.4: {} - - brace-expansion@5.0.5: - dependencies: - balanced-match: 4.0.4 - - buffer-from@1.1.2: {} - - cac@7.0.0: {} - - capital-case@1.0.4: - dependencies: - no-case: 3.0.4 - tslib: 2.8.1 - upper-case-first: 2.0.2 - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - class-transformer@0.5.1: {} - - cli-table3@0.6.5: - dependencies: - string-width: 4.2.3 - optionalDependencies: - '@colors/colors': 1.5.0 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - commander@14.0.0: {} - - commander@14.0.2: {} - - commander@14.0.3: {} - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - debug@4.4.3(supports-color@8.1.1): - dependencies: - ms: 2.1.3 - optionalDependencies: - supports-color: 8.1.1 - - detect-libc@2.1.2: {} - - diff@4.0.4: {} - - emoji-regex@8.0.0: {} - - error-stack-parser@2.1.4: - dependencies: - stackframe: 1.3.4 - - es-module-lexer@1.7.0: {} - - esbuild@0.27.4: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.4 - '@esbuild/android-arm': 0.27.4 - '@esbuild/android-arm64': 0.27.4 - '@esbuild/android-x64': 0.27.4 - '@esbuild/darwin-arm64': 0.27.4 - '@esbuild/darwin-x64': 0.27.4 - '@esbuild/freebsd-arm64': 0.27.4 - '@esbuild/freebsd-x64': 0.27.4 - '@esbuild/linux-arm': 0.27.4 - '@esbuild/linux-arm64': 0.27.4 - '@esbuild/linux-ia32': 0.27.4 - '@esbuild/linux-loong64': 0.27.4 - '@esbuild/linux-mips64el': 0.27.4 - '@esbuild/linux-ppc64': 0.27.4 - '@esbuild/linux-riscv64': 0.27.4 - '@esbuild/linux-s390x': 0.27.4 - '@esbuild/linux-x64': 0.27.4 - '@esbuild/netbsd-arm64': 0.27.4 - '@esbuild/netbsd-x64': 0.27.4 - '@esbuild/openbsd-arm64': 0.27.4 - '@esbuild/openbsd-x64': 0.27.4 - '@esbuild/openharmony-arm64': 0.27.4 - '@esbuild/sunos-x64': 0.27.4 - '@esbuild/win32-arm64': 0.27.4 - '@esbuild/win32-ia32': 0.27.4 - '@esbuild/win32-x64': 0.27.4 - - escape-string-regexp@1.0.5: {} - - fdir@6.5.0(picomatch@4.0.4): - optionalDependencies: - picomatch: 4.0.4 - - figures@3.2.0: - dependencies: - escape-string-regexp: 1.0.5 - - find-up-simple@1.0.1: {} - - fsevents@2.3.2: - optional: true - - fsevents@2.3.3: - optional: true - - get-tsconfig@4.13.7: - dependencies: - resolve-pkg-maps: 1.0.0 - - glob@13.0.6: - dependencies: - minimatch: 10.2.4 - minipass: 7.1.3 - path-scurry: 2.0.2 - - global-dirs@3.0.1: - dependencies: - ini: 2.0.0 - - has-ansi@4.0.1: - dependencies: - ansi-regex: 4.1.1 - - has-flag@4.0.0: {} - - hosted-git-info@9.0.2: - dependencies: - lru-cache: 11.2.7 - - indent-string@4.0.0: {} - - index-to-position@1.2.0: {} - - ini@2.0.0: {} - - is-fullwidth-code-point@3.0.0: {} - - is-installed-globally@0.4.0: - dependencies: - global-dirs: 3.0.1 - is-path-inside: 3.0.3 - - is-path-inside@3.0.3: {} - - is-stream@2.0.1: {} - - isexe@2.0.0: {} - - js-tokens@4.0.0: {} - - knuth-shuffle-seeded@1.0.6: - dependencies: - seed-random: 2.2.0 - - lightningcss-android-arm64@1.32.0: - optional: true - - lightningcss-darwin-arm64@1.32.0: - optional: true - - lightningcss-darwin-x64@1.32.0: - optional: true - - lightningcss-freebsd-x64@1.32.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.32.0: - optional: true - - lightningcss-linux-arm64-gnu@1.32.0: - optional: true - - lightningcss-linux-arm64-musl@1.32.0: - optional: true - - lightningcss-linux-x64-gnu@1.32.0: - optional: true - - lightningcss-linux-x64-musl@1.32.0: - optional: true - - lightningcss-win32-arm64-msvc@1.32.0: - optional: true - - lightningcss-win32-x64-msvc@1.32.0: - optional: true - - lightningcss@1.32.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.32.0 - lightningcss-darwin-arm64: 1.32.0 - lightningcss-darwin-x64: 1.32.0 - lightningcss-freebsd-x64: 1.32.0 - lightningcss-linux-arm-gnueabihf: 1.32.0 - lightningcss-linux-arm64-gnu: 1.32.0 - lightningcss-linux-arm64-musl: 1.32.0 - lightningcss-linux-x64-gnu: 1.32.0 - lightningcss-linux-x64-musl: 1.32.0 - lightningcss-win32-arm64-msvc: 1.32.0 - lightningcss-win32-x64-msvc: 1.32.0 - - lodash.merge@4.6.2: {} - - lodash.mergewith@4.6.2: {} - - lodash.sortby@4.7.0: {} - - lower-case@2.0.2: - dependencies: - tslib: 2.8.1 - - lru-cache@11.2.7: {} - - luxon@3.7.2: {} - - mime@3.0.0: {} - - minimatch@10.2.4: - dependencies: - brace-expansion: 5.0.5 - - minipass@7.1.3: {} - - mkdirp@3.0.1: {} - - mrmime@2.0.1: {} - - ms@2.1.3: {} - - mz@2.7.0: - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 - - nanoid@3.3.11: {} - - no-case@3.0.4: - dependencies: - lower-case: 2.0.2 - tslib: 2.8.1 - - normalize-package-data@8.0.0: - dependencies: - hosted-git-info: 9.0.2 - semver: 7.7.4 - validate-npm-package-license: 3.0.4 - - object-assign@4.1.1: {} - - obug@2.1.1: {} - - oxfmt@0.42.0: - dependencies: - tinypool: 2.1.0 - optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.42.0 - '@oxfmt/binding-android-arm64': 0.42.0 - '@oxfmt/binding-darwin-arm64': 0.42.0 - '@oxfmt/binding-darwin-x64': 0.42.0 - '@oxfmt/binding-freebsd-x64': 0.42.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.42.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.42.0 - '@oxfmt/binding-linux-arm64-gnu': 0.42.0 - '@oxfmt/binding-linux-arm64-musl': 0.42.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.42.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.42.0 - '@oxfmt/binding-linux-riscv64-musl': 0.42.0 - '@oxfmt/binding-linux-s390x-gnu': 0.42.0 - '@oxfmt/binding-linux-x64-gnu': 0.42.0 - '@oxfmt/binding-linux-x64-musl': 0.42.0 - '@oxfmt/binding-openharmony-arm64': 0.42.0 - '@oxfmt/binding-win32-arm64-msvc': 0.42.0 - '@oxfmt/binding-win32-ia32-msvc': 0.42.0 - '@oxfmt/binding-win32-x64-msvc': 0.42.0 - - oxlint-tsgolint@0.17.3: - optionalDependencies: - '@oxlint-tsgolint/darwin-arm64': 0.17.3 - '@oxlint-tsgolint/darwin-x64': 0.17.3 - '@oxlint-tsgolint/linux-arm64': 0.17.3 - '@oxlint-tsgolint/linux-x64': 0.17.3 - '@oxlint-tsgolint/win32-arm64': 0.17.3 - '@oxlint-tsgolint/win32-x64': 0.17.3 - - oxlint@1.57.0(oxlint-tsgolint@0.17.3): - optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.57.0 - '@oxlint/binding-android-arm64': 1.57.0 - '@oxlint/binding-darwin-arm64': 1.57.0 - '@oxlint/binding-darwin-x64': 1.57.0 - '@oxlint/binding-freebsd-x64': 1.57.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.57.0 - '@oxlint/binding-linux-arm-musleabihf': 1.57.0 - '@oxlint/binding-linux-arm64-gnu': 1.57.0 - '@oxlint/binding-linux-arm64-musl': 1.57.0 - '@oxlint/binding-linux-ppc64-gnu': 1.57.0 - '@oxlint/binding-linux-riscv64-gnu': 1.57.0 - '@oxlint/binding-linux-riscv64-musl': 1.57.0 - '@oxlint/binding-linux-s390x-gnu': 1.57.0 - '@oxlint/binding-linux-x64-gnu': 1.57.0 - '@oxlint/binding-linux-x64-musl': 1.57.0 - '@oxlint/binding-openharmony-arm64': 1.57.0 - '@oxlint/binding-win32-arm64-msvc': 1.57.0 - '@oxlint/binding-win32-ia32-msvc': 1.57.0 - '@oxlint/binding-win32-x64-msvc': 1.57.0 - oxlint-tsgolint: 0.17.3 - - pad-right@0.2.2: - dependencies: - repeat-string: 1.6.1 - - parse-json@8.3.0: - dependencies: - '@babel/code-frame': 7.29.0 - index-to-position: 1.2.0 - type-fest: 4.41.0 - - path-key@3.1.1: {} - - path-scurry@2.0.2: - dependencies: - lru-cache: 11.2.7 - minipass: 7.1.3 - - picocolors@1.1.1: {} - - picomatch@4.0.4: {} - - pixelmatch@7.1.0: - dependencies: - pngjs: 7.0.0 - - playwright-core@1.51.1: {} - - playwright@1.51.1: - dependencies: - playwright-core: 1.51.1 - optionalDependencies: - fsevents: 2.3.2 - - pngjs@7.0.0: {} - - postcss@8.5.8: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - progress@2.0.3: {} - - property-expr@2.0.6: {} - - read-package-up@12.0.0: - dependencies: - find-up-simple: 1.0.1 - read-pkg: 10.1.0 - type-fest: 5.5.0 - - read-pkg@10.1.0: - dependencies: - '@types/normalize-package-data': 2.4.4 - normalize-package-data: 8.0.0 - parse-json: 8.3.0 - type-fest: 5.5.0 - unicorn-magic: 0.4.0 - - reflect-metadata@0.2.2: {} - - regexp-match-indices@1.0.2: - dependencies: - regexp-tree: 0.1.27 - - regexp-tree@0.1.27: {} - - repeat-string@1.6.1: {} - - resolve-pkg-maps@1.0.0: {} - - rolldown@1.0.0-rc.12: - dependencies: - '@oxc-project/types': 0.122.0 - '@rolldown/pluginutils': 1.0.0-rc.12 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.12 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 - '@rolldown/binding-darwin-x64': 1.0.0-rc.12 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 - - seed-random@2.2.0: {} - - semver@7.7.4: {} - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - sirv@3.0.2: - dependencies: - '@polka/url': 1.0.0-next.29 - mrmime: 2.0.1 - totalist: 3.0.1 - - source-map-js@1.2.1: {} - - source-map-support@0.5.21: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - - source-map@0.6.1: {} - - spdx-correct@3.2.0: - dependencies: - spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.23 - - spdx-exceptions@2.5.0: {} - - spdx-expression-parse@3.0.1: - dependencies: - spdx-exceptions: 2.5.0 - spdx-license-ids: 3.0.23 - - spdx-license-ids@3.0.23: {} - - stackframe@1.3.4: {} - - std-env@4.0.0: {} - - string-argv@0.3.1: {} - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - - tagged-tag@1.0.0: {} - - thenify-all@1.6.0: - dependencies: - thenify: 3.3.1 - - thenify@3.3.1: - dependencies: - any-promise: 1.3.0 - - tiny-case@1.0.3: {} - - tinybench@2.9.0: {} - - tinyexec@1.0.4: {} - - tinyglobby@0.2.15: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - - tinypool@2.1.0: {} - - toposort@2.0.2: {} - - totalist@3.0.1: {} - - ts-dedent@2.2.0: {} - - tslib@2.8.1: {} - - tsx@4.21.0: - dependencies: - esbuild: 0.27.4 - get-tsconfig: 4.13.7 - optionalDependencies: - fsevents: 2.3.3 - - type-fest@2.19.0: {} - - type-fest@4.41.0: {} - - type-fest@5.5.0: - dependencies: - tagged-tag: 1.0.0 - - typescript@5.9.3: {} - - undici-types@7.18.2: {} - - unicorn-magic@0.4.0: {} - - upper-case-first@2.0.2: - dependencies: - tslib: 2.8.1 - - util-arity@1.1.0: {} - - validate-npm-package-license@3.0.4: - dependencies: - spdx-correct: 3.2.0 - spdx-expression-parse: 3.0.1 - - vite-plus@0.1.14(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3): - dependencies: - '@oxc-project/types': 0.122.0 - '@voidzero-dev/vite-plus-core': 0.1.14(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) - '@voidzero-dev/vite-plus-test': 0.1.14(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) - cac: 7.0.0 - cross-spawn: 7.0.6 - oxfmt: 0.42.0 - oxlint: 1.57.0(oxlint-tsgolint@0.17.3) - oxlint-tsgolint: 0.17.3 - picocolors: 1.1.1 - optionalDependencies: - '@voidzero-dev/vite-plus-darwin-arm64': 0.1.14 - '@voidzero-dev/vite-plus-darwin-x64': 0.1.14 - '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.14 - '@voidzero-dev/vite-plus-linux-arm64-musl': 0.1.14 - '@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.14 - '@voidzero-dev/vite-plus-linux-x64-musl': 0.1.14 - '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.14 - '@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.14 - transitivePeerDependencies: - - '@arethetypeswrong/core' - - '@edge-runtime/vm' - - '@opentelemetry/api' - - '@tsdown/css' - - '@tsdown/exe' - - '@types/node' - - '@vitejs/devtools' - - '@vitest/ui' - - bufferutil - - esbuild - - happy-dom - - jiti - - jsdom - - less - - publint - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - typescript - - unplugin-unused - - utf-8-validate - - vite - - yaml - - vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(yaml@2.8.3): - dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.8 - rolldown: 1.0.0-rc.12 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 25.5.0 - esbuild: 0.27.4 - fsevents: 2.3.3 - tsx: 4.21.0 - yaml: 2.8.3 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - ws@8.20.0: {} - - xmlbuilder@15.1.1: {} - - yaml@2.8.3: {} - - yup@1.7.1: - dependencies: - property-expr: 2.0.6 - tiny-case: 1.0.3 - toposort: 2.0.2 - type-fest: 2.19.0 diff --git a/package.json b/package.json new file mode 100644 index 0000000000..07f1e16153 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "dify", + "private": true, + "engines": { + "node": "^22.22.1" + }, + "packageManager": "pnpm@10.33.0", + "devDependencies": { + "taze": "catalog:" + } +} diff --git a/web/pnpm-lock.yaml b/pnpm-lock.yaml similarity index 77% rename from web/pnpm-lock.yaml rename to pnpm-lock.yaml index cd1a8a8556..01a96c5585 100644 --- a/web/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,564 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +catalogs: + default: + '@amplitude/analytics-browser': + specifier: 2.38.0 + version: 2.38.0 + '@amplitude/plugin-session-replay-browser': + specifier: 1.27.5 + version: 1.27.5 + '@antfu/eslint-config': + specifier: 7.7.3 + version: 7.7.3 + '@base-ui/react': + specifier: 1.3.0 + version: 1.3.0 + '@chromatic-com/storybook': + specifier: 5.1.1 + version: 5.1.1 + '@cucumber/cucumber': + specifier: 12.7.0 + version: 12.7.0 + '@egoist/tailwindcss-icons': + specifier: 1.9.2 + version: 1.9.2 + '@emoji-mart/data': + specifier: 1.2.1 + version: 1.2.1 + '@eslint-react/eslint-plugin': + specifier: 3.0.0 + version: 3.0.0 + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1 + '@floating-ui/react': + specifier: 0.27.19 + version: 0.27.19 + '@formatjs/intl-localematcher': + specifier: 0.8.2 + version: 0.8.2 + '@headlessui/react': + specifier: 2.2.9 + version: 2.2.9 + '@heroicons/react': + specifier: 2.2.0 + version: 2.2.0 + '@hono/node-server': + specifier: 1.19.11 + version: 1.19.11 + '@iconify-json/heroicons': + specifier: 1.2.3 + version: 1.2.3 + '@iconify-json/ri': + specifier: 1.2.10 + version: 1.2.10 + '@lexical/link': + specifier: 0.42.0 + version: 0.42.0 + '@lexical/list': + specifier: 0.42.0 + version: 0.42.0 + '@lexical/react': + specifier: 0.42.0 + version: 0.42.0 + '@lexical/selection': + specifier: 0.42.0 + version: 0.42.0 + '@lexical/text': + specifier: 0.42.0 + version: 0.42.0 + '@lexical/utils': + specifier: 0.42.0 + version: 0.42.0 + '@mdx-js/loader': + specifier: 3.1.1 + version: 3.1.1 + '@mdx-js/react': + specifier: 3.1.1 + version: 3.1.1 + '@mdx-js/rollup': + specifier: 3.1.1 + version: 3.1.1 + '@monaco-editor/react': + specifier: 4.7.0 + version: 4.7.0 + '@next/eslint-plugin-next': + specifier: 16.2.1 + version: 16.2.1 + '@next/mdx': + specifier: 16.2.1 + version: 16.2.1 + '@orpc/client': + specifier: 1.13.13 + version: 1.13.13 + '@orpc/contract': + specifier: 1.13.13 + version: 1.13.13 + '@orpc/openapi-client': + specifier: 1.13.13 + version: 1.13.13 + '@orpc/tanstack-query': + specifier: 1.13.13 + version: 1.13.13 + '@playwright/test': + specifier: 1.58.2 + version: 1.58.2 + '@remixicon/react': + specifier: 4.9.0 + version: 4.9.0 + '@rgrove/parse-xml': + specifier: 4.2.0 + version: 4.2.0 + '@sentry/react': + specifier: 10.46.0 + version: 10.46.0 + '@storybook/addon-docs': + specifier: 10.3.3 + version: 10.3.3 + '@storybook/addon-links': + specifier: 10.3.3 + version: 10.3.3 + '@storybook/addon-onboarding': + specifier: 10.3.3 + version: 10.3.3 + '@storybook/addon-themes': + specifier: 10.3.3 + version: 10.3.3 + '@storybook/nextjs-vite': + specifier: 10.3.3 + version: 10.3.3 + '@storybook/react': + specifier: 10.3.3 + version: 10.3.3 + '@streamdown/math': + specifier: 1.0.2 + version: 1.0.2 + '@svgdotjs/svg.js': + specifier: 3.2.5 + version: 3.2.5 + '@t3-oss/env-nextjs': + specifier: 0.13.11 + version: 0.13.11 + '@tailwindcss/typography': + specifier: 0.5.19 + version: 0.5.19 + '@tanstack/eslint-plugin-query': + specifier: 5.95.2 + version: 5.95.2 + '@tanstack/react-devtools': + specifier: 0.10.0 + version: 0.10.0 + '@tanstack/react-form': + specifier: 1.28.5 + version: 1.28.5 + '@tanstack/react-form-devtools': + specifier: 0.2.19 + version: 0.2.19 + '@tanstack/react-query': + specifier: 5.95.2 + version: 5.95.2 + '@tanstack/react-query-devtools': + specifier: 5.95.2 + version: 5.95.2 + '@testing-library/dom': + specifier: 10.4.1 + version: 10.4.1 + '@testing-library/jest-dom': + specifier: 6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: 16.3.2 + version: 16.3.2 + '@testing-library/user-event': + specifier: 14.6.1 + version: 14.6.1 + '@tsslint/cli': + specifier: 3.0.2 + version: 3.0.2 + '@tsslint/compat-eslint': + specifier: 3.0.2 + version: 3.0.2 + '@tsslint/config': + specifier: 3.0.2 + version: 3.0.2 + '@types/js-cookie': + specifier: 3.0.6 + version: 3.0.6 + '@types/js-yaml': + specifier: 4.0.9 + version: 4.0.9 + '@types/negotiator': + specifier: 0.6.4 + version: 0.6.4 + '@types/node': + specifier: 25.5.0 + version: 25.5.0 + '@types/postcss-js': + specifier: 4.1.0 + version: 4.1.0 + '@types/qs': + specifier: 6.15.0 + version: 6.15.0 + '@types/react': + specifier: 19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: 19.2.3 + version: 19.2.3 + '@types/react-syntax-highlighter': + specifier: 15.5.13 + version: 15.5.13 + '@types/react-window': + specifier: 1.8.8 + version: 1.8.8 + '@types/sortablejs': + specifier: 1.15.9 + version: 1.15.9 + '@typescript-eslint/eslint-plugin': + specifier: ^8.57.2 + version: 8.57.2 + '@typescript-eslint/parser': + specifier: 8.57.2 + version: 8.57.2 + '@typescript/native-preview': + specifier: 7.0.0-dev.20260329.1 + version: 7.0.0-dev.20260329.1 + '@vitejs/plugin-react': + specifier: 6.0.1 + version: 6.0.1 + '@vitejs/plugin-rsc': + specifier: 0.5.21 + version: 0.5.21 + '@vitest/coverage-v8': + specifier: 4.1.2 + version: 4.1.2 + abcjs: + specifier: 6.6.2 + version: 6.6.2 + agentation: + specifier: 3.0.2 + version: 3.0.2 + ahooks: + specifier: 3.9.7 + version: 3.9.7 + autoprefixer: + specifier: 10.4.27 + version: 10.4.27 + axios: + specifier: ^1.14.0 + version: 1.14.0 + class-variance-authority: + specifier: 0.7.1 + version: 0.7.1 + clsx: + specifier: 2.1.1 + version: 2.1.1 + cmdk: + specifier: 1.1.1 + version: 1.1.1 + code-inspector-plugin: + specifier: 1.4.5 + version: 1.4.5 + copy-to-clipboard: + specifier: 3.3.3 + version: 3.3.3 + cron-parser: + specifier: 5.5.0 + version: 5.5.0 + dayjs: + specifier: 1.11.20 + version: 1.11.20 + decimal.js: + specifier: 10.6.0 + version: 10.6.0 + dompurify: + specifier: 3.3.3 + version: 3.3.3 + echarts: + specifier: 6.0.0 + version: 6.0.0 + echarts-for-react: + specifier: 3.0.6 + version: 3.0.6 + elkjs: + specifier: 0.11.1 + version: 0.11.1 + embla-carousel-autoplay: + specifier: 8.6.0 + version: 8.6.0 + embla-carousel-react: + specifier: 8.6.0 + version: 8.6.0 + emoji-mart: + specifier: 5.6.0 + version: 5.6.0 + es-toolkit: + specifier: 1.45.1 + version: 1.45.1 + eslint: + specifier: 10.1.0 + version: 10.1.0 + eslint-markdown: + specifier: 0.6.0 + version: 0.6.0 + eslint-plugin-better-tailwindcss: + specifier: 4.3.2 + version: 4.3.2 + eslint-plugin-hyoban: + specifier: 0.14.1 + version: 0.14.1 + eslint-plugin-markdown-preferences: + specifier: 0.40.3 + version: 0.40.3 + eslint-plugin-no-barrel-files: + specifier: 1.2.2 + version: 1.2.2 + eslint-plugin-react-hooks: + specifier: 7.0.1 + version: 7.0.1 + eslint-plugin-react-refresh: + specifier: 0.5.2 + version: 0.5.2 + eslint-plugin-sonarjs: + specifier: 4.0.2 + version: 4.0.2 + eslint-plugin-storybook: + specifier: 10.3.3 + version: 10.3.3 + fast-deep-equal: + specifier: 3.1.3 + version: 3.1.3 + foxact: + specifier: 0.3.0 + version: 0.3.0 + happy-dom: + specifier: 20.8.9 + version: 20.8.9 + hono: + specifier: 4.12.9 + version: 4.12.9 + html-entities: + specifier: 2.6.0 + version: 2.6.0 + html-to-image: + specifier: 1.11.13 + version: 1.11.13 + husky: + specifier: 9.1.7 + version: 9.1.7 + i18next: + specifier: 25.10.10 + version: 25.10.10 + i18next-resources-to-backend: + specifier: 1.2.1 + version: 1.2.1 + iconify-import-svg: + specifier: 0.1.2 + version: 0.1.2 + immer: + specifier: 11.1.4 + version: 11.1.4 + jotai: + specifier: 2.19.0 + version: 2.19.0 + js-audio-recorder: + specifier: 1.0.7 + version: 1.0.7 + js-cookie: + specifier: 3.0.5 + version: 3.0.5 + js-yaml: + specifier: 4.1.1 + version: 4.1.1 + jsonschema: + specifier: 1.5.0 + version: 1.5.0 + katex: + specifier: 0.16.44 + version: 0.16.44 + knip: + specifier: 6.1.0 + version: 6.1.0 + ky: + specifier: 1.14.3 + version: 1.14.3 + lamejs: + specifier: 1.2.1 + version: 1.2.1 + lexical: + specifier: 0.42.0 + version: 0.42.0 + lint-staged: + specifier: 16.4.0 + version: 16.4.0 + mermaid: + specifier: 11.13.0 + version: 11.13.0 + mime: + specifier: 4.1.0 + version: 4.1.0 + mitt: + specifier: 3.0.1 + version: 3.0.1 + negotiator: + specifier: 1.0.0 + version: 1.0.0 + next: + specifier: 16.2.1 + version: 16.2.1 + next-themes: + specifier: 0.4.6 + version: 0.4.6 + nuqs: + specifier: 2.8.9 + version: 2.8.9 + pinyin-pro: + specifier: 3.28.0 + version: 3.28.0 + postcss: + specifier: 8.5.8 + version: 8.5.8 + postcss-js: + specifier: 5.1.0 + version: 5.1.0 + qrcode.react: + specifier: 4.2.0 + version: 4.2.0 + qs: + specifier: 6.15.0 + version: 6.15.0 + react: + specifier: 19.2.4 + version: 19.2.4 + react-18-input-autosize: + specifier: 3.0.0 + version: 3.0.0 + react-dom: + specifier: 19.2.4 + version: 19.2.4 + react-easy-crop: + specifier: 5.5.7 + version: 5.5.7 + react-hotkeys-hook: + specifier: 5.2.4 + version: 5.2.4 + react-i18next: + specifier: 16.6.6 + version: 16.6.6 + react-multi-email: + specifier: 1.0.25 + version: 1.0.25 + react-papaparse: + specifier: 4.4.0 + version: 4.4.0 + react-pdf-highlighter: + specifier: 8.0.0-rc.0 + version: 8.0.0-rc.0 + react-server-dom-webpack: + specifier: 19.2.4 + version: 19.2.4 + react-sortablejs: + specifier: 6.1.4 + version: 6.1.4 + react-syntax-highlighter: + specifier: 15.6.6 + version: 15.6.6 + react-textarea-autosize: + specifier: 8.5.9 + version: 8.5.9 + react-window: + specifier: 1.8.11 + version: 1.8.11 + reactflow: + specifier: 11.11.4 + version: 11.11.4 + remark-breaks: + specifier: 4.0.0 + version: 4.0.0 + remark-directive: + specifier: 4.0.0 + version: 4.0.0 + sass: + specifier: 1.98.0 + version: 1.98.0 + scheduler: + specifier: 0.27.0 + version: 0.27.0 + sharp: + specifier: 0.34.5 + version: 0.34.5 + sortablejs: + specifier: 1.15.7 + version: 1.15.7 + std-semver: + specifier: 1.0.8 + version: 1.0.8 + storybook: + specifier: 10.3.3 + version: 10.3.3 + streamdown: + specifier: 2.5.0 + version: 2.5.0 + string-ts: + specifier: 2.3.1 + version: 2.3.1 + tailwind-merge: + specifier: 2.6.1 + version: 2.6.1 + tailwindcss: + specifier: 3.4.19 + version: 3.4.19 + taze: + specifier: 19.10.0 + version: 19.10.0 + tldts: + specifier: 7.0.27 + version: 7.0.27 + tsup: + specifier: ^8.5.1 + version: 8.5.1 + tsx: + specifier: 4.21.0 + version: 4.21.0 + typescript: + specifier: 5.9.3 + version: 5.9.3 + uglify-js: + specifier: 3.19.3 + version: 3.19.3 + unist-util-visit: + specifier: 5.1.0 + version: 5.1.0 + use-context-selector: + specifier: 2.0.0 + version: 2.0.0 + uuid: + specifier: 13.0.0 + version: 13.0.0 + vinext: + specifier: 0.0.38 + version: 0.0.38 + vite-plugin-inspect: + specifier: 12.0.0-beta.1 + version: 12.0.0-beta.1 + vite-plus: + specifier: 0.1.14 + version: 0.1.14 + vitest-canvas-mock: + specifier: 1.1.4 + version: 1.1.4 + zod: + specifier: 4.3.6 + version: 4.3.6 + zundo: + specifier: 2.3.0 + version: 2.3.0 + zustand: + specifier: 5.0.12 + version: 5.0.12 + overrides: '@lexical/code': npm:lexical-code-no-prism@0.41.0 '@monaco-editor/loader': 1.7.0 @@ -21,6 +579,7 @@ overrides: dompurify@>=3.1.3 <=3.3.1: 3.3.2 es-iterator-helpers: npm:@nolyfill/es-iterator-helpers@^1.0.21 esbuild@<0.27.2: 0.27.2 + flatted@<=3.4.1: 3.4.2 glob@>=10.2.0 <10.5.0: 11.1.0 hasown: npm:@nolyfill/hasown@^1.0.44 is-arguments: npm:@nolyfill/is-arguments@^1.0.44 @@ -55,8 +614,8 @@ overrides: tar@<=7.5.10: 7.5.11 typed-array-buffer: npm:@nolyfill/typed-array-buffer@^1.0.44 undici@>=7.0.0 <7.24.0: 7.24.0 - vite: npm:@voidzero-dev/vite-plus-core@0.1.13 - vitest: npm:@voidzero-dev/vite-plus-test@0.1.13 + vite: npm:@voidzero-dev/vite-plus-core@0.1.14 + vitest: npm:@voidzero-dev/vite-plus-test@0.1.14 which-typed-array: npm:@nolyfill/which-typed-array@^1.0.44 yaml@>=2.0.0 <2.8.3: 2.8.3 yauzl@<3.2.1: 3.2.1 @@ -64,554 +623,612 @@ overrides: importers: .: + devDependencies: + taze: + specifier: 'catalog:' + version: 19.10.0 + + e2e: + devDependencies: + '@cucumber/cucumber': + specifier: 'catalog:' + version: 12.7.0 + '@playwright/test': + specifier: 'catalog:' + version: 1.58.2 + '@types/node': + specifier: 'catalog:' + version: 25.5.0 + tsx: + specifier: 'catalog:' + version: 4.21.0 + typescript: + specifier: 'catalog:' + version: 5.9.3 + vite-plus: + specifier: 'catalog:' + version: 0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) + + sdks/nodejs-client: + dependencies: + axios: + specifier: 'catalog:' + version: 1.14.0 + devDependencies: + '@eslint/js': + specifier: 'catalog:' + version: 10.0.1(eslint@10.1.0(jiti@2.6.1)) + '@types/node': + specifier: 'catalog:' + version: 25.5.0 + '@typescript-eslint/eslint-plugin': + specifier: 'catalog:' + version: 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: 'catalog:' + version: 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@vitest/coverage-v8': + specifier: 'catalog:' + version: 4.1.2(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)) + eslint: + specifier: 'catalog:' + version: 10.1.0(jiti@2.6.1) + tsup: + specifier: 'catalog:' + version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + typescript: + specifier: 'catalog:' + version: 5.9.3 + vitest: + specifier: npm:@voidzero-dev/vite-plus-test@0.1.14 + version: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)' + + web: dependencies: '@amplitude/analytics-browser': - specifier: 2.37.0 - version: 2.37.0 + specifier: 'catalog:' + version: 2.38.0 '@amplitude/plugin-session-replay-browser': - specifier: 1.27.1 - version: 1.27.1(@amplitude/rrweb@2.0.0-alpha.36)(rollup@4.59.0) + specifier: 'catalog:' + version: 1.27.5(@amplitude/rrweb@2.0.0-alpha.37)(rollup@4.59.0) '@base-ui/react': - specifier: 1.3.0 + specifier: 'catalog:' version: 1.3.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@emoji-mart/data': - specifier: 1.2.1 + specifier: 'catalog:' version: 1.2.1 '@floating-ui/react': - specifier: 0.27.19 + specifier: 'catalog:' version: 0.27.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@formatjs/intl-localematcher': - specifier: 0.8.2 + specifier: 'catalog:' version: 0.8.2 '@headlessui/react': - specifier: 2.2.9 + specifier: 'catalog:' version: 2.2.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@heroicons/react': - specifier: 2.2.0 + specifier: 'catalog:' version: 2.2.0(react@19.2.4) '@lexical/code': specifier: npm:lexical-code-no-prism@0.41.0 version: lexical-code-no-prism@0.41.0(@lexical/utils@0.42.0)(lexical@0.42.0) '@lexical/link': - specifier: 0.42.0 + specifier: 'catalog:' version: 0.42.0 '@lexical/list': - specifier: 0.42.0 + specifier: 'catalog:' version: 0.42.0 '@lexical/react': - specifier: 0.42.0 + specifier: 'catalog:' version: 0.42.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(yjs@13.6.30) '@lexical/selection': - specifier: 0.42.0 + specifier: 'catalog:' version: 0.42.0 '@lexical/text': - specifier: 0.42.0 + specifier: 'catalog:' version: 0.42.0 '@lexical/utils': - specifier: 0.42.0 + specifier: 'catalog:' version: 0.42.0 '@monaco-editor/react': - specifier: 4.7.0 + specifier: 'catalog:' version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@orpc/client': - specifier: 1.13.9 - version: 1.13.9 + specifier: 'catalog:' + version: 1.13.13 '@orpc/contract': - specifier: 1.13.9 - version: 1.13.9 + specifier: 'catalog:' + version: 1.13.13 '@orpc/openapi-client': - specifier: 1.13.9 - version: 1.13.9 + specifier: 'catalog:' + version: 1.13.13 '@orpc/tanstack-query': - specifier: 1.13.9 - version: 1.13.9(@orpc/client@1.13.9)(@tanstack/query-core@5.95.0) + specifier: 'catalog:' + version: 1.13.13(@orpc/client@1.13.13)(@tanstack/query-core@5.95.2) '@remixicon/react': - specifier: 4.9.0 + specifier: 'catalog:' version: 4.9.0(react@19.2.4) '@sentry/react': - specifier: 10.45.0 - version: 10.45.0(react@19.2.4) + specifier: 'catalog:' + version: 10.46.0(react@19.2.4) '@streamdown/math': - specifier: 1.0.2 + specifier: 'catalog:' version: 1.0.2(react@19.2.4) '@svgdotjs/svg.js': - specifier: 3.2.5 + specifier: 'catalog:' version: 3.2.5 '@t3-oss/env-nextjs': - specifier: 0.13.11 - version: 0.13.11(typescript@5.9.3)(valibot@1.3.0(typescript@5.9.3))(zod@4.3.6) + specifier: 'catalog:' + version: 0.13.11(typescript@5.9.3)(valibot@1.3.1(typescript@5.9.3))(zod@4.3.6) '@tailwindcss/typography': - specifier: 0.5.19 + specifier: 'catalog:' version: 0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3)) '@tanstack/react-form': - specifier: 1.28.5 + specifier: 'catalog:' version: 1.28.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-query': - specifier: 5.95.0 - version: 5.95.0(react@19.2.4) + specifier: 'catalog:' + version: 5.95.2(react@19.2.4) abcjs: - specifier: 6.6.2 + specifier: 'catalog:' version: 6.6.2 ahooks: - specifier: 3.9.6 - version: 3.9.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 'catalog:' + version: 3.9.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) class-variance-authority: - specifier: 0.7.1 + specifier: 'catalog:' version: 0.7.1 clsx: - specifier: 2.1.1 + specifier: 'catalog:' version: 2.1.1 cmdk: - specifier: 1.1.1 + specifier: 'catalog:' version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) copy-to-clipboard: - specifier: 3.3.3 + specifier: 'catalog:' version: 3.3.3 cron-parser: - specifier: 5.5.0 + specifier: 'catalog:' version: 5.5.0 dayjs: - specifier: 1.11.20 + specifier: 'catalog:' version: 1.11.20 decimal.js: - specifier: 10.6.0 + specifier: 'catalog:' version: 10.6.0 dompurify: - specifier: 3.3.3 + specifier: 'catalog:' version: 3.3.3 echarts: - specifier: 6.0.0 + specifier: 'catalog:' version: 6.0.0 echarts-for-react: - specifier: 3.0.6 + specifier: 'catalog:' version: 3.0.6(echarts@6.0.0)(react@19.2.4) elkjs: - specifier: 0.11.1 + specifier: 'catalog:' version: 0.11.1 embla-carousel-autoplay: - specifier: 8.6.0 + specifier: 'catalog:' version: 8.6.0(embla-carousel@8.6.0) embla-carousel-react: - specifier: 8.6.0 + specifier: 'catalog:' version: 8.6.0(react@19.2.4) emoji-mart: - specifier: 5.6.0 + specifier: 'catalog:' version: 5.6.0 es-toolkit: - specifier: 1.45.1 + specifier: 'catalog:' version: 1.45.1 fast-deep-equal: - specifier: 3.1.3 + specifier: 'catalog:' version: 3.1.3 foxact: - specifier: 0.3.0 + specifier: 'catalog:' version: 0.3.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) html-entities: - specifier: 2.6.0 + specifier: 'catalog:' version: 2.6.0 html-to-image: - specifier: 1.11.13 + specifier: 'catalog:' version: 1.11.13 i18next: - specifier: 25.10.4 - version: 25.10.4(typescript@5.9.3) + specifier: 'catalog:' + version: 25.10.10(typescript@5.9.3) i18next-resources-to-backend: - specifier: 1.2.1 + specifier: 'catalog:' version: 1.2.1 immer: - specifier: 11.1.4 + specifier: 'catalog:' version: 11.1.4 jotai: - specifier: 2.18.1 - version: 2.18.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4) + specifier: 'catalog:' + version: 2.19.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4) js-audio-recorder: - specifier: 1.0.7 + specifier: 'catalog:' version: 1.0.7 js-cookie: - specifier: 3.0.5 + specifier: 'catalog:' version: 3.0.5 js-yaml: - specifier: 4.1.1 + specifier: 'catalog:' version: 4.1.1 jsonschema: - specifier: 1.5.0 + specifier: 'catalog:' version: 1.5.0 katex: - specifier: 0.16.40 - version: 0.16.40 + specifier: 'catalog:' + version: 0.16.44 ky: - specifier: 1.14.3 + specifier: 'catalog:' version: 1.14.3 lamejs: - specifier: 1.2.1 + specifier: 'catalog:' version: 1.2.1 lexical: - specifier: 0.42.0 + specifier: 'catalog:' version: 0.42.0 mermaid: - specifier: 11.13.0 + specifier: 'catalog:' version: 11.13.0 mime: - specifier: 4.1.0 + specifier: 'catalog:' version: 4.1.0 mitt: - specifier: 3.0.1 + specifier: 'catalog:' version: 3.0.1 negotiator: - specifier: 1.0.0 + specifier: 'catalog:' version: 1.0.0 next: - specifier: 16.2.1 - version: 16.2.1(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) + specifier: 'catalog:' + version: 16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) next-themes: - specifier: 0.4.6 + specifier: 'catalog:' version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) nuqs: - specifier: 2.8.9 - version: 2.8.9(next@16.2.1(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react@19.2.4) + specifier: 'catalog:' + version: 2.8.9(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react@19.2.4) pinyin-pro: - specifier: 3.28.0 + specifier: 'catalog:' version: 3.28.0 qrcode.react: - specifier: 4.2.0 + specifier: 'catalog:' version: 4.2.0(react@19.2.4) qs: - specifier: 6.15.0 + specifier: 'catalog:' version: 6.15.0 react: - specifier: 19.2.4 + specifier: 'catalog:' version: 19.2.4 react-18-input-autosize: - specifier: 3.0.0 + specifier: 'catalog:' version: 3.0.0(react@19.2.4) react-dom: - specifier: 19.2.4 + specifier: 'catalog:' version: 19.2.4(react@19.2.4) react-easy-crop: - specifier: 5.5.6 - version: 5.5.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 'catalog:' + version: 5.5.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-hotkeys-hook: - specifier: 5.2.4 + specifier: 'catalog:' version: 5.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-i18next: - specifier: 16.6.1 - version: 16.6.1(i18next@25.10.4(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + specifier: 'catalog:' + version: 16.6.6(i18next@25.10.10(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) react-multi-email: - specifier: 1.0.25 + specifier: 'catalog:' version: 1.0.25(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-papaparse: - specifier: 4.4.0 + specifier: 'catalog:' version: 4.4.0 react-pdf-highlighter: - specifier: 8.0.0-rc.0 + specifier: 'catalog:' version: 8.0.0-rc.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-sortablejs: - specifier: 6.1.4 + specifier: 'catalog:' version: 6.1.4(@types/sortablejs@1.15.9)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sortablejs@1.15.7) react-syntax-highlighter: - specifier: 15.6.6 + specifier: 'catalog:' version: 15.6.6(react@19.2.4) react-textarea-autosize: - specifier: 8.5.9 + specifier: 'catalog:' version: 8.5.9(@types/react@19.2.14)(react@19.2.4) react-window: - specifier: 1.8.11 + specifier: 'catalog:' version: 1.8.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4) reactflow: - specifier: 11.11.4 + specifier: 'catalog:' version: 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) remark-breaks: - specifier: 4.0.0 + specifier: 'catalog:' version: 4.0.0 remark-directive: - specifier: 4.0.0 + specifier: 'catalog:' version: 4.0.0 scheduler: - specifier: 0.27.0 + specifier: 'catalog:' version: 0.27.0 sharp: - specifier: 0.34.5 + specifier: 'catalog:' version: 0.34.5 sortablejs: - specifier: 1.15.7 + specifier: 'catalog:' version: 1.15.7 std-semver: - specifier: 1.0.8 + specifier: 'catalog:' version: 1.0.8 streamdown: - specifier: 2.5.0 + specifier: 'catalog:' version: 2.5.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) string-ts: - specifier: 2.3.1 + specifier: 'catalog:' version: 2.3.1 tailwind-merge: - specifier: 2.6.1 + specifier: 'catalog:' version: 2.6.1 tldts: - specifier: 7.0.27 + specifier: 'catalog:' version: 7.0.27 unist-util-visit: - specifier: 5.1.0 + specifier: 'catalog:' version: 5.1.0 use-context-selector: - specifier: 2.0.0 + specifier: 'catalog:' version: 2.0.0(react@19.2.4)(scheduler@0.27.0) uuid: - specifier: 13.0.0 + specifier: 'catalog:' version: 13.0.0 zod: - specifier: 4.3.6 + specifier: 'catalog:' version: 4.3.6 zundo: - specifier: 2.3.0 + specifier: 'catalog:' version: 2.3.0(zustand@5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))) zustand: - specifier: 5.0.12 + specifier: 'catalog:' version: 5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) devDependencies: '@antfu/eslint-config': - specifier: 7.7.3 - version: 7.7.3(@eslint-react/eslint-plugin@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@next/eslint-plugin-next@16.2.1)(@typescript-eslint/rule-tester@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.1(typescript@5.9.3))(@typescript-eslint/utils@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(@vue/compiler-sfc@3.5.30)(eslint-plugin-react-hooks@7.0.1(eslint@10.1.0(jiti@1.21.7)))(eslint-plugin-react-refresh@0.5.2(eslint@10.1.0(jiti@1.21.7)))(eslint@10.1.0(jiti@1.21.7))(oxlint@1.56.0(oxlint-tsgolint@0.17.1))(typescript@5.9.3) + specifier: 'catalog:' + version: 7.7.3(@eslint-react/eslint-plugin@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@next/eslint-plugin-next@16.2.1)(@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3))(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(@vue/compiler-sfc@3.5.31)(eslint-plugin-react-hooks@7.0.1(eslint@10.1.0(jiti@1.21.7)))(eslint-plugin-react-refresh@0.5.2(eslint@10.1.0(jiti@1.21.7)))(eslint@10.1.0(jiti@1.21.7))(oxlint@1.57.0(oxlint-tsgolint@0.17.3))(typescript@5.9.3) '@chromatic-com/storybook': - specifier: 5.0.2 - version: 5.0.2(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + specifier: 'catalog:' + version: 5.1.1(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) '@egoist/tailwindcss-icons': - specifier: 1.9.2 + specifier: 'catalog:' version: 1.9.2(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3)) '@eslint-react/eslint-plugin': - specifier: 3.0.0 + specifier: 'catalog:' version: 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) '@hono/node-server': - specifier: 1.19.11 - version: 1.19.11(hono@4.12.8) + specifier: 'catalog:' + version: 1.19.11(hono@4.12.9) '@iconify-json/heroicons': - specifier: 1.2.3 + specifier: 'catalog:' version: 1.2.3 '@iconify-json/ri': - specifier: 1.2.10 + specifier: 'catalog:' version: 1.2.10 '@mdx-js/loader': - specifier: 3.1.1 + specifier: 'catalog:' version: 3.1.1(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) '@mdx-js/react': - specifier: 3.1.1 + specifier: 'catalog:' version: 3.1.1(@types/react@19.2.14)(react@19.2.4) '@mdx-js/rollup': - specifier: 3.1.1 + specifier: 'catalog:' version: 3.1.1(rollup@4.59.0) '@next/eslint-plugin-next': - specifier: 16.2.1 + specifier: 'catalog:' version: 16.2.1 '@next/mdx': - specifier: 16.2.1 + specifier: 'catalog:' version: 16.2.1(@mdx-js/loader@3.1.1(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.4)) '@rgrove/parse-xml': - specifier: 4.2.0 + specifier: 'catalog:' version: 4.2.0 '@storybook/addon-docs': - specifier: 10.3.1 - version: 10.3.1(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + specifier: 'catalog:' + version: 10.3.3(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) '@storybook/addon-links': - specifier: 10.3.1 - version: 10.3.1(react@19.2.4)(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + specifier: 'catalog:' + version: 10.3.3(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) '@storybook/addon-onboarding': - specifier: 10.3.1 - version: 10.3.1(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + specifier: 'catalog:' + version: 10.3.3(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) '@storybook/addon-themes': - specifier: 10.3.1 - version: 10.3.1(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + specifier: 'catalog:' + version: 10.3.3(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) '@storybook/nextjs-vite': - specifier: 10.3.1 - version: 10.3.1(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(next@16.2.1(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + specifier: 'catalog:' + version: 10.3.3(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) '@storybook/react': - specifier: 10.3.1 - version: 10.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + specifier: 'catalog:' + version: 10.3.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) '@tanstack/eslint-plugin-query': - specifier: 5.95.0 - version: 5.95.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + specifier: 'catalog:' + version: 5.95.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) '@tanstack/react-devtools': - specifier: 0.10.0 + specifier: 'catalog:' version: 0.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11) '@tanstack/react-form-devtools': - specifier: 0.2.19 + specifier: 'catalog:' version: 0.2.19(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11) '@tanstack/react-query-devtools': - specifier: 5.95.0 - version: 5.95.0(@tanstack/react-query@5.95.0(react@19.2.4))(react@19.2.4) + specifier: 'catalog:' + version: 5.95.2(@tanstack/react-query@5.95.2(react@19.2.4))(react@19.2.4) '@testing-library/dom': - specifier: 10.4.1 + specifier: 'catalog:' version: 10.4.1 '@testing-library/jest-dom': - specifier: 6.9.1 + specifier: 'catalog:' version: 6.9.1 '@testing-library/react': - specifier: 16.3.2 + specifier: 'catalog:' version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@testing-library/user-event': - specifier: 14.6.1 + specifier: 'catalog:' version: 14.6.1(@testing-library/dom@10.4.1) '@tsslint/cli': - specifier: 3.0.2 + specifier: 'catalog:' version: 3.0.2(@tsslint/compat-eslint@3.0.2(jiti@1.21.7)(typescript@5.9.3))(typescript@5.9.3) '@tsslint/compat-eslint': - specifier: 3.0.2 + specifier: 'catalog:' version: 3.0.2(jiti@1.21.7)(typescript@5.9.3) '@tsslint/config': - specifier: 3.0.2 + specifier: 'catalog:' version: 3.0.2(@tsslint/compat-eslint@3.0.2(jiti@1.21.7)(typescript@5.9.3))(typescript@5.9.3) '@types/js-cookie': - specifier: 3.0.6 + specifier: 'catalog:' version: 3.0.6 '@types/js-yaml': - specifier: 4.0.9 + specifier: 'catalog:' version: 4.0.9 '@types/negotiator': - specifier: 0.6.4 + specifier: 'catalog:' version: 0.6.4 '@types/node': - specifier: 25.5.0 + specifier: 'catalog:' version: 25.5.0 '@types/postcss-js': - specifier: 4.1.0 + specifier: 'catalog:' version: 4.1.0 '@types/qs': - specifier: 6.15.0 + specifier: 'catalog:' version: 6.15.0 '@types/react': - specifier: 19.2.14 + specifier: 'catalog:' version: 19.2.14 '@types/react-dom': - specifier: 19.2.3 + specifier: 'catalog:' version: 19.2.3(@types/react@19.2.14) '@types/react-syntax-highlighter': - specifier: 15.5.13 + specifier: 'catalog:' version: 15.5.13 '@types/react-window': - specifier: 1.8.8 + specifier: 'catalog:' version: 1.8.8 '@types/sortablejs': - specifier: 1.15.9 + specifier: 'catalog:' version: 1.15.9 '@typescript-eslint/parser': - specifier: 8.57.1 - version: 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + specifier: 'catalog:' + version: 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) '@typescript/native-preview': - specifier: 7.0.0-dev.20260322.1 - version: 7.0.0-dev.20260322.1 + specifier: 'catalog:' + version: 7.0.0-dev.20260329.1 '@vitejs/plugin-react': - specifier: 6.0.1 - version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) + specifier: 'catalog:' + version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) '@vitejs/plugin-rsc': - specifier: 0.5.21 - version: 0.5.21(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(react@19.2.4) + specifier: 'catalog:' + version: 0.5.21(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(react@19.2.4) '@vitest/coverage-v8': - specifier: 4.1.0 - version: 4.1.0(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) + specifier: 'catalog:' + version: 4.1.2(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) agentation: - specifier: 2.3.3 - version: 2.3.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 'catalog:' + version: 3.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) autoprefixer: - specifier: 10.4.27 + specifier: 'catalog:' version: 10.4.27(postcss@8.5.8) code-inspector-plugin: - specifier: 1.4.5 + specifier: 'catalog:' version: 1.4.5 eslint: - specifier: 10.1.0 + specifier: 'catalog:' version: 10.1.0(jiti@1.21.7) eslint-markdown: - specifier: 0.6.0 + specifier: 'catalog:' version: 0.6.0(eslint@10.1.0(jiti@1.21.7)) eslint-plugin-better-tailwindcss: - specifier: 4.3.2 - version: 4.3.2(eslint@10.1.0(jiti@1.21.7))(oxlint@1.56.0(oxlint-tsgolint@0.17.1))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))(typescript@5.9.3) + specifier: 'catalog:' + version: 4.3.2(eslint@10.1.0(jiti@1.21.7))(oxlint@1.57.0(oxlint-tsgolint@0.17.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))(typescript@5.9.3) eslint-plugin-hyoban: - specifier: 0.14.1 + specifier: 'catalog:' version: 0.14.1(eslint@10.1.0(jiti@1.21.7)) eslint-plugin-markdown-preferences: - specifier: 0.40.3 + specifier: 'catalog:' version: 0.40.3(@eslint/markdown@7.5.1)(eslint@10.1.0(jiti@1.21.7)) eslint-plugin-no-barrel-files: - specifier: 1.2.2 + specifier: 'catalog:' version: 1.2.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) eslint-plugin-react-hooks: - specifier: 7.0.1 + specifier: 'catalog:' version: 7.0.1(eslint@10.1.0(jiti@1.21.7)) eslint-plugin-react-refresh: - specifier: 0.5.2 + specifier: 'catalog:' version: 0.5.2(eslint@10.1.0(jiti@1.21.7)) eslint-plugin-sonarjs: - specifier: 4.0.2 + specifier: 'catalog:' version: 4.0.2(eslint@10.1.0(jiti@1.21.7)) eslint-plugin-storybook: - specifier: 10.3.1 - version: 10.3.1(eslint@10.1.0(jiti@1.21.7))(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + specifier: 'catalog:' + version: 10.3.3(eslint@10.1.0(jiti@1.21.7))(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) happy-dom: - specifier: 20.8.9 + specifier: 'catalog:' version: 20.8.9 hono: - specifier: 4.12.8 - version: 4.12.8 + specifier: 'catalog:' + version: 4.12.9 husky: - specifier: 9.1.7 + specifier: 'catalog:' version: 9.1.7 iconify-import-svg: - specifier: 0.1.2 + specifier: 'catalog:' version: 0.1.2 knip: - specifier: 6.0.2 - version: 6.0.2 + specifier: 'catalog:' + version: 6.1.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) lint-staged: - specifier: 16.4.0 + specifier: 'catalog:' version: 16.4.0 postcss: - specifier: 8.5.8 + specifier: 'catalog:' version: 8.5.8 postcss-js: - specifier: 5.1.0 + specifier: 'catalog:' version: 5.1.0(postcss@8.5.8) react-server-dom-webpack: - specifier: 19.2.4 + specifier: 'catalog:' version: 19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) sass: - specifier: 1.98.0 + specifier: 'catalog:' version: 1.98.0 storybook: - specifier: 10.3.1 - version: 10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 'catalog:' + version: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tailwindcss: - specifier: 3.4.19 + specifier: 'catalog:' version: 3.4.19(tsx@4.21.0)(yaml@2.8.3) - taze: - specifier: 19.10.0 - version: 19.10.0 tsx: - specifier: 4.21.0 + specifier: 'catalog:' version: 4.21.0 typescript: - specifier: 5.9.3 + specifier: 'catalog:' version: 5.9.3 uglify-js: - specifier: 3.19.3 + specifier: 'catalog:' version: 3.19.3 vinext: - specifier: https://pkg.pr.new/vinext@b6a2cac - version: https://pkg.pr.new/vinext@b6a2cac(33c71b051bfc49f90bf5d8b6a8976975) + specifier: 'catalog:' + version: 0.0.38(f5786d681f520e26604259e094ebaa46) vite: - specifier: npm:@voidzero-dev/vite-plus-core@0.1.13 - version: '@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + specifier: npm:@voidzero-dev/vite-plus-core@0.1.14 + version: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' vite-plugin-inspect: - specifier: 11.3.3 - version: 11.3.3(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) + specifier: 'catalog:' + version: 12.0.0-beta.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)(ws@8.20.0) vite-plus: - specifier: 0.1.13 - version: 0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + specifier: 'catalog:' + version: 0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) vitest: - specifier: npm:@voidzero-dev/vite-plus-test@0.1.13 - version: '@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + specifier: npm:@voidzero-dev/vite-plus-test@0.1.14 + version: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' vitest-canvas-mock: - specifier: 1.1.3 - version: 1.1.3(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) + specifier: 'catalog:' + version: 1.1.4(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) packages: @@ -622,17 +1239,17 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@amplitude/analytics-browser@2.37.0': - resolution: {integrity: sha512-/BWDneHRfq6+9bcPQC09Ep79SEj7aRJLZ1jJrPHtxA9KZJUz2au2COlJc1ReCaNzCcrA1xXv/MQ0Fv7TwoBglg==} + '@amplitude/analytics-browser@2.38.0': + resolution: {integrity: sha512-MhqyEkr1gGAR4s4GSSflDhFVheIx9Nv3FfElQu9NlNrXB2Hh3BEOyVgdK7hgfi6NJwFyfw30+t5lym+njtA8hA==} - '@amplitude/analytics-client-common@2.4.39': - resolution: {integrity: sha512-DFzi2/D2eu7EBCyslk86lToQa+qo1AmKgvZQVDDqkLG37/meTRcBAZiL0FAdTX21AYwpC/Ym4FWowD04foiBIQ==} + '@amplitude/analytics-client-common@2.4.41': + resolution: {integrity: sha512-+GbvtvhsUROotPBwfAxbrqovKePhC0oQKXtxjbeNQleOHjBjsAs5jEOCHpJenCKtaRpucg/FuK3NVOS09MfW7Q==} '@amplitude/analytics-connector@1.6.4': resolution: {integrity: sha512-SpIv0IQMNIq6SH3UqFGiaZyGSc7PBZwRdq7lvP0pBxW8i4Ny+8zwI0pV+VMfMHQwWY3wdIbWw5WQphNjpdq1/Q==} - '@amplitude/analytics-core@2.43.0': - resolution: {integrity: sha512-rcDqi4cmI9Ro7hN5wjAuTm92IdN2i0lhIDAj+JOd9BP3SRMrhhiw2lzcScj3owig8CiV9X7EHPTuZe6XCTfIgQ==} + '@amplitude/analytics-core@2.44.0': + resolution: {integrity: sha512-z9QuTxLqEQ8KIeAT6Vmy6K48rP9TUmjnb4GwUMYoV/fxu3B9ClTaN18zqXQMmDw9HwUiIreHiVbwTb7OQRN5aA==} '@amplitude/analytics-types@2.11.1': resolution: {integrity: sha512-wFEgb0t99ly2uJKm5oZ28Lti0Kh5RecR5XBkwfUpDzn84IoCIZ8GJTsMw/nThu8FZFc7xFDA4UAt76zhZKrs9A==} @@ -640,29 +1257,29 @@ packages: '@amplitude/experiment-core@0.7.2': resolution: {integrity: sha512-Wc2NWvgQ+bLJLeF0A9wBSPIaw0XuqqgkPKsoNFQrmS7r5Djd56um75In05tqmVntPJZRvGKU46pAp8o5tdf4mA==} - '@amplitude/plugin-autocapture-browser@1.24.1': - resolution: {integrity: sha512-cvjOFew2MFNBDTbk3+H7WNi3D0Jdp476m6faCaVhY99M5zqRCHDMRS7dC4HczvL9zYXlAcW9jAWucwES2m3TiQ==} + '@amplitude/plugin-autocapture-browser@1.25.0': + resolution: {integrity: sha512-YuWsz8XmJuKu3NlMxkvlhLey/5tGCeOwwfsROHficR0yDWO9gNG0WtHl7A0Pw1PUc9iaXjqfG2AjYumAtiq16Q==} - '@amplitude/plugin-custom-enrichment-browser@0.1.0': - resolution: {integrity: sha512-y3VmqZvCP1Z3jNgo/mtKVHON9L0P2SyqkMmUsbbFuLu1+TKIkicotnVq/lzlLU1TrW68mkInOM+We8JngasZBA==} + '@amplitude/plugin-custom-enrichment-browser@0.1.2': + resolution: {integrity: sha512-ZX9BKqs1E1OI7l7QCGu9JnB/1kqLN+zqIePgM2tuEhZNFQJaw4NhAMUaMRqvNnaCkHlmpVRISzSj/4D3tWMRtA==} - '@amplitude/plugin-network-capture-browser@1.9.9': - resolution: {integrity: sha512-SJIOQN04Mk9vCsnVd9QRcIvkMV7XSGZIKfbaKNQY5O3ueV33Kc8opm7YjPg2sWcxdzTcJijbCkOI0wCwOaRolg==} + '@amplitude/plugin-network-capture-browser@1.9.11': + resolution: {integrity: sha512-49o3zYnKUmRdrxgAEcr1iHnXR1um40e1icO0hzugSq04k19hs27zcl3zpEk9geO+nNKwO744ryE1q93gqVbHrQ==} - '@amplitude/plugin-page-url-enrichment-browser@0.7.0': - resolution: {integrity: sha512-MkM7TDq24k7ilUDNZISqjDSkVfmDJxWcnUagwYEXjLILhno5hGm7wdgFvVXXzKlZQHEogBxkbnq7wZXS9/YsMw==} + '@amplitude/plugin-page-url-enrichment-browser@0.7.3': + resolution: {integrity: sha512-3UZq/zKg4lcsRgziWAPSEeaUsNsbyjjxmsAE9kSDi/hIj5RaWnwWhY6TGhv45UAReugTA4vVZyFRg9btf3c/Fg==} - '@amplitude/plugin-page-view-tracking-browser@2.9.1': - resolution: {integrity: sha512-jkxz2lkJDAfsjj7mpbPUZx9N3qJssC3uYyv8Nk73z+p+v0wjBikWdOoKuNQkcuP09701zRdXp9ziU8+qwkGusw==} + '@amplitude/plugin-page-view-tracking-browser@2.9.4': + resolution: {integrity: sha512-J16zmEadnzNpkHSmzpTiQN2q9pGJ/4SkHONA9O8KxUsMU/MYTDgof3rAYY/w5B5rmvdxfMRCjqWtvnkizzgZ6w==} - '@amplitude/plugin-session-replay-browser@1.27.1': - resolution: {integrity: sha512-IEkAU7O3LbL23piMD7Lu0ej9wT/LQdQsyY1okTW5y2Nov8ZCmqLhZPLk6s9vKCUxGukDi7IL6gqXpURTLYj5rQ==} + '@amplitude/plugin-session-replay-browser@1.27.5': + resolution: {integrity: sha512-tf0Ty1nNF8OJ5QQ5scEqdGfzdgIaqkRf2MSzQfHbGcTIoYuVmAKuCgn3yMLk62MKnwgG3IsTIugMdRRv7l85PA==} - '@amplitude/plugin-web-vitals-browser@1.1.24': - resolution: {integrity: sha512-7AaytUK78RKdyDsblYJCKYan1lQi3Qzsp1WHItHJ+RSXPccmi4mCcvNtx0e8T9LmNJlUnsmYeEGR/6FaWvyvFg==} + '@amplitude/plugin-web-vitals-browser@1.1.26': + resolution: {integrity: sha512-wiD4vy+f2fepr+8Lnn26TYYjDEnWsmlGhJog99x+xfbZ/D+stGdaCIOz5AOjU1TpTRvxvamEu2XuOh+8EZOCSA==} - '@amplitude/rrdom@2.0.0-alpha.36': - resolution: {integrity: sha512-8jNhYEzjp6aaZON7qY/IpZIVbl8SUojb8kxD58StknlvnjKeGV7nHheXbkIz+T1LSVbWsdh+noIWuqhyFWzvgg==} + '@amplitude/rrdom@2.0.0-alpha.37': + resolution: {integrity: sha512-u4dSnBtlbJ8oU5P/Ywl2RLqvjqWbkl4ScMUbvQA7in4pWcx+0NRN+VVjLZXQcd8Fn7E/rcxjeUh7e7HfwvdasQ==} '@amplitude/rrweb-packer@2.0.0-alpha.36': resolution: {integrity: sha512-kqKg6OGoxHZvG4jwyO4kIjLdf8MkL6JcY5iLB09PQNP7O36ysnrH+ecJfa4V1Rld99kX25Pefkw4bzKmmFAqcg==} @@ -675,20 +1292,26 @@ packages: '@amplitude/rrweb-record@2.0.0-alpha.36': resolution: {integrity: sha512-zSHvmG5NUG4jNgWNVM7Oj3+rJPagv+TiHlnSiJ1X0WWLIg1GbUnOoTqpincZS5QupqTxQchNQaUg9MNu0MM3sQ==} - '@amplitude/rrweb-snapshot@2.0.0-alpha.36': - resolution: {integrity: sha512-vUvTXkNcu+cN736tykQDUVWERetFz1hyzgS0Yib5qSeWJwbse/4BaiWaZ7c5WevbbtcjLbDJqYKySJM92H5SxQ==} + '@amplitude/rrweb-snapshot@2.0.0-alpha.37': + resolution: {integrity: sha512-OPW2r8ESAguq+1R+z+WxGyzZzkMtojZ49Lpp6NrataNFyjdKaNXehDuLoNlEQkkUZGyDBiA7RSYvUw+JPSmmSQ==} '@amplitude/rrweb-types@2.0.0-alpha.36': resolution: {integrity: sha512-Bd2r3Bs0XIJt5fgPRWVl8bhvA9FCjJn8vQlDTO8ffPxilGPIzUXLQ06+xoLYkK9v+PDKJnCapOTL4A2LilDmgA==} + '@amplitude/rrweb-types@2.0.0-alpha.37': + resolution: {integrity: sha512-LW9wQ85umaAW/qlemTrUC408WVoBx99hvFCjsNRnxAyUmRemWyYY7+o8xPyeUexoWGqizPMkkNnPEO8t1NFjtw==} + '@amplitude/rrweb-utils@2.0.0-alpha.36': resolution: {integrity: sha512-w5RGROLU1Kyrq9j+trxcvvfkTp05MEKJ70Ig+YvHyZsE0nElh1PCF8PHAjV0/kji68+KqB03c0hoyaV99CDaDw==} - '@amplitude/rrweb@2.0.0-alpha.36': - resolution: {integrity: sha512-8vhPOk4fvszfxYZTk37EObW3n7uwEgO//funRSMt/QiBWtgQ8jhpFV9FcOAYdgde0Yw1uIM8oUbWZfy/XrexNw==} + '@amplitude/rrweb-utils@2.0.0-alpha.37': + resolution: {integrity: sha512-40YvPj24ietFQ3BTLfvFRPriRqdNOp3DzGiPU+WDOZkI3KjInQrEsibaqNBSXzJ+kMWrm8/eEwcQ0FkLk7Achw==} - '@amplitude/session-replay-browser@1.34.1': - resolution: {integrity: sha512-oQ9Pi/vcEhcRxmMIDMOZopt9vSaGYB4X64kp8idKut2Or8/DBhdztSjujwvkYvU48jNfqmT7oxIY5sCLYdiM6w==} + '@amplitude/rrweb@2.0.0-alpha.37': + resolution: {integrity: sha512-jJkSpPYiVgOZB422pb2jOJJn3pvb5E5f9vKK8CEmUlk2mVAl6kPQzW98mb05M65OJFj5nn9tSe9h5r5+Cl93ag==} + + '@amplitude/session-replay-browser@1.35.0': + resolution: {integrity: sha512-aGqu807oC8UIMmP+g1jBYsgN+/VeR/ThtK6fpxuZCugEogx7EZ9sXDEeudUmyvkQQfWmD+nLmrhYPX8FpROT5w==} '@amplitude/targeting@0.2.0': resolution: {integrity: sha512-/50ywTrC4hfcfJVBbh5DFbqMPPfaIOivZeb5Gb+OGM03QrA+lsUqdvtnKLNuWtceD4H6QQ2KFzPJ5aAJLyzVDA==} @@ -768,17 +1391,6 @@ packages: '@antfu/utils@8.1.1': resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==} - '@asamuzakjp/css-color@5.1.1': - resolution: {integrity: sha512-iGWN8E45Ws0XWx3D44Q1t6vX2LqhCKcwfmwBYCDsFrYFS6m4q/Ks61L2veETaLv+ckDC6+dTETJoaAAb7VjLiw==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - - '@asamuzakjp/dom-selector@7.0.4': - resolution: {integrity: sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - - '@asamuzakjp/nwsapi@2.3.9': - resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} - '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -878,10 +1490,6 @@ packages: '@braintree/sanitize-url@7.1.2': resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} - '@bramus/specificity@2.4.2': - resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} - hasBin: true - '@chevrotain/cst-dts-gen@11.1.2': resolution: {integrity: sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q==} @@ -897,8 +1505,8 @@ packages: '@chevrotain/utils@11.1.2': resolution: {integrity: sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA==} - '@chromatic-com/storybook@5.0.2': - resolution: {integrity: sha512-uLd5gyvcz8q83GI0rYWjml45ryO3ZJwZLretLEZvWFJ3UlFk5C5Km9cwRcKZgZp0F3zYwbb8nEe6PJdgA1eKxg==} + '@chromatic-com/storybook@5.1.1': + resolution: {integrity: sha512-BPoAXHM71XgeCK2u0jKr9i8apeQMm/Z9IWGyndA2FMijfQG9m8ox45DdWh/pxFkK5ClhGgirv5QwMhFIeHmThg==} engines: {node: '>=20.0.0', yarn: '>=1.22.18'} peerDependencies: storybook: ^0.0.0-0 || ^10.1.0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 @@ -933,41 +1541,67 @@ packages: '@code-inspector/webpack@1.4.5': resolution: {integrity: sha512-lwUv+X1FNSUWz+FKcUsE2dT2pg6VFRRXKt16hg/m+Lwtdet2adfi6BFLZmNz3OPIEGbRB5Kjx6bfaghZhbDCCg==} - '@csstools/color-helpers@6.0.2': - resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} - engines: {node: '>=20.19.0'} + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} - '@csstools/css-calc@3.1.1': - resolution: {integrity: sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==} - engines: {node: '>=20.19.0'} + '@cucumber/ci-environment@13.0.0': + resolution: {integrity: sha512-cs+3NzfNkGbcmHPddjEv4TKFiBpZRQ6WJEEufB9mw+ExS22V/4R/zpDSEG+fsJ/iSNCd6A2sATdY8PFOyY3YnA==} + + '@cucumber/cucumber-expressions@19.0.0': + resolution: {integrity: sha512-4FKoOQh2Uf6F6/Ln+1OxuK8LkTg6PyAqekhf2Ix8zqV2M54sH+m7XNJNLhOFOAW/t9nxzRbw2CcvXbCLjcvHZg==} + + '@cucumber/cucumber@12.7.0': + resolution: {integrity: sha512-7A/9CJpJDxv1SQ7hAZU0zPn2yRxx6XMR+LO4T94Enm3cYNWsEEj+RGX38NLX4INT+H6w5raX3Csb/qs4vUBsOA==} + engines: {node: 20 || 22 || >=24} + hasBin: true + + '@cucumber/gherkin-streams@6.0.0': + resolution: {integrity: sha512-HLSHMmdDH0vCr7vsVEURcDA4WwnRLdjkhqr6a4HQ3i4RFK1wiDGPjBGVdGJLyuXuRdJpJbFc6QxHvT8pU4t6jw==} + hasBin: true peerDependencies: - '@csstools/css-parser-algorithms': ^4.0.0 - '@csstools/css-tokenizer': ^4.0.0 + '@cucumber/gherkin': '>=22.0.0' + '@cucumber/message-streams': '>=4.0.0' + '@cucumber/messages': '>=17.1.1' - '@csstools/css-color-parser@4.0.2': - resolution: {integrity: sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==} - engines: {node: '>=20.19.0'} + '@cucumber/gherkin-utils@11.0.0': + resolution: {integrity: sha512-LJ+s4+TepHTgdKWDR4zbPyT7rQjmYIcukTwNbwNwgqr6i8Gjcmzf6NmtbYDA19m1ZFg6kWbFsmHnj37ZuX+kZA==} + hasBin: true + + '@cucumber/gherkin@38.0.0': + resolution: {integrity: sha512-duEXK+KDfQUzu3vsSzXjkxQ2tirF5PRsc1Xrts6THKHJO6mjw4RjM8RV+vliuDasmhhrmdLcOcM7d9nurNTJKw==} + + '@cucumber/html-formatter@23.0.0': + resolution: {integrity: sha512-WwcRzdM8Ixy4e53j+Frm3fKM5rNuIyWUfy4HajEN+Xk/YcjA6yW0ACGTFDReB++VDZz/iUtwYdTlPRY36NbqJg==} peerDependencies: - '@csstools/css-parser-algorithms': ^4.0.0 - '@csstools/css-tokenizer': ^4.0.0 + '@cucumber/messages': '>=18' - '@csstools/css-parser-algorithms@4.0.0': - resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} - engines: {node: '>=20.19.0'} + '@cucumber/junit-xml-formatter@0.9.0': + resolution: {integrity: sha512-WF+A7pBaXpKMD1i7K59Nk5519zj4extxY4+4nSgv5XLsGXHDf1gJnb84BkLUzevNtp2o2QzMG0vWLwSm8V5blw==} peerDependencies: - '@csstools/css-tokenizer': ^4.0.0 + '@cucumber/messages': '*' - '@csstools/css-syntax-patches-for-csstree@1.1.2': - resolution: {integrity: sha512-5GkLzz4prTIpoyeUiIu3iV6CSG3Plo7xRVOFPKI7FVEJ3mZ0A8SwK0XU3Gl7xAkiQ+mDyam+NNp875/C5y+jSA==} + '@cucumber/message-streams@4.0.1': + resolution: {integrity: sha512-Kxap9uP5jD8tHUZVjTWgzxemi/0uOsbGjd4LBOSxcJoOCRbESFwemUzilJuzNTB8pcTQUh8D5oudUyxfkJOKmA==} peerDependencies: - css-tree: ^3.2.1 - peerDependenciesMeta: - css-tree: - optional: true + '@cucumber/messages': '>=17.1.1' - '@csstools/css-tokenizer@4.0.0': - resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} - engines: {node: '>=20.19.0'} + '@cucumber/messages@32.0.1': + resolution: {integrity: sha512-1OSoW+GQvFUNAl6tdP2CTBexTXMNJF0094goVUcvugtQeXtJ0K8sCP0xbq7GGoiezs/eJAAOD03+zAPT64orHQ==} + + '@cucumber/pretty-formatter@1.0.1': + resolution: {integrity: sha512-A1lU4VVP0aUWdOTmpdzvXOyEYuPtBDI0xYwYJnmoMDplzxMdhcHk86lyyvYDoMoPzzq6OkOE3isuosvUU4X7IQ==} + peerDependencies: + '@cucumber/cucumber': '>=7.0.0' + '@cucumber/messages': '*' + + '@cucumber/query@14.7.0': + resolution: {integrity: sha512-fiqZ4gMEgYjmbuWproF/YeCdD5y+gD2BqgBIGbpihOsx6UlNsyzoDSfO+Tny0q65DxfK+pHo2UkPyEl7dO7wmQ==} + peerDependencies: + '@cucumber/messages': '*' + + '@cucumber/tag-expressions@9.1.0': + resolution: {integrity: sha512-bvHjcRFZ+J1TqIa9eFNO1wGHqwx4V9ZKV3hYgkuK/VahHx73uiP4rKV3JVrvWSMrwrFvJG6C8aEwnCWSvbyFdQ==} '@e18e/eslint-plugin@0.2.0': resolution: {integrity: sha512-mXgODVwhuDjTJ+UT+XSvmMmCidtGKfrV5nMIv1UtpWex2pYLsIM3RSpT8HWIMAebS9qANbXPKlSX4BE7ZvuCgA==} @@ -985,11 +1619,11 @@ packages: peerDependencies: tailwindcss: '*' - '@emnapi/core@1.9.0': - resolution: {integrity: sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==} + '@emnapi/core@1.9.1': + resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} - '@emnapi/runtime@1.9.0': - resolution: {integrity: sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==} + '@emnapi/runtime@1.9.1': + resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} '@emnapi/wasi-threads@1.2.0': resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} @@ -1261,6 +1895,15 @@ packages: resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + '@eslint/js@9.27.0': resolution: {integrity: sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1289,15 +1932,6 @@ packages: resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@exodus/bytes@1.15.0': - resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - peerDependencies: - '@noble/hashes': ^1.8.0 || ^2.0.0 - peerDependenciesMeta: - '@noble/hashes': - optional: true - '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -1680,8 +2314,11 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@napi-rs/wasm-runtime@1.1.1': - resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@napi-rs/wasm-runtime@1.1.2': + resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 '@neoconfetti/react@1.0.0': resolution: {integrity: sha512-klcSooChXXOzIm+SE5IISIAn3bYzYfPjbX7D7HoqZL84oAfgREeSg5vSIaSFH+DaGzzvImTyWe1OyrJ67vik4A==} @@ -1770,6 +2407,10 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@nolyfill/hasown@1.0.44': + resolution: {integrity: sha512-GA/21lkTr2PAQuT6jGnhLuBD5IFd/AEhBXJ/tf33+/bVxPxg+5ejKx9jGQGnyV/P0eSmdup5E+s8b2HL6lOrwQ==} + engines: {node: '>=12.4.0'} + '@nolyfill/is-core-module@1.0.39': resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} @@ -1782,175 +2423,178 @@ packages: resolution: {integrity: sha512-y3SvzjuY1ygnzWA4Krwx/WaJAsTMP11DN+e21A8Fa8PW1oDtVB5NSRW7LWurAiS2oKRkuCgcjTYMkBuBkcPCRg==} engines: {node: '>=12.4.0'} - '@orpc/client@1.13.9': - resolution: {integrity: sha512-RmD2HDgmGgF6zgHHdybE4zH6QJoHjC+/C3n56yLf+fmWbiZtwnOUETgGCroY6S8aK2fpy6hJ3wZaJUjfWVuGHg==} + '@orpc/client@1.13.13': + resolution: {integrity: sha512-jagx/Sa+9K4HEC5lBrUlMSrmR/06hvZctWh93/sKZc8GBk4zM0+71oT1kXQVw1oRYFV2XAq3xy3m6NdM6gfKYA==} - '@orpc/contract@1.13.9': - resolution: {integrity: sha512-0zxMyF82pxE8DwHzarCsCtOHQK96PE23qubMMBkxkP0XTtLJ7f8aYhrG8F16pNApypmTHiRlQlqNX8VXNViMqQ==} + '@orpc/contract@1.13.13': + resolution: {integrity: sha512-md6iyrYkePBSJNs1VnVEEnAUORMDPHIf3JGRSHxyssIcNakev/iOjP0HvpH0Sx0MlTBhihAJo6uFL8Vpth58Nw==} - '@orpc/openapi-client@1.13.9': - resolution: {integrity: sha512-zvNrc7wgF/INKeewH2ih48U/q9tG7rLZCnmMrb5/1jdZgYYOBAEuILlDAejeQwGdRce6W18GTBjLKIEdP3WwqA==} + '@orpc/openapi-client@1.13.13': + resolution: {integrity: sha512-k8od+bD7MqysKPPybAkxgfaNIaNseFPXtbidWkZAdCZ5w34SnDc7QPZJ0PQbyt9n9B+jOXSADNwQSTWSuGpjyA==} - '@orpc/shared@1.13.9': - resolution: {integrity: sha512-gpMY2e9jDsSyikh4DjBCO2Cs0wGj2I6xo2juIcmogYK5ecsTGO/U5huIftQn+2NUMk1cItwmykJBwc4pqHWVHw==} + '@orpc/shared@1.13.13': + resolution: {integrity: sha512-kNpYOBjHvmgKHla6munWOaEeA0utEfAvoiZpXjiRjjt1RxTibdwQvVHgxRIBNMXfQsb+ON3Q/wDkoaUhvvSnIw==} peerDependencies: '@opentelemetry/api': '>=1.9.0' peerDependenciesMeta: '@opentelemetry/api': optional: true - '@orpc/standard-server-fetch@1.13.9': - resolution: {integrity: sha512-/dJmHO+EVONyvmX3CFZkRjlRHeBfq0+6nnpFIVueGo4fNUbtQc+qurKEtpQqPxL/b7GSehskNH21XKLE0IE0gQ==} + '@orpc/standard-server-fetch@1.13.13': + resolution: {integrity: sha512-Lffy26+WtCQkwOUacsrdyeJF1GNzrhm75O3LXKVFXqmSdyVVdyI6zuqLn/YKGODU2L9IqGxZ2CwsV2tE298SSA==} - '@orpc/standard-server-peer@1.13.9': - resolution: {integrity: sha512-r8hSykxNIKwXSMuLYWBxQx1c3DU8b6nU8V76DZhtwC5g1SLYIzw+dzT/EgHplOfmsFeyodiEDXXX1k/twRLuzw==} + '@orpc/standard-server-peer@1.13.13': + resolution: {integrity: sha512-FeWAbXfnZDPYQRajM0hD6GJvHeC3DZILngAjdcLHy5zt3riu6nL2lLPSWDv5yNWWscmYU+CfKmXWd0Z01BOeWA==} - '@orpc/standard-server@1.13.9': - resolution: {integrity: sha512-dwsky7CScgOaDBa7CBF85aPGk/3UoB4fJjitVghb/sZD0Nt+CGIeiPHMsjEgxw5rJwgawMWLI5KxFH9euAJlWw==} + '@orpc/standard-server@1.13.13': + resolution: {integrity: sha512-9pgS8XvauuRQElkyuD8F3om+nN0KBEnTkhblDHCBzkZERjWkmfirJmshQrWHoFaDTk+nnXHIaY6d7TBTxXdPRw==} - '@orpc/tanstack-query@1.13.9': - resolution: {integrity: sha512-gOVJkCT9JGfu0e0TlTY3YUueXP2+Kzp6TcgfL2U3yXcYdTLv+jTrNOVJdtAAbeweUIU6dBEtatlhAQ7OgHWbsw==} + '@orpc/tanstack-query@1.13.13': + resolution: {integrity: sha512-6+Cheaiu+RDPdszdeRKoBINrF8MQp64zSeZB+L3gqgF43zlYDhLOgELZMzYa6U3U6bLk4rmIeubpk+i1kACfRg==} peerDependencies: - '@orpc/client': 1.13.9 + '@orpc/client': 1.13.13 '@tanstack/query-core': '>=5.80.2' '@ota-meshi/ast-token-store@0.3.0': resolution: {integrity: sha512-XRO0zi2NIUKq2lUk3T1ecFSld1fMWRKE6naRFGkgkdeosx7IslyUKNv5Dcb5PJTja9tHJoFu0v/7yEpAkrkrTg==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@oxc-parser/binding-android-arm-eabi@0.120.0': - resolution: {integrity: sha512-WU3qtINx802wOl8RxAF1v0VvmC2O4D9M8Sv486nLeQ7iPHVmncYZrtBhB4SYyX+XZxj2PNnCcN+PW21jHgiOxg==} + '@oxc-parser/binding-android-arm-eabi@0.121.0': + resolution: {integrity: sha512-n07FQcySwOlzap424/PLMtOkbS7xOu8nsJduKL8P3COGHKgKoDYXwoAHCbChfgFpHnviehrLWIPX0lKGtbEk/A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxc-parser/binding-android-arm64@0.120.0': - resolution: {integrity: sha512-SEf80EHdhlbjZEgzeWm0ZA/br4GKMenDW3QB/gtyeTV1gStvvZeFi40ioHDZvds2m4Z9J1bUAUL8yn1/+A6iGg==} + '@oxc-parser/binding-android-arm64@0.121.0': + resolution: {integrity: sha512-/Dd1xIXboYAicw+twT2utxPD7bL8qh7d3ej0qvaYIMj3/EgIrGR+tSnjCUkiCT6g6uTC0neSS4JY8LxhdSU/sA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxc-parser/binding-darwin-arm64@0.120.0': - resolution: {integrity: sha512-xVrrbCai8R8CUIBu3CjryutQnEYhZqs1maIqDvtUCFZb8vY33H7uh9mHpL3a0JBIKoBUKjPH8+rzyAeXnS2d6A==} + '@oxc-parser/binding-darwin-arm64@0.121.0': + resolution: {integrity: sha512-A0jNEvv7QMtCO1yk205t3DWU9sWUjQ2KNF0hSVO5W9R9r/R1BIvzG01UQAfmtC0dQm7sCrs5puixurKSfr2bRQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxc-parser/binding-darwin-x64@0.120.0': - resolution: {integrity: sha512-xyHBbnJ6mydnQUH7MAcafOkkrNzQC6T+LXgDH/3InEq2BWl/g424IMRiJVSpVqGjB+p2bd0h0WRR8iIwzjU7rw==} + '@oxc-parser/binding-darwin-x64@0.121.0': + resolution: {integrity: sha512-SsHzipdxTKUs3I9EOAPmnIimEeJOemqRlRDOp9LIj+96wtxZejF51gNibmoGq8KoqbT1ssAI5po/E3J+vEtXGA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxc-parser/binding-freebsd-x64@0.120.0': - resolution: {integrity: sha512-UMnVRllquXUYTeNfFKmxTTEdZ/ix1nLl0ducDzMSREoWYGVIHnOOxoKMWlCOvRr9Wk/HZqo2rh1jeumbPGPV9A==} + '@oxc-parser/binding-freebsd-x64@0.121.0': + resolution: {integrity: sha512-v1APOTkCp+RWOIDAHRoaeW/UoaHF15a60E8eUL6kUQXh+i4K7PBwq2Wi7jm8p0ymID5/m/oC1w3W31Z/+r7HQw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxc-parser/binding-linux-arm-gnueabihf@0.120.0': - resolution: {integrity: sha512-tkvn2CQ7QdcsMnpfiX3fd3wA3EFsWKYlcQzq9cFw/xc89Al7W6Y4O0FgLVkVQpo0Tnq/qtE1XfkJOnRRA9S/NA==} + '@oxc-parser/binding-linux-arm-gnueabihf@0.121.0': + resolution: {integrity: sha512-PmqPQuqHZyFVWA4ycr0eu4VnTMmq9laOHZd+8R359w6kzuNZPvmmunmNJ8ybkm769A0nCoVp3TJ6dUz7B3FYIQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm-musleabihf@0.120.0': - resolution: {integrity: sha512-WN5y135Ic42gQDk9grbwY9++fDhqf8knN6fnP+0WALlAUh4odY/BDK1nfTJRSfpJD9P3r1BwU0m3pW2DU89whQ==} + '@oxc-parser/binding-linux-arm-musleabihf@0.121.0': + resolution: {integrity: sha512-vF24htj+MOH+Q7y9A8NuC6pUZu8t/C2Fr/kDOi2OcNf28oogr2xadBPXAbml802E8wRAVfbta6YLDQTearz+jw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm64-gnu@0.120.0': - resolution: {integrity: sha512-1GgQBCcXvFMw99EPdMy+4NZ3aYyXsxjf9kbUUg8HuAy3ZBXzOry5KfFEzT9nqmgZI1cuetvApkiJBZLAPo8uaw==} + '@oxc-parser/binding-linux-arm64-gnu@0.121.0': + resolution: {integrity: sha512-wjH8cIG2Lu/3d64iZpbYr73hREMgKAfu7fqpXjgM2S16y2zhTfDIp8EQjxO8vlDtKP5Rc7waZW72lh8nZtWrpA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-arm64-musl@0.120.0': - resolution: {integrity: sha512-gmMQ70gsPdDBgpcErvJEoWNBr7bJooSLlvOBVBSGfOzlP5NvJ3bFvnUeZZ9d+dPrqSngtonf7nyzWUTUj/U+lw==} + '@oxc-parser/binding-linux-arm64-musl@0.121.0': + resolution: {integrity: sha512-qT663J/W8yQFw3dtscbEi9LKJevr20V7uWs2MPGTnvNZ3rm8anhhE16gXGpxDOHeg9raySaSHKhd4IGa3YZvuw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxc-parser/binding-linux-ppc64-gnu@0.120.0': - resolution: {integrity: sha512-T/kZuU0ajop0xhzVMwH5r3srC9Nqup5HaIo+3uFjIN5uPxa0LvSxC1ZqP4aQGJVW5G0z8/nCkjIfSMS91P/wzw==} + '@oxc-parser/binding-linux-ppc64-gnu@0.121.0': + resolution: {integrity: sha512-mYNe4NhVvDBbPkAP8JaVS8lC1dsoJZWH5WCjpw5E+sjhk1R08wt3NnXYUzum7tIiWPfgQxbCMcoxgeemFASbRw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-riscv64-gnu@0.120.0': - resolution: {integrity: sha512-vn21KXLAXzaI3N5CZWlBr1iWeXLl9QFIMor7S1hUjUGTeUuWCoE6JZB040/ZNDwf+JXPX8Ao9KbmJq9FMC2iGw==} + '@oxc-parser/binding-linux-riscv64-gnu@0.121.0': + resolution: {integrity: sha512-+QiFoGxhAbaI/amqX567784cDyyuZIpinBrJNxUzb+/L2aBRX67mN6Jv40pqduHf15yYByI+K5gUEygCuv0z9w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-riscv64-musl@0.120.0': - resolution: {integrity: sha512-SUbUxlar007LTGmSLGIC5x/WJvwhdX+PwNzFJ9f/nOzZOrCFbOT4ikt7pJIRg1tXVsEfzk5mWpGO1NFiSs4PIw==} + '@oxc-parser/binding-linux-riscv64-musl@0.121.0': + resolution: {integrity: sha512-9ykEgyTa5JD/Uhv2sttbKnCfl2PieUfOjyxJC/oDL2UO0qtXOtjPLl7H8Kaj5G7p3hIvFgu3YWvAxvE0sqY+hQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxc-parser/binding-linux-s390x-gnu@0.120.0': - resolution: {integrity: sha512-hYiPJTxyfJY2+lMBFk3p2bo0R9GN+TtpPFlRqVchL1qvLG+pznstramHNvJlw9AjaoRUHwp9IKR7UZQnRPGjgQ==} + '@oxc-parser/binding-linux-s390x-gnu@0.121.0': + resolution: {integrity: sha512-DB1EW5VHZdc1lIRjOI3bW/wV6R6y0xlfvdVrqj6kKi7Ayu2U3UqUBdq9KviVkcUGd5Oq+dROqvUEEFRXGAM7EQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-x64-gnu@0.120.0': - resolution: {integrity: sha512-q+5jSVZkprJCIy3dzJpApat0InJaoxQLsJuD6DkX8hrUS61z2lHQ1Fe9L2+TYbKHXCLWbL0zXe7ovkIdopBGMQ==} + '@oxc-parser/binding-linux-x64-gnu@0.121.0': + resolution: {integrity: sha512-s4lfobX9p4kPTclvMiH3gcQUd88VlnkMTF6n2MTMDAyX5FPNRhhRSFZK05Ykhf8Zy5NibV4PbGR6DnK7FGNN6A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-x64-musl@0.120.0': - resolution: {integrity: sha512-D9QDDZNnH24e7X4ftSa6ar/2hCavETfW3uk0zgcMIrZNy459O5deTbWrjGzZiVrSWigGtlQwzs2McBP0QsfV1w==} + '@oxc-parser/binding-linux-x64-musl@0.121.0': + resolution: {integrity: sha512-P9KlyTpuBuMi3NRGpJO8MicuGZfOoqZVRP1WjOecwx8yk4L/+mrCRNc5egSi0byhuReblBF2oVoDSMgV9Bj4Hw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxc-parser/binding-openharmony-arm64@0.120.0': - resolution: {integrity: sha512-TBU8ZwOUWAOUWVfmI16CYWbvh4uQb9zHnGBHsw5Cp2JUVG044OIY1CSHODLifqzQIMTXvDvLzcL89GGdUIqNrA==} + '@oxc-parser/binding-openharmony-arm64@0.121.0': + resolution: {integrity: sha512-R+4jrWOfF2OAPPhj3Eb3U5CaKNAH9/btMveMULIrcNW/hjfysFQlF8wE0GaVBr81dWz8JLgQlsxwctoL78JwXw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxc-parser/binding-wasm32-wasi@0.120.0': - resolution: {integrity: sha512-WG/FOZgDJCpJnuF3ToG/K28rcOmSY7FmFmfBKYb2fmLyhDzPpUldFGV7/Fz4ru0Iz/v4KPmf8xVgO8N3lO4KHA==} + '@oxc-parser/binding-wasm32-wasi@0.121.0': + resolution: {integrity: sha512-5TFISkPTymKvsmIlKasPVTPuWxzCcrT8pM+p77+mtQbIZDd1UC8zww4CJcRI46kolmgrEX6QpKO8AvWMVZ+ifw==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@oxc-parser/binding-win32-arm64-msvc@0.120.0': - resolution: {integrity: sha512-1T0HKGcsz/BKo77t7+89L8Qvu4f9DoleKWHp3C5sJEcbCjDOLx3m9m722bWZTY+hANlUEs+yjlK+lBFsA+vrVQ==} + '@oxc-parser/binding-win32-arm64-msvc@0.121.0': + resolution: {integrity: sha512-V0pxh4mql4XTt3aiEtRNUeBAUFOw5jzZNxPABLaOKAWrVzSr9+XUaB095lY7jqMf5t8vkfh8NManGB28zanYKw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxc-parser/binding-win32-ia32-msvc@0.120.0': - resolution: {integrity: sha512-L7vfLzbOXsjBXV0rv/6Y3Jd9BRjPeCivINZAqrSyAOZN3moCopDN+Psq9ZrGNZtJzP8946MtlRFZ0Als0wBCOw==} + '@oxc-parser/binding-win32-ia32-msvc@0.121.0': + resolution: {integrity: sha512-4Ob1qvYMPnlF2N9rdmKdkQFdrq16QVcQwBsO8yiPZXof0fHKFF+LmQV501XFbi7lHyrKm8rlJRfQ/M8bZZPVLw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxc-parser/binding-win32-x64-msvc@0.120.0': - resolution: {integrity: sha512-ys+upfqNtSu58huAhJMBKl3XCkGzyVFBlMlGPzHeFKgpFF/OdgNs1MMf8oaJIbgMH8ZxgGF7qfue39eJohmKIg==} + '@oxc-parser/binding-win32-x64-msvc@0.121.0': + resolution: {integrity: sha512-BOp1KCzdboB1tPqoCPXgntgFs0jjeSyOXHzgxVFR7B/qfr3F8r4YDacHkTOUNXtDgM8YwKnkf3rE5gwALYX7NA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxc-project/runtime@0.120.0': - resolution: {integrity: sha512-7fvACzS46TkHuzA+Tag8ac40qfwURXRTdc4AtyItF59AoNPOO/QjPMqPyvJH8CaUdGu0ntWDX1CCUNyLMxxX5g==} + '@oxc-project/runtime@0.121.0': + resolution: {integrity: sha512-p0bQukD8OEHxzY4T9OlANBbEFGnOnjo1CYi50HES7OD36UO2yPh6T+uOJKLtlg06eclxroipRCpQGMpeH8EJ/g==} engines: {node: ^20.19.0 || >=22.12.0} - '@oxc-project/types@0.120.0': - resolution: {integrity: sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==} + '@oxc-project/types@0.121.0': + resolution: {integrity: sha512-CGtOARQb9tyv7ECgdAlFxi0Fv7lmzvmlm2rpD/RdijOO9rfk/JvB1CjT8EnoD+tjna/IYgKKw3IV7objRb+aYw==} + + '@oxc-project/types@0.122.0': + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} '@oxc-resolver/binding-android-arm-eabi@11.19.1': resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} @@ -2060,276 +2704,276 @@ packages: cpu: [x64] os: [win32] - '@oxfmt/binding-android-arm-eabi@0.41.0': - resolution: {integrity: sha512-REfrqeMKGkfMP+m/ScX4f5jJBSmVNYcpoDF8vP8f8eYPDuPGZmzp56NIUsYmx3h7f6NzC6cE3gqh8GDWrJHCKw==} + '@oxfmt/binding-android-arm-eabi@0.42.0': + resolution: {integrity: sha512-dsqPTYsozeokRjlrt/b4E7Pj0z3eS3Eg74TWQuuKbjY4VttBmA88rB7d50Xrd+TZ986qdXCNeZRPEzZHAe+jow==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.41.0': - resolution: {integrity: sha512-s0b1dxNgb2KomspFV2LfogC2XtSJB42POXF4bMCLJyvQmAGos4ZtjGPfQreToQEaY0FQFjz3030ggI36rF1q5g==} + '@oxfmt/binding-android-arm64@0.42.0': + resolution: {integrity: sha512-t+aAjHxcr5eOBphFHdg1ouQU9qmZZoRxnX7UOJSaTwSoKsb6TYezNKO0YbWytGXCECObRqNcUxPoPr0KaraAIg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.41.0': - resolution: {integrity: sha512-EGXGualADbv/ZmamE7/2DbsrYmjoPlAmHEpTL4vapLF4EfVD6fr8/uQDFnPJkUBjiSWFJZtFNsGeN1B6V3owmA==} + '@oxfmt/binding-darwin-arm64@0.42.0': + resolution: {integrity: sha512-ulpSEYMKg61C5bRMZinFHrKJYRoKGVbvMEXA5zM1puX3O9T6Q4XXDbft20yrDijpYWeuG59z3Nabt+npeTsM1A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.41.0': - resolution: {integrity: sha512-WxySJEvdQQYMmyvISH3qDpTvoS0ebnIP63IMxLLWowJyPp/AAH0hdWtlo+iGNK5y3eVfa5jZguwNaQkDKWpGSw==} + '@oxfmt/binding-darwin-x64@0.42.0': + resolution: {integrity: sha512-ttxLKhQYPdFiM8I/Ri37cvqChE4Xa562nNOsZFcv1CKTVLeEozXjKuYClNvxkXmNlcF55nzM80P+CQkdFBu+uQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.41.0': - resolution: {integrity: sha512-Y2kzMkv3U3oyuYaR4wTfGjOTYTXiFC/hXmG0yVASKkbh02BJkvD98Ij8bIevr45hNZ0DmZEgqiXF+9buD4yMYQ==} + '@oxfmt/binding-freebsd-x64@0.42.0': + resolution: {integrity: sha512-Og7QS3yI3tdIKYZ58SXik0rADxIk2jmd+/YvuHRyKULWpG4V2fR5V4hvKm624Mc0cQET35waPXiCQWvjQEjwYQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.41.0': - resolution: {integrity: sha512-ptazDjdUyhket01IjPTT6ULS1KFuBfTUU97osTP96X5y/0oso+AgAaJzuH81oP0+XXyrWIHbRzozSAuQm4p48g==} + '@oxfmt/binding-linux-arm-gnueabihf@0.42.0': + resolution: {integrity: sha512-jwLOw/3CW4H6Vxcry4/buQHk7zm9Ne2YsidzTL1kpiMe4qqrRCwev3dkyWe2YkFmP+iZCQ7zku4KwjcLRoh8ew==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.41.0': - resolution: {integrity: sha512-UkoL2OKxFD+56bPEBcdGn+4juTW4HRv/T6w1dIDLnvKKWr6DbarB/mtHXlADKlFiJubJz8pRkttOR7qjYR6lTA==} + '@oxfmt/binding-linux-arm-musleabihf@0.42.0': + resolution: {integrity: sha512-XwXu2vkMtiq2h7tfvN+WA/9/5/1IoGAVCFPiiQUvcAuG3efR97KNcRGM8BetmbYouFotQ2bDal3yyjUx6IPsTg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.41.0': - resolution: {integrity: sha512-gofu0PuumSOHYczD8p62CPY4UF6ee+rSLZJdUXkpwxg6pILiwSDBIouPskjF/5nF3A7QZTz2O9KFNkNxxFN9tA==} + '@oxfmt/binding-linux-arm64-gnu@0.42.0': + resolution: {integrity: sha512-ea7s/XUJoT7ENAtUQDudFe3nkSM3e3Qpz4nJFRdzO2wbgXEcjnchKLEsV3+t4ev3r8nWxIYr9NRjPWtnyIFJVA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-arm64-musl@0.41.0': - resolution: {integrity: sha512-VfVZxL0+6RU86T8F8vKiDBa+iHsr8PAjQmKGBzSCAX70b6x+UOMFl+2dNihmKmUwqkCazCPfYjt6SuAPOeQJ3g==} + '@oxfmt/binding-linux-arm64-musl@0.42.0': + resolution: {integrity: sha512-+JA0YMlSdDqmacygGi2REp57c3fN+tzARD8nwsukx9pkCHK+6DkbAA9ojS4lNKsiBjIW8WWa0pBrBWhdZEqfuw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-ppc64-gnu@0.41.0': - resolution: {integrity: sha512-bwzokz2eGvdfJbc0i+zXMJ4BBjQPqg13jyWpEEZDOrBCQ91r8KeY2Mi2kUeuMTZNFXju+jcAbAbpyJxRGla0eg==} + '@oxfmt/binding-linux-ppc64-gnu@0.42.0': + resolution: {integrity: sha512-VfnET0j4Y5mdfCzh5gBt0NK28lgn5DKx+8WgSMLYYeSooHhohdbzwAStLki9pNuGy51y4I7IoW8bqwAaCMiJQg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-gnu@0.41.0': - resolution: {integrity: sha512-POLM//PCH9uqDeNDwWL3b3DkMmI3oI2cU6hwc2lnztD1o7dzrQs3R9nq555BZ6wI7t2lyhT9CS+CRaz5X0XqLA==} + '@oxfmt/binding-linux-riscv64-gnu@0.42.0': + resolution: {integrity: sha512-gVlCbmBkB0fxBWbhBj9rcxezPydsQHf4MFKeHoTSPicOQ+8oGeTQgQ8EeesSybWeiFPVRx3bgdt4IJnH6nOjAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-musl@0.41.0': - resolution: {integrity: sha512-NNK7PzhFqLUwx/G12Xtm6scGv7UITvyGdAR5Y+TlqsG+essnuRWR4jRNODWRjzLZod0T3SayRbnkSIWMBov33w==} + '@oxfmt/binding-linux-riscv64-musl@0.42.0': + resolution: {integrity: sha512-zN5OfstL0avgt/IgvRu0zjQzVh/EPkcLzs33E9LMAzpqlLWiPWeMDZyMGFlSRGOdDjuNmlZBCgj0pFnK5u32TQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-s390x-gnu@0.41.0': - resolution: {integrity: sha512-qVf/zDC5cN9eKe4qI/O/m445er1IRl6swsSl7jHkqmOSVfknwCe5JXitYjZca+V/cNJSU/xPlC5EFMabMMFDpw==} + '@oxfmt/binding-linux-s390x-gnu@0.42.0': + resolution: {integrity: sha512-9X6+H2L0qMc2sCAgO9HS03bkGLMKvOFjmEdchaFlany3vNZOjnVui//D8k/xZAtQv2vaCs1reD5KAgPoIU4msA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-gnu@0.41.0': - resolution: {integrity: sha512-ojxYWu7vUb6ysYqVCPHuAPVZHAI40gfZ0PDtZAMwVmh2f0V8ExpPIKoAKr7/8sNbAXJBBpZhs2coypIo2jJX4w==} + '@oxfmt/binding-linux-x64-gnu@0.42.0': + resolution: {integrity: sha512-BajxJ6KQvMMdpXGPWhBGyjb2Jvx4uec0w+wi6TJZ6Tv7+MzPwe0pO8g5h1U0jyFgoaF7mDl6yKPW3ykWcbUJRw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-musl@0.41.0': - resolution: {integrity: sha512-O2exZLBxoCMIv2vlvcbkdedazJPTdG0VSup+0QUCfYQtx751zCZNboX2ZUOiQ/gDTdhtXvSiot0h6GEGkOyalA==} + '@oxfmt/binding-linux-x64-musl@0.42.0': + resolution: {integrity: sha512-0wV284I6vc5f0AqAhgAbHU2935B4bVpncPoe5n/WzVZY/KnHgqxC8iSFGeSyLWEgstFboIcWkOPck7tqbdHkzA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxfmt/binding-openharmony-arm64@0.41.0': - resolution: {integrity: sha512-N+31/VoL+z+NNBt8viy3I4NaIdPbiYeOnB884LKqvXldaE2dRztdPv3q5ipfZYv0RwFp7JfqS4I27K/DSHCakg==} + '@oxfmt/binding-openharmony-arm64@0.42.0': + resolution: {integrity: sha512-p4BG6HpGnhfgHk1rzZfyR6zcWkE7iLrWxyehHfXUy4Qa5j3e0roglFOdP/Nj5cJJ58MA3isQ5dlfkW2nNEpolw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.41.0': - resolution: {integrity: sha512-Z7NAtu/RN8kjCQ1y5oDD0nTAeRswh3GJ93qwcW51srmidP7XPBmZbLlwERu1W5veCevQJtPS9xmkpcDTYsGIwQ==} + '@oxfmt/binding-win32-arm64-msvc@0.42.0': + resolution: {integrity: sha512-mn//WV60A+IetORDxYieYGAoQso4KnVRRjORDewMcod4irlRe0OSC7YPhhwaexYNPQz/GCFk+v9iUcZ2W22yxQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.41.0': - resolution: {integrity: sha512-uNxxP3l4bJ6VyzIeRqCmBU2Q0SkCFgIhvx9/9dJ9V8t/v+jP1IBsuaLwCXGR8JPHtkj4tFp+RHtUmU2ZYAUpMA==} + '@oxfmt/binding-win32-ia32-msvc@0.42.0': + resolution: {integrity: sha512-3gWltUrvuz4LPJXWivoAxZ28Of2O4N7OGuM5/X3ubPXCEV8hmgECLZzjz7UYvSDUS3grfdccQwmjynm+51EFpw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.41.0': - resolution: {integrity: sha512-49ZSpbZ1noozyPapE8SUOSm3IN0Ze4b5nkO+4+7fq6oEYQQJFhE0saj5k/Gg4oewVPdjn0L3ZFeWk2Vehjcw7A==} + '@oxfmt/binding-win32-x64-msvc@0.42.0': + resolution: {integrity: sha512-Wg4TMAfQRL9J9AZevJ/ZNy3uyyDztDYQtGr4P8UyyzIhLhFrdSmz1J/9JT+rv0fiCDLaFOBQnj3f3K3+a5PzDQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxlint-tsgolint/darwin-arm64@0.17.1': - resolution: {integrity: sha512-JNWNwyvSDcUQSBlQRl10XrCeNcN66TMvDw3gIDQeop5SNa1F7wFhsEx4zitYb7fGHwGh9095tsNttmuCaNXCbw==} + '@oxlint-tsgolint/darwin-arm64@0.17.3': + resolution: {integrity: sha512-5aDl4mxXWs+Bj02pNrX6YY6v9KMZjLIytXoqolLEo0dfBNVeZUonZgJAa/w0aUmijwIRrBhxEzb42oLuUtfkGw==} cpu: [arm64] os: [darwin] - '@oxlint-tsgolint/darwin-x64@0.17.1': - resolution: {integrity: sha512-SluNf6CW88pgGPqQUGC5GoK5qESWo2ct1PRDbza3vbf9SK2npx3igvylGQIgE9qYYOcjgnVdLOJ0+q0gItgUmQ==} + '@oxlint-tsgolint/darwin-x64@0.17.3': + resolution: {integrity: sha512-gPBy4DS5ueCgXzko20XsNZzDe/Cxde056B+QuPLGvz05CGEAtmRfpImwnyY2lAXXjPL+SmnC/OYexu8zI12yHQ==} cpu: [x64] os: [darwin] - '@oxlint-tsgolint/linux-arm64@0.17.1': - resolution: {integrity: sha512-BJxQ7/cdo2dNdGIBs2PIR6BaPA7cPfe+r1HE/uY+K7g2ygip+0LHB3GUO9GaNDZuWpsnDyjLYYowEGrVK8dokA==} + '@oxlint-tsgolint/linux-arm64@0.17.3': + resolution: {integrity: sha512-+pkunvCfB6pB0G9qHVVXUao3nqzXQPo4O3DReIi+5nGa+bOU3J3Srgy+Zb8VyOL+WDsSMJ+U7+r09cKHWhz3hg==} cpu: [arm64] os: [linux] - '@oxlint-tsgolint/linux-x64@0.17.1': - resolution: {integrity: sha512-s6UjmuaJbZ4zz/wJKdEw/s5mc0t41rgwxQJCSHPuzMumMK6ylrB7nydhDf8ObTtzhTIZdAS/2S/uayJmDcGbxw==} + '@oxlint-tsgolint/linux-x64@0.17.3': + resolution: {integrity: sha512-/kW5oXtBThu4FjmgIBthdmMjWLzT3M1TEDQhxDu7hQU5xDeTd60CDXb2SSwKCbue9xu7MbiFoJu83LN0Z/d38g==} cpu: [x64] os: [linux] - '@oxlint-tsgolint/win32-arm64@0.17.1': - resolution: {integrity: sha512-EO/Oj0ixHX+UQdu9hM7YUzibZI888MvPUo/DF8lSxFBt4JNEt8qGkwJEbCYjB/1LhUNmPHzSw2Tr9dCFVfW9nw==} + '@oxlint-tsgolint/win32-arm64@0.17.3': + resolution: {integrity: sha512-NMELRvbz4Ed4dxg8WiqZxtu3k4OJEp2B9KInZW+BMfqEqbwZdEJY83tbqz2hD1EjKO2akrqBQ0GpRUJEkd8kKw==} cpu: [arm64] os: [win32] - '@oxlint-tsgolint/win32-x64@0.17.1': - resolution: {integrity: sha512-jhv7XktAJ1sMRSb//yDYTauFSZ06H81i2SLEBPaSUKxSKoPMK8p1ACUJlnmwZX2MgapRLEj1Ml22B6+HiM2YIA==} + '@oxlint-tsgolint/win32-x64@0.17.3': + resolution: {integrity: sha512-+pJ7r8J3SLPws5uoidVplZc8R/lpKyKPE6LoPGv9BME00Y1VjT6jWGx/dtUN8PWvcu3iTC6k+8u3ojFSJNmWTg==} cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.56.0': - resolution: {integrity: sha512-IyfYPthZyiSKwAv/dLjeO18SaK8MxLI9Yss2JrRDyweQAkuL3LhEy7pwIwI7uA3KQc1Vdn20kdmj3q0oUIQL6A==} + '@oxlint/binding-android-arm-eabi@1.57.0': + resolution: {integrity: sha512-C7EiyfAJG4B70496eV543nKiq5cH0o/xIh/ufbjQz3SIvHhlDDsyn+mRFh+aW8KskTyUpyH2LGWL8p2oN6bl1A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.56.0': - resolution: {integrity: sha512-Ga5zYrzH6vc/VFxhn6MmyUnYEfy9vRpwTIks99mY3j6Nz30yYpIkWryI0QKPCgvGUtDSXVLEaMum5nA+WrNOSg==} + '@oxlint/binding-android-arm64@1.57.0': + resolution: {integrity: sha512-9i80AresjZ/FZf5xK8tKFbhQnijD4s1eOZw6/FHUwD59HEZbVLRc2C88ADYJfLZrF5XofWDiRX/Ja9KefCLy7w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.56.0': - resolution: {integrity: sha512-ogmbdJysnw/D4bDcpf1sPLpFThZ48lYp4aKYm10Z/6Nh1SON6NtnNhTNOlhEY296tDFItsZUz+2tgcSYqh8Eyw==} + '@oxlint/binding-darwin-arm64@1.57.0': + resolution: {integrity: sha512-0eUfhRz5L2yKa9I8k3qpyl37XK3oBS5BvrgdVIx599WZK63P8sMbg+0s4IuxmIiZuBK68Ek+Z+gcKgeYf0otsg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.56.0': - resolution: {integrity: sha512-x8QE1h+RAtQ2g+3KPsP6Fk/tdz6zJQUv5c7fTrJxXV3GHOo+Ry5p/PsogU4U+iUZg0rj6hS+E4xi+mnwwlDCWQ==} + '@oxlint/binding-darwin-x64@1.57.0': + resolution: {integrity: sha512-UvrSuzBaYOue+QMAcuDITe0k/Vhj6KZGjfnI6x+NkxBTke/VoM7ZisaxgNY0LWuBkTnd1OmeQfEQdQ48fRjkQg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.56.0': - resolution: {integrity: sha512-6G+WMZvwJpMvY7my+/SHEjb7BTk/PFbePqLpmVmUJRIsJMy/UlyYqjpuh0RCgYYkPLcnXm1rUM04kbTk8yS1Yg==} + '@oxlint/binding-freebsd-x64@1.57.0': + resolution: {integrity: sha512-wtQq0dCoiw4bUwlsNVDJJ3pxJA218fOezpgtLKrbQqUtQJcM9yP8z+I9fu14aHg0uyAxIY+99toL6uBa2r7nxA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.56.0': - resolution: {integrity: sha512-YYHBsk/sl7fYwQOok+6W5lBPeUEvisznV/HZD2IfZmF3Bns6cPC3Z0vCtSEOaAWTjYWN3jVsdu55jMxKlsdlhg==} + '@oxlint/binding-linux-arm-gnueabihf@1.57.0': + resolution: {integrity: sha512-qxFWl2BBBFcT4djKa+OtMdnLgoHEJXpqjyGwz8OhW35ImoCwR5qtAGqApNYce5260FQqoAHW8S8eZTjiX67Tsg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.56.0': - resolution: {integrity: sha512-+AZK8rOUr78y8WT6XkDb04IbMRqauNV+vgT6f8ZLOH8wnpQ9i7Nol0XLxAu+Cq7Sb+J9wC0j6Km5hG8rj47/yQ==} + '@oxlint/binding-linux-arm-musleabihf@1.57.0': + resolution: {integrity: sha512-SQoIsBU7J0bDW15/f0/RvxHfY3Y0+eB/caKBQtNFbuerTiA6JCYx9P1MrrFTwY2dTm/lMgTSgskvCEYk2AtG/Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.56.0': - resolution: {integrity: sha512-urse2SnugwJRojUkGSSeH2LPMaje5Q50yQtvtL9HFckiyeqXzoFwOAZqD5TR29R2lq7UHidfFDM9EGcchcbb8A==} + '@oxlint/binding-linux-arm64-gnu@1.57.0': + resolution: {integrity: sha512-jqxYd1W6WMeozsCmqe9Rzbu3SRrGTyGDAipRlRggetyYbUksJqJKvUNTQtZR/KFoJPb+grnSm5SHhdWrywv3RQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.56.0': - resolution: {integrity: sha512-rkTZkBfJ4TYLjansjSzL6mgZOdN5IvUnSq3oNJSLwBcNvy3dlgQtpHPrRxrCEbbcp7oQ6If0tkNaqfOsphYZ9g==} + '@oxlint/binding-linux-arm64-musl@1.57.0': + resolution: {integrity: sha512-i66WyEPVEvq9bxRUCJ/MP5EBfnTDN3nhwEdFZFTO5MmLLvzngfWEG3NSdXQzTT3vk5B9i6C2XSIYBh+aG6uqyg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.56.0': - resolution: {integrity: sha512-uqL1kMH3u69/e1CH2EJhP3CP28jw2ExLsku4o8RVAZ7fySo9zOyI2fy9pVlTAp4voBLVgzndXi3SgtdyCTa2aA==} + '@oxlint/binding-linux-ppc64-gnu@1.57.0': + resolution: {integrity: sha512-oMZDCwz4NobclZU3pH+V1/upVlJZiZvne4jQP+zhJwt+lmio4XXr4qG47CehvrW1Lx2YZiIHuxM2D4YpkG3KVA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.56.0': - resolution: {integrity: sha512-j0CcMBOgV6KsRaBdsebIeiy7hCjEvq2KdEsiULf2LZqAq0v1M1lWjelhCV57LxsqaIGChXFuFJ0RiFrSRHPhSg==} + '@oxlint/binding-linux-riscv64-gnu@1.57.0': + resolution: {integrity: sha512-uoBnjJ3MMEBbfnWC1jSFr7/nSCkcQYa72NYoNtLl1imshDnWSolYCjzb8LVCwYCCfLJXD+0gBLD7fyC14c0+0g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.56.0': - resolution: {integrity: sha512-7VDOiL8cDG3DQ/CY3yKjbV1c4YPvc4vH8qW09Vv+5ukq3l/Kcyr6XGCd5NvxUmxqDb2vjMpM+eW/4JrEEsUetA==} + '@oxlint/binding-linux-riscv64-musl@1.57.0': + resolution: {integrity: sha512-BdrwD7haPZ8a9KrZhKJRSj6jwCor+Z8tHFZ3PT89Y3Jq5v3LfMfEePeAmD0LOTWpiTmzSzdmyw9ijneapiVHKQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.56.0': - resolution: {integrity: sha512-JGRpX0M+ikD3WpwJ7vKcHKV6Kg0dT52BW2Eu2BupXotYeqGXBrbY+QPkAyKO6MNgKozyTNaRh3r7g+VWgyAQYQ==} + '@oxlint/binding-linux-s390x-gnu@1.57.0': + resolution: {integrity: sha512-BNs+7ZNsRstVg2tpNxAXfMX/Iv5oZh204dVyb8Z37+/gCh+yZqNTlg6YwCLIMPSk5wLWIGOaQjT0GUOahKYImw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.56.0': - resolution: {integrity: sha512-dNaICPvtmuxFP/VbqdofrLqdS3bM/AKJN3LMJD52si44ea7Be1cBk6NpfIahaysG9Uo+L98QKddU9CD5L8UHnQ==} + '@oxlint/binding-linux-x64-gnu@1.57.0': + resolution: {integrity: sha512-AghS18w+XcENcAX0+BQGLiqjpqpaxKJa4cWWP0OWNLacs27vHBxu7TYkv9LUSGe5w8lOJHeMxcYfZNOAPqw2bg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.56.0': - resolution: {integrity: sha512-pF1vOtM+GuXmbklM1hV8WMsn6tCNPvkUzklj/Ej98JhlanbmA2RB1BILgOpwSuCTRTIYx2MXssmEyQQ90QF5aA==} + '@oxlint/binding-linux-x64-musl@1.57.0': + resolution: {integrity: sha512-E/FV3GB8phu/Rpkhz5T96hAiJlGzn91qX5yj5gU754P5cmVGXY1Jw/VSjDSlZBCY3VHjsVLdzgdkJaomEmcNOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.56.0': - resolution: {integrity: sha512-bp8NQ4RE6fDIFLa4bdBiOA+TAvkNkg+rslR+AvvjlLTYXLy9/uKAYLQudaQouWihLD/hgkrXIKKzXi5IXOewwg==} + '@oxlint/binding-openharmony-arm64@1.57.0': + resolution: {integrity: sha512-xvZ2yZt0nUVfU14iuGv3V25jpr9pov5N0Wr28RXnHFxHCRxNDMtYPHV61gGLhN9IlXM96gI4pyYpLSJC5ClLCQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.56.0': - resolution: {integrity: sha512-PxT4OJDfMOQBzo3OlzFb9gkoSD+n8qSBxyVq2wQSZIHFQYGEqIRTo9M0ZStvZm5fdhMqaVYpOnJvH2hUMEDk/g==} + '@oxlint/binding-win32-arm64-msvc@1.57.0': + resolution: {integrity: sha512-Z4D8Pd0AyHBKeazhdIXeUUy5sIS3Mo0veOlzlDECg6PhRRKgEsBJCCV1n+keUZtQ04OP+i7+itS3kOykUyNhDg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.56.0': - resolution: {integrity: sha512-PTRy6sIEPqy2x8PTP1baBNReN/BNEFmde0L+mYeHmjXE1Vlcc9+I5nsqENsB2yAm5wLkzPoTNCMY/7AnabT4/A==} + '@oxlint/binding-win32-ia32-msvc@1.57.0': + resolution: {integrity: sha512-StOZ9nFMVKvevicbQfql6Pouu9pgbeQnu60Fvhz2S6yfMaii+wnueLnqQ5I1JPgNF0Syew4voBlAaHD13wH6tw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.56.0': - resolution: {integrity: sha512-ZHa0clocjLmIDr+1LwoWtxRcoYniAvERotvwKUYKhH41NVfl0Y4LNbyQkwMZzwDvKklKGvGZ5+DAG58/Ik47tQ==} + '@oxlint/binding-win32-x64-msvc@1.57.0': + resolution: {integrity: sha512-6PuxhYgth8TuW0+ABPOIkGdBYw+qYGxgIdXPHSVpiCDm+hqTTWCmC739St1Xni0DJBt8HnSHTG67i1y6gr8qrA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -2426,6 +3070,11 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + engines: {node: '>=18'} + hasBin: true + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -2714,6 +3363,104 @@ packages: resolution: {integrity: sha512-UuBOt7BOsKVOkFXRe4Ypd/lADuNIfqJXv8GvHqtXaTYXPPKkj2nS2zPllVsrtRjcomDhIJVBnZwfmlI222WH8g==} engines: {node: '>=14.0.0'} + '@rolldown/binding-android-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.12': + resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + '@rolldown/pluginutils@1.0.0-rc.5': resolution: {integrity: sha512-RxlLX/DPoarZ9PtxVrQgZhPoor987YtKQqCo5zkjX+0S0yLJ7Vv515Wk6+xtTL67VONKJKxETWZwuZjss2idYw==} @@ -2876,32 +3623,32 @@ packages: cpu: [x64] os: [win32] - '@sentry-internal/browser-utils@10.45.0': - resolution: {integrity: sha512-ZPZpeIarXKScvquGx2AfNKcYiVNDA4wegMmjyGVsTA2JPmP0TrJoO3UybJS6KGDeee8V3I3EfD/ruauMm7jOFQ==} + '@sentry-internal/browser-utils@10.46.0': + resolution: {integrity: sha512-WB1gBT9G13V02ekZ6NpUhoI1aGHV2eNfjEPthkU2bGBvFpQKnstwzjg7waIRGR7cu+YSW2Q6UI6aQLgBeOPD1g==} engines: {node: '>=18'} - '@sentry-internal/feedback@10.45.0': - resolution: {integrity: sha512-vCSurazFVq7RUeYiM5X326jA5gOVrWYD6lYX2fbjBOMcyCEhDnveNxMT62zKkZDyNT/jyD194nz/cjntBUkyWA==} + '@sentry-internal/feedback@10.46.0': + resolution: {integrity: sha512-c4pI/z9nZCQXe9GYEw/hE/YTY9AxGBp8/wgKI+T8zylrN35SGHaXv63szzE1WbI8lacBY8lBF7rstq9bQVCaHw==} engines: {node: '>=18'} - '@sentry-internal/replay-canvas@10.45.0': - resolution: {integrity: sha512-nvq/AocdZTuD7y0KSiWi3gVaY0s5HOFy86mC/v1kDZmT/jsBAzN5LDkk/f1FvsWma1peqQmpUqxvhC+YIW294Q==} + '@sentry-internal/replay-canvas@10.46.0': + resolution: {integrity: sha512-ub314MWUsekVCuoH0/HJbbimlI24SkV745UW2pj9xRbxOAEf1wjkmIzxKrMDbTgJGuEunug02XZVdJFJUzOcDw==} engines: {node: '>=18'} - '@sentry-internal/replay@10.45.0': - resolution: {integrity: sha512-vjosRoGA1bzhVAEO1oce+CsRdd70quzBeo7WvYqpcUnoLe/Rv8qpOMqWX3j26z7XfFHMExWQNQeLxmtYOArvlw==} + '@sentry-internal/replay@10.46.0': + resolution: {integrity: sha512-JBsWeXG6bRbxBFK8GzWymWGOB9QE7Kl57BeF3jzgdHTuHSWZ2mRnAmb1K05T4LU+gVygk6yW0KmdC8Py9Qzg9A==} engines: {node: '>=18'} - '@sentry/browser@10.45.0': - resolution: {integrity: sha512-e/a8UMiQhqqv706McSIcG6XK+AoQf9INthi2pD+giZfNRTzXTdqHzUT5OIO5hg8Am6eF63nDJc+vrYNPhzs51Q==} + '@sentry/browser@10.46.0': + resolution: {integrity: sha512-80DmGlTk5Z2/OxVOzLNxwolMyouuAYKqG8KUcoyintZqHbF6kO1RulI610HmyUt3OagKeBCqt9S7w0VIfCRL+Q==} engines: {node: '>=18'} - '@sentry/core@10.45.0': - resolution: {integrity: sha512-s69UXxvefeQxuZ5nY7/THtTrIEvJxNVCp3ns4kwoCw1qMpgpvn/296WCKVmM7MiwnaAdzEKnAvLAwaxZc2nM7Q==} + '@sentry/core@10.46.0': + resolution: {integrity: sha512-N3fj4zqBQOhXliS1Ne9euqIKuciHCGOJfPGQLwBoW9DNz03jF+NB8+dUKtrJ79YLoftjVgf8nbgwtADK7NR+2Q==} engines: {node: '>=18'} - '@sentry/react@10.45.0': - resolution: {integrity: sha512-jLezuxi4BUIU3raKyAPR5xMbQG/nhwnWmKo5p11NCbLmWzkS+lxoyDTUB4B8TAKZLfdtdkKLOn1S0tFc8vbUHw==} + '@sentry/react@10.46.0': + resolution: {integrity: sha512-Rb1S+9OuUPVwsz7GWnQ6Kgf3azbsseUymIegg3JZHNcW/fM1nPpaljzTBnuineia113DH0pgMBcdrrZDLaosFQ==} engines: {node: '>=18'} peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x @@ -2951,42 +3698,42 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@storybook/addon-docs@10.3.1': - resolution: {integrity: sha512-0FBhfMEg96QUmhdtks3rchktEEWF2hKcEsr3XluybBoBi4xAIw1vm+RJtL9Jm45ppTdg28LF7U+OeMx5LwkMzQ==} + '@storybook/addon-docs@10.3.3': + resolution: {integrity: sha512-trJQTpOtuOEuNv1Rn8X2Sopp5hSPpb0u0soEJ71BZAbxe4d2Y1d/1MYcxBdRKwncum6sCTsnxTpqQ/qvSJKlTQ==} peerDependencies: - storybook: ^10.3.1 + storybook: ^10.3.3 - '@storybook/addon-links@10.3.1': - resolution: {integrity: sha512-ooV8FU9PhcmSwpkSETZW6SYzVwQ0ui3DEp8gx5Kzf0JXAkESwxnzQVikxzHCLaP6KgYPb9gajN6jhin2KUGrhw==} + '@storybook/addon-links@10.3.3': + resolution: {integrity: sha512-tazBHlB+YbU62bde5DWsq0lnxZjcAsPB3YRUpN2hSMfAySsudRingyWrgu5KeOxXhJvKJj0ohjQvGcMx/wgQUA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.1 + storybook: ^10.3.3 peerDependenciesMeta: react: optional: true - '@storybook/addon-onboarding@10.3.1': - resolution: {integrity: sha512-fXhkG0dPvsuwlOmK2eOmc0CYJXUeWV8hZWlnthkqGKrzUyqXx0YmM3VdnKwk0/OnOCp1zykDoMtnjnDqWW4saQ==} + '@storybook/addon-onboarding@10.3.3': + resolution: {integrity: sha512-HZiHfXdcLc29WkYFW+1VAMtJCeAZOOLRYPvs97woJUcZqW8yfWEJ9MWH+j++736SFAv2aqZWNmP47OdBJ/kMkw==} peerDependencies: - storybook: ^10.3.1 + storybook: ^10.3.3 - '@storybook/addon-themes@10.3.1': - resolution: {integrity: sha512-Y4ZCof3C+nsXvfhDmUvxt1klnZ6SPh1tLuDWo4eE8MUG1jQ2tixiIQX6Ups8fqfYCN8RgjcDDHnIyNZRZlgB2Q==} + '@storybook/addon-themes@10.3.3': + resolution: {integrity: sha512-6PgH1o7yNnWRVj4lAT1DNcX/eZXKgzjhfmzgWh3oFpPfDDvUzpFxx+MClM5f/ZieIbyQscxEuq8li7+e/F5VEQ==} peerDependencies: - storybook: ^10.3.1 + storybook: ^10.3.3 - '@storybook/builder-vite@10.3.1': - resolution: {integrity: sha512-8X3Mv6VxVaVHip51ZuTAjQv7jI3K4GxpgW0ZAhaLi8atSTHezu7hQOuISC1cHAwhMV0GhGHtCCKi33G9EGx5hw==} + '@storybook/builder-vite@10.3.3': + resolution: {integrity: sha512-awspKCTZvXyeV3KabL0id62mFbxR5u/5yyGQultwCiSb2/yVgBfip2MAqLyS850pvTiB6QFVM9deOyd2/G/bEA==} peerDependencies: - storybook: ^10.3.1 + storybook: ^10.3.3 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@storybook/csf-plugin@10.3.1': - resolution: {integrity: sha512-P1WUSoyueV+ULpNeip4eIjjDvOXDBQI4gaq/s1PdAg1Szz/0GhDPu/CXuwukgkmyHaJP3aVR3pHPvSfeLfMCrA==} + '@storybook/csf-plugin@10.3.3': + resolution: {integrity: sha512-Utlh7zubm+4iOzBBfzLW4F4vD99UBtl2Do4edlzK2F7krQIcFvR2ontjAE8S1FQVLZAC3WHalCOS+Ch8zf3knA==} peerDependencies: esbuild: 0.27.2 rollup: 4.59.0 - storybook: ^10.3.1 + storybook: ^10.3.3 vite: '*' webpack: '*' peerDependenciesMeta: @@ -3008,40 +3755,40 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@storybook/nextjs-vite@10.3.1': - resolution: {integrity: sha512-//xqijMeZGYSagUMmuRZVW4pHYWqiQozEil2NM6HUseqc3bReFNqPpDAThCVGKAckIulVIIUZbF/4Lh9OYplOA==} + '@storybook/nextjs-vite@10.3.3': + resolution: {integrity: sha512-/OzOo0dSd0eFIAF9ft+ptwaXHa5Xj01cw3NXEtmPdODZXl0eiPmTvWYIJeP26UEPzI2FFSm4fK64ZZJluKpGOA==} peerDependencies: next: ^14.1.0 || ^15.0.0 || ^16.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.1 + storybook: ^10.3.3 typescript: '*' vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: typescript: optional: true - '@storybook/react-dom-shim@10.3.1': - resolution: {integrity: sha512-X337d639Bw9ej8vIi29bxgRsHcrFHhux1gMSmDifYjBRhTUXE3/OeDtoEl6ZV5Pgc5BAabUF5L2cl0mb428BYQ==} + '@storybook/react-dom-shim@10.3.3': + resolution: {integrity: sha512-lkhuh4G3UTreU9M3Iz5Dt32c6U+l/4XuvqLtbe1sDHENZH6aPj7y0b5FwnfHyvuTvYRhtbo29xZrF5Bp9kCC0w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.1 + storybook: ^10.3.3 - '@storybook/react-vite@10.3.1': - resolution: {integrity: sha512-6ATC5oZKXtNFdyLR1DyJY9s6qDltFL/Dfew6loJK4bBqd5a46+wpNJebMBhBxdhHa9FDJS5tv2noNSO5kXc+Sw==} + '@storybook/react-vite@10.3.3': + resolution: {integrity: sha512-qHdlBe1hjqFAGXa8JL7bWTLbP/gDqXbWDm+SYCB646NHh5yvVDkZLwigP5Y+UL7M2ASfqFtosnroUK9tcCM2dw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.1 + storybook: ^10.3.3 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@storybook/react@10.3.1': - resolution: {integrity: sha512-DoiOwfVG8VVIxA9JD3wz5lE30RTxwOnSHJJv4qdlCCiPIJWBGjxug9bqFxUZlqDkkbUzFLGDOBxYDp05Y66dbQ==} + '@storybook/react@10.3.3': + resolution: {integrity: sha512-cGG5TbR8Tdx9zwlpsWyBEfWrejm5iWdYF26EwIhwuKq9GFUTAVrQzo0Rs7Tqc3ZyVhRS/YfsRiWSEH+zmq2JiQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.1 + storybook: ^10.3.3 typescript: '>= 4.9.x' peerDependenciesMeta: typescript: @@ -3064,8 +3811,8 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - '@swc/helpers@0.5.19': - resolution: {integrity: sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==} + '@swc/helpers@0.5.20': + resolution: {integrity: sha512-2egEBHUMasdypIzrprsu8g+OEVd7Vp2MM3a2eVlM/cyFYto0nGz5BX5BTgh/ShZZI9ed+ozEq+Ngt+rgmUs8tw==} '@t3-oss/env-core@0.13.11': resolution: {integrity: sha512-sM7GYY+KL7H/Hl0BE0inWfk3nRHZOLhmVn7sHGxaZt9FAR6KqREXAE+6TqKfiavfXmpRxO/OZ2QgKRd+oiBYRQ==} @@ -3154,8 +3901,8 @@ packages: peerDependencies: solid-js: 1.9.11 - '@tanstack/eslint-plugin-query@5.95.0': - resolution: {integrity: sha512-XvEfgHyZoeGYGt0uOFwEbgkNMrRxoPt8Gy44cu3OwYFw6CU8uPAaUUiDJCqeyvYNNkuhnR4gWRn6vu5fcFSTUQ==} + '@tanstack/eslint-plugin-query@5.95.2': + resolution: {integrity: sha512-EYUFRaqjBep4EHMPpZR12sXP7Kr5qv9iDIlq93NfbhHwhITaW6Txu3ROO6dLFz5r84T8p+oZXBG77pa2Wuok7A==} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ^5.4.0 @@ -3175,11 +3922,11 @@ packages: resolution: {integrity: sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==} engines: {node: '>=18'} - '@tanstack/query-core@5.95.0': - resolution: {integrity: sha512-H1/CWCe8tGL3YIVeo770Z6kPbt0B3M1d/iQXIIK1qlFiFt6G2neYdkHgLapOC8uMYNt9DmHjmGukEKgdMk1P+A==} + '@tanstack/query-core@5.95.2': + resolution: {integrity: sha512-o4T8vZHZET4Bib3jZ/tCW9/7080urD4c+0/AUaYVpIqOsr7y0reBc1oX3ttNaSW5mYyvZHctiQ/UOP2PfdmFEQ==} - '@tanstack/query-devtools@5.95.0': - resolution: {integrity: sha512-i8IzjIsZSE9y9XGndeVYeUusrZpKyhOnOPIzWKao8iAVzmk8ZesPe5URt02aLwC5A0Rg72N+vgqolXXCXm4fFg==} + '@tanstack/query-devtools@5.95.2': + resolution: {integrity: sha512-QfaoqBn9uAZ+ICkA8brd1EHj+qBF6glCFgt94U8XP5BT6ppSsDBI8IJ00BU+cAGjQzp6wcKJL2EmRYvxy0TWIg==} '@tanstack/react-devtools@0.10.0': resolution: {integrity: sha512-cUMzOQb1IHmkb8MsD0TrxHT8EL92Rx3G0Huq+IFkWeoaZPGlIiaIcGTpS5VvQDeI4BVUT+ZGt6CQTpx8oSTECg==} @@ -3204,19 +3951,19 @@ packages: '@tanstack/react-start': optional: true - '@tanstack/react-query-devtools@5.95.0': - resolution: {integrity: sha512-w4lYQTuyGM6l8C32UDIvxeodCrOwbw0JGSK6sQXYlF24CJnTcNmCxvfvrW2L3f3NObyvEQYcGTfjOr0Vw8jaWA==} + '@tanstack/react-query-devtools@5.95.2': + resolution: {integrity: sha512-AFQFmbznVkbtfpx8VJ2DylW17wWagQel/qLstVLkYmNRo2CmJt3SNej5hvl6EnEeljJIdC3BTB+W7HZtpsH+3g==} peerDependencies: - '@tanstack/react-query': ^5.95.0 + '@tanstack/react-query': ^5.95.2 react: ^18 || ^19 - '@tanstack/react-query@5.95.0': - resolution: {integrity: sha512-EMP8B+BK9zvnAemT8M/y3z/WO0NjZ7fIUY3T3wnHYK6AA3qK/k33i7tPgCXCejhX0cd4I6bJIXN2GmjrHjDBzg==} + '@tanstack/react-query@5.95.2': + resolution: {integrity: sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA==} peerDependencies: react: ^18 || ^19 - '@tanstack/react-store@0.9.2': - resolution: {integrity: sha512-Vt5usJE5sHG/cMechQfmwvwne6ktGCELe89Lmvoxe3LKRoFrhPa8OCKWs0NliG8HTJElEIj7PLtaBQIcux5pAQ==} + '@tanstack/react-store@0.9.3': + resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -3227,12 +3974,16 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/store@0.9.2': - resolution: {integrity: sha512-K013lUJEFJK2ofFQ/hZKJUmCnpcV00ebLyOyFOWQvyQHUOZp/iYO84BM6aOGiV81JzwbX0APTVmW8YI7yiG5oA==} + '@tanstack/store@0.9.3': + resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==} '@tanstack/virtual-core@3.13.23': resolution: {integrity: sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==} + '@teppeis/multimaps@3.0.0': + resolution: {integrity: sha512-ID7fosbc50TbT0MK0EG12O+gAP3W3Aa/Pz4DaTtQtEvlc9Odaqi0de+xuZ7Li2GtK4HzEX7IuRWS/JmZLksR3Q==} + engines: {node: '>=14'} + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} @@ -3409,8 +4160,8 @@ packages: '@types/d3@7.4.3': resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} - '@types/debug@4.1.12': - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -3469,6 +4220,9 @@ packages: '@types/node@25.5.0': resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/papaparse@5.5.2': resolution: {integrity: sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==} @@ -3519,108 +4273,108 @@ packages: '@types/zen-observable@0.8.3': resolution: {integrity: sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw==} - '@typescript-eslint/eslint-plugin@8.57.1': - resolution: {integrity: sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==} + '@typescript-eslint/eslint-plugin@8.57.2': + resolution: {integrity: sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.57.1 + '@typescript-eslint/parser': ^8.57.2 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.57.1': - resolution: {integrity: sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==} + '@typescript-eslint/parser@8.57.2': + resolution: {integrity: sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.57.1': - resolution: {integrity: sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==} + '@typescript-eslint/project-service@8.57.2': + resolution: {integrity: sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/rule-tester@8.57.1': - resolution: {integrity: sha512-gk0q0rLa7a1uEB0iD2t1GZELK1z6HfudiKYeSVhjQ5gW5FdL0OcZ+8f09Lg7NbmHSBF3V+S9BDuw0qoCFkHR+w==} + '@typescript-eslint/rule-tester@8.57.2': + resolution: {integrity: sha512-cb5m0irr1449waTuYzGi4KD3SGUH3khL4ta/o9lzShvT7gnIwR5qVhU0VM0p966kCrtFId8hwmkvz1fOElsxTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - '@typescript-eslint/scope-manager@8.57.1': - resolution: {integrity: sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==} + '@typescript-eslint/scope-manager@8.57.2': + resolution: {integrity: sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.57.1': - resolution: {integrity: sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==} + '@typescript-eslint/tsconfig-utils@8.57.2': + resolution: {integrity: sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.57.1': - resolution: {integrity: sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==} + '@typescript-eslint/type-utils@8.57.2': + resolution: {integrity: sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.57.1': - resolution: {integrity: sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==} + '@typescript-eslint/types@8.57.2': + resolution: {integrity: sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.57.1': - resolution: {integrity: sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==} + '@typescript-eslint/typescript-estree@8.57.2': + resolution: {integrity: sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.57.1': - resolution: {integrity: sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==} + '@typescript-eslint/utils@8.57.2': + resolution: {integrity: sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.57.1': - resolution: {integrity: sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==} + '@typescript-eslint/visitor-keys@8.57.2': + resolution: {integrity: sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260322.1': - resolution: {integrity: sha512-5wSilxwLGX5fMKJgsUkCBwOfW9GMG3WF5j77CVBOdFI7miFaR3JQaPzTA+uyHDMNIIeSDo1KtV77GT48Y/d0Xg==} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260329.1': + resolution: {integrity: sha512-zS1thDk7luD82nXVwvMd97F7FgxAE6jGtSmnHeXdaQ+6hJQcQLOVkfUdaehhdodqKDapWA2jEURxQAYjDGvv3g==} cpu: [arm64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260322.1': - resolution: {integrity: sha512-G806SrfxkYNAgZ9Xk53+OvbmIg9iD5hjaiD2QhDQL2aZjzy10D4MhcdaZEOoMfw0OI/PoJPYOiPD+9/x2kw3Lg==} + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260329.1': + resolution: {integrity: sha512-3IJ2qmpjQ1OXpZNUhJRjF1+SbDuqGC14Ug8DjWJlPBp06isi1fcJph90f5qW//FxEsNnJPYRcNwpP0A2RbTASg==} cpu: [x64] os: [darwin] - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260322.1': - resolution: {integrity: sha512-+FyomEEt3K8TBO//n3Ijr61SDM2F7cxZCVqGt+Wk3rLcOCQ2i+8+p64gdsZCmImy3CyP0hBnxPydEbyNkZLtvg==} + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260329.1': + resolution: {integrity: sha512-gQb6SjB5JlUKDaDuz6mv/m+/OBWVDlcjHINFOykBZZYZtgxBx6nEDjLrT8TiJRjmHEG6hSbv+yisUL9IThWycA==} cpu: [arm64] os: [linux] - '@typescript/native-preview-linux-arm@7.0.0-dev.20260322.1': - resolution: {integrity: sha512-0a12pp19ELiNHMqTglfQQQNMsxvtzpjAa4qf12oMJoGyy+UnguKEmaaaCHdp75KvBXGDzlssfDAdiy+NirN19A==} + '@typescript/native-preview-linux-arm@7.0.0-dev.20260329.1': + resolution: {integrity: sha512-WKSSJrH611DFFAg6YCkgbnkdy0a4RRpzvDpNXtPzLTbMYC5oJdq3Dpvncx5nrJvGh4J4yvzXoMxraGPyygqGLw==} cpu: [arm] os: [linux] - '@typescript/native-preview-linux-x64@7.0.0-dev.20260322.1': - resolution: {integrity: sha512-MviQe5x4WqQGv/Vhu4hcv2A0qTW/BTaZPbOLYCtvhuovNFO6D++ZmJAbHvA0h/bJEaNTgxKZdZPHMpCfSEKfjA==} + '@typescript/native-preview-linux-x64@7.0.0-dev.20260329.1': + resolution: {integrity: sha512-kg4r+ssxoEWruBynUg9bFMdcMpo5NupzAPqNBlV8uWbmYGZjaPLonFWAi9ZZMiVJY/x5ZQ9GBl6xskwLdd3PJQ==} cpu: [x64] os: [linux] - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260322.1': - resolution: {integrity: sha512-ibnMaXDJPSgMXKC61NHiFlww/xjAEINgc1mcn2ntTfuGHwduU4P9Bi038TxXg95Wmu3v6xIPIorXXsBOdE+p3Q==} + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260329.1': + resolution: {integrity: sha512-Qi4lddVxl5MG7Tk67gYhCFnoqqLGd4TvaI8RN4qHFjt3GV8s6c+0cQGsJXJnVgMx27qbyDTdsyAa2pvb42rYcQ==} cpu: [arm64] os: [win32] - '@typescript/native-preview-win32-x64@7.0.0-dev.20260322.1': - resolution: {integrity: sha512-O+r1RToWBbGkK7NXC7DpraLObSWyxvSqRiSfr/BlZ351Cdq1q3121zCGzVtqERGeRtVoEMRrzS5ITOd6On/pCw==} + '@typescript/native-preview-win32-x64@7.0.0-dev.20260329.1': + resolution: {integrity: sha512-+k5+usuB8HZ6Xc+enLdb95ZJd25bQqsnI1zXxfRCHP+RS9mxs70Mi9ezQz3lKOLZFFXShSH7iW9iulm8KwVzCQ==} cpu: [x64] os: [win32] - '@typescript/native-preview@7.0.0-dev.20260322.1': - resolution: {integrity: sha512-CmzQTKvesYHmz3g92G+XPDis25ocvHqa/gK8m98w+bML99KJLEWQKVlvkLrYA85JiJEK+XBIiz+6lCgUqRkWXA==} + '@typescript/native-preview@7.0.0-dev.20260329.1': + resolution: {integrity: sha512-v5lJ0TgSt2m9yVk2xoj9+NH/gTDeWTLaWGPx6MJsUKOYd6bmCJhHbMcWmb8d/zlfhE9ffpixUKYj62CdYfriqA==} hasBin: true '@ungap/structured-clone@1.3.0': @@ -3651,6 +4405,19 @@ packages: resolution: {integrity: sha512-hBcWIOppZV14bi+eAmCZj8Elj8hVSUZJTpf1lgGBhVD85pervzQ1poM/qYfFUlPraYSZYP+ASg6To5BwYmUSGQ==} engines: {node: '>=16'} + '@vitejs/devtools-kit@0.1.11': + resolution: {integrity: sha512-ZmBr54Nk8IwdbNCBNtOkQ3WcskWcL55ndfiB0UM8eTZ0ZoNwzPTCHiHgk/RnbhviXiB0kTowyTTYp4RfqGEWUQ==} + peerDependencies: + vite: '*' + + '@vitejs/devtools-rpc@0.1.11': + resolution: {integrity: sha512-APo34qbV05bNJB//Jmn4QLDrCU1CQuFvYbQdqvvyCKjxwWuoHhGobqzgoRS5V23tn8Sbliz7/Fyhfh+7C0LtKA==} + peerDependencies: + ws: '*' + peerDependenciesMeta: + ws: + optional: true + '@vitejs/plugin-react@6.0.1': resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3675,23 +4442,26 @@ packages: react-server-dom-webpack: optional: true - '@vitest/coverage-v8@4.1.0': - resolution: {integrity: sha512-nDWulKeik2bL2Va/Wl4x7DLuTKAXa906iRFooIRPR+huHkcvp9QDkPQ2RJdmjOFrqOqvNfoSQLF68deE3xC3CQ==} + '@vitest/coverage-v8@4.1.2': + resolution: {integrity: sha512-sPK//PHO+kAkScb8XITeB1bf7fsk85Km7+rt4eeuRR3VS1/crD47cmV5wicisJmjNdfeokTZwjMk4Mj2d58Mgg==} peerDependencies: - '@vitest/browser': 4.1.0 - vitest: 4.1.0 + '@vitest/browser': 4.1.2 + vitest: 4.1.2 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/eslint-plugin@1.6.12': - resolution: {integrity: sha512-4kI47BJNFE+EQ5bmPbHzBF+ibNzx2Fj0Jo9xhWsTPxMddlHwIWl6YAxagefh461hrwx/W0QwBZpxGS404kBXyg==} + '@vitest/eslint-plugin@1.6.13': + resolution: {integrity: sha512-ui7JGWBoQpS5NKKW0FDb1eTuFEZ5EupEv2Psemuyfba7DfA5K52SeDLelt6P4pQJJ/4UGkker/BgMk/KrjH3WQ==} engines: {node: '>=18'} peerDependencies: + '@typescript-eslint/eslint-plugin': '*' eslint: '>=8.57.0' typescript: '>=5.0.0' vitest: '*' peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true typescript: optional: true vitest: @@ -3703,8 +4473,8 @@ packages: '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - '@vitest/pretty-format@4.1.0': - resolution: {integrity: sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==} + '@vitest/pretty-format@4.1.2': + resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} @@ -3712,11 +4482,11 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@vitest/utils@4.1.0': - resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} + '@vitest/utils@4.1.2': + resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} - '@voidzero-dev/vite-plus-core@0.1.13': - resolution: {integrity: sha512-72dAIYgGrrmh4ap5Tbvzo0EYCrmVRoPQjz3NERpZ34CWCjFB8+WAyBkxG631Jz9/qC1TR/ZThjOKbdYXQ5z9Aw==} + '@voidzero-dev/vite-plus-core@0.1.14': + resolution: {integrity: sha512-CCWzdkfW0fo0cQNlIsYp5fOuH2IwKuPZEb2UY2Z8gXcp5pG74A82H2Pthj0heAuvYTAnfT7kEC6zM+RbiBgQbg==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@arethetypeswrong/core': ^0.18.1 @@ -3775,43 +4545,57 @@ packages: yaml: optional: true - '@voidzero-dev/vite-plus-darwin-arm64@0.1.13': - resolution: {integrity: sha512-GgQ5dW1VR/Vuc8cRDsdpLMdly2rHiq8ihNKIh1eu8hR85bDjDxE4DSXeadCDMWC0bHTjQiR1HqApzjoPYsVF/w==} + '@voidzero-dev/vite-plus-darwin-arm64@0.1.14': + resolution: {integrity: sha512-q2ESUSbapwsxVRe/KevKATahNRraoX5nti3HT9S3266OHT5sMroBY14jaxTv74ekjQc9E6EPhyLGQWuWQuuBRw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@voidzero-dev/vite-plus-darwin-x64@0.1.13': - resolution: {integrity: sha512-X4ZXbjIhNg5jxEkPVn7kJZEVIvNiOCWztrY67nHD94yqsWLy2Hs7yo+DhrpEQihsnlZ1hRRtwDirdCncvEulUg==} + '@voidzero-dev/vite-plus-darwin-x64@0.1.14': + resolution: {integrity: sha512-UpcDZc9G99E/4HDRoobvYHxMvFOG5uv3RwEcq0HF70u4DsnEMl1z8RaJLeWV7a09LGwj9Q+YWC3Z4INWnTLs8g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.13': - resolution: {integrity: sha512-oPtwztuF1cierDWA68beais5mwm6dXsmOOvccn6ZHjNpKXig84LvgIoY4bMazA3Z0SE9nWqxmP0kePiO5SoiuA==} + '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.14': + resolution: {integrity: sha512-GIjn35RABUEDB9gHD26nRq7T72Te+Qy2+NIzogwEaUE728PvPkatF5gMCeF4sigCoc8c4qxDwsG+A2A2LYGnDg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.13': - resolution: {integrity: sha512-RgNHwTXrnYjt60K0g083VxOjaJNXHvZXViBQd/oC7RUwGUvxuHkraq/4mWaI69Pffx2KpyykxgCrtmhWq5Tgjg==} + '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.14': + resolution: {integrity: sha512-qo2RToGirG0XCcxZ2AEOuonLM256z6dNbJzDDIo5gWYA+cIKigFQJbkPyr25zsT1tsP2aY0OTxt2038XbVlRkQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.14': + resolution: {integrity: sha512-BsMWKZfdfGcYLxxLyaePpg6NW54xqzzcfq8sFUwKfwby0kgOKQ4WymUXyBvO9nnBb0ZPsJQrV0sx+Onac/LTaw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@voidzero-dev/vite-plus-test@0.1.13': - resolution: {integrity: sha512-P3n9adJZsaIUGlgbzyT2YvlA1yr2lCYhNjrZsiLAKMVyQzk2D++ptTre3SnYf9j1TQeMP1VonRXGjtZhTf8wHg==} + '@voidzero-dev/vite-plus-linux-x64-musl@0.1.14': + resolution: {integrity: sha512-mOrEpj7ntW9RopGbcOYG/L0pOs0qHzUG4Vz7NXbuf4dbOSlY4JjyoMOIWxjKQORQht02Hzuf8YrMGNwa6AjVSQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@voidzero-dev/vite-plus-test@0.1.14': + resolution: {integrity: sha512-rjF+qpYD+5+THOJZ3gbE3+cxsk5sW7nJ0ODK7y6ZKeS4amREUMedEDYykzKBwR7OZDC/WwE90A0iLWCr6qAXhA==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/ui': 4.1.0 + '@vitest/ui': 4.1.1 happy-dom: '*' jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -3826,14 +4610,14 @@ packages: jsdom: optional: true - '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.13': - resolution: {integrity: sha512-+oygKTgglu0HkA4y9kFs8/BbHFsvShkHuL+8bK++Zek3x2ArKHRjCMgcYUXyj6nYufMIL2ba/Und7aHUK2ZGiQ==} + '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.14': + resolution: {integrity: sha512-7iC+Ig+8D/zACy0IJf7w/vQ7duTjux9Ttmm3KOBdVWH4dl3JihydA7+SQVMhz71a4WiqJ6nPidoG8D6hUP4MVQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.13': - resolution: {integrity: sha512-+7zTnX/HqYCaBKmSLHjmCXQBRSSIJ6EFry55+4C0R4AMyayfn9w3LL0/NuVeCNkG69u3FnkRuwkqdWpzxztoHQ==} + '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.14': + resolution: {integrity: sha512-yRJ/8yAYFluNHx0Ej6Kevx65MIeM3wFKklnxosVZRlz2ZRL1Ea1Qh3tWATr3Ipk1ciRxBv8KJgp6zXqjxtZSoQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -3850,20 +4634,20 @@ packages: '@volar/typescript@2.4.28': resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} - '@vue/compiler-core@3.5.30': - resolution: {integrity: sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==} + '@vue/compiler-core@3.5.31': + resolution: {integrity: sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==} - '@vue/compiler-dom@3.5.30': - resolution: {integrity: sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==} + '@vue/compiler-dom@3.5.31': + resolution: {integrity: sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==} - '@vue/compiler-sfc@3.5.30': - resolution: {integrity: sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==} + '@vue/compiler-sfc@3.5.31': + resolution: {integrity: sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==} - '@vue/compiler-ssr@3.5.30': - resolution: {integrity: sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==} + '@vue/compiler-ssr@3.5.31': + resolution: {integrity: sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==} - '@vue/shared@3.5.30': - resolution: {integrity: sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==} + '@vue/shared@3.5.31': + resolution: {integrity: sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==} '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -3942,8 +4726,8 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - agentation@2.3.3: - resolution: {integrity: sha512-AUZgFCdBQ/nAohlFsHByM9S2Dp7ECMNqVjlOke4hv/90v+wTiwrGladEkgWS60RDQp+CJ5p97meeCthYgTFlKQ==} + agentation@3.0.2: + resolution: {integrity: sha512-iGzBxFVTuZEIKzLY6AExSLAQH6i6SwxV4pAu7v7m3X6bInZ7qlZXAwrEqyc4+EfP4gM7z2RXBF6SF4DeH0f2lA==} peerDependencies: react: '>=18.0.0' react-dom: '>=18.0.0' @@ -3953,8 +4737,8 @@ packages: react-dom: optional: true - ahooks@3.9.6: - resolution: {integrity: sha512-Mr7f05swd5SmKlR9SZo5U6M0LsL4ErweLzpdgXjA1JPmnZ78Vr6wzx0jUtvoxrcqGKYnX0Yjc02iEASVxHFPjQ==} + ahooks@3.9.7: + resolution: {integrity: sha512-S0lvzhbdlhK36RFBkGv+RbOM/dbbweym+BIHM/bwwuWVSVN5TuVErHPMWo4w0t1NDYg5KPp2iEf7Y7E5LASYiw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -3982,6 +4766,10 @@ packages: resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} engines: {node: '>=18'} + ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -4034,6 +4822,9 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} + assertion-error-formatter@3.0.0: + resolution: {integrity: sha512-6YyAVLrEze0kQ7CmJfUgrLHb+Y7XghmL2Ie7ijVa2Y9ynP3LV+VDiwFk62Dn0qtqbmY0BT0ss6p1xxpiF2PYbQ==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -4052,6 +4843,9 @@ packages: async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + autoprefixer@10.4.27: resolution: {integrity: sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==} engines: {node: ^10 || ^12 || >=14} @@ -4059,6 +4853,9 @@ packages: peerDependencies: postcss: ^8.1.0 + axios@1.14.0: + resolution: {integrity: sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==} + bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -4080,14 +4877,11 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.8: - resolution: {integrity: sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==} + baseline-browser-mapping@2.10.12: + resolution: {integrity: sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==} engines: {node: '>=6.0.0'} hasBin: true - bidi-js@1.0.3: - resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} - binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -4095,8 +4889,8 @@ packages: birecord@0.1.1: resolution: {integrity: sha512-VUpsf/qykW0heRlC8LooCq28Kxn3mAqKohhDG/49rrsQ1dT1CXyj/pgXS+5BSRzFTR/3DyIBOqQOrGyZOh71Aw==} - birpc@2.9.0: - resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + birpc@4.0.0: + resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -4107,8 +4901,8 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - brace-expansion@5.0.4: - resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -4141,14 +4935,28 @@ packages: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: 0.27.2 + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + cac@7.0.0: resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} engines: {node: '>=20.19.0'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -4160,13 +4968,16 @@ packages: camelize@1.0.1: resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} - caniuse-lite@1.0.30001780: - resolution: {integrity: sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==} + caniuse-lite@1.0.30001781: + resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==} canvas@3.2.2: resolution: {integrity: sha512-duEt4h1HHu9sJZyVKfLRXR6tsKPY7cEELzxSRJkwddOXYvQT3P/+es98SV384JA0zMOZ5s+9gatnGfM6sL4Drg==} engines: {node: ^18.12.0 || >= 20.9.0} + capital-case@1.0.4: + resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -4260,6 +5071,9 @@ packages: resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} engines: {node: '>=8'} + class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -4277,6 +5091,10 @@ packages: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + cli-truncate@5.2.0: resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} engines: {node: '>=20'} @@ -4310,12 +5128,24 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + comma-separated-tokens@1.0.8: resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@14.0.0: + resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==} + engines: {node: '>=20'} + + commander@14.0.2: + resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} + engines: {node: '>=20'} + commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -4339,6 +5169,10 @@ packages: resolution: {integrity: sha512-aRDkn3uyIlCFfk5NUA+VdwMmMsh8JGhc4hapfV4yxymHGQ3BVskMQfoXGpCo5IoBuQ9tS5iiVKhCpTcB4pW4qw==} engines: {node: '>= 12.0.0'} + comment-parser@1.4.6: + resolution: {integrity: sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==} + engines: {node: '>= 12.0.0'} + compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} @@ -4348,6 +5182,10 @@ packages: confbox@0.2.4: resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -4399,10 +5237,6 @@ packages: resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - css-tree@3.2.1: - resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - css-what@6.2.2: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} @@ -4581,10 +5415,6 @@ packages: dagre-d3-es@7.0.14: resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} - data-urls@7.0.0: - resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - dayjs@1.11.20: resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} @@ -4633,8 +5463,12 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} - delaunator@5.0.1: - resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} @@ -4660,6 +5494,10 @@ packages: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + diff@4.0.4: + resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} + engines: {node: '>=0.3.1'} + dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} @@ -4697,6 +5535,10 @@ packages: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + echarts-for-react@3.0.6: resolution: {integrity: sha512-4zqLgTGWS3JvkQDXjzkR1k1CHRdpd6by0988TWMJgnvDytegWLbeP/VNZmMa+0VJx2eD7Y632bi2JquXDgiGJg==} peerDependencies: @@ -4706,8 +5548,8 @@ packages: echarts@6.0.0: resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==} - electron-to-chromium@1.5.313: - resolution: {integrity: sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==} + electron-to-chromium@1.5.328: + resolution: {integrity: sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==} elkjs@0.11.1: resolution: {integrity: sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==} @@ -4770,12 +5612,31 @@ packages: error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} es-module-lexer@2.0.0: resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + es-toolkit@1.45.1: resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} @@ -4815,8 +5676,8 @@ packages: peerDependencies: eslint: '>=6.0.0' - eslint-config-flat-gitignore@2.2.1: - resolution: {integrity: sha512-wA5EqN0era7/7Gt5Botlsfin/UNY0etJSEeBgbUlFLFrBi47rAN//+39fI7fpYcl8RENutlFtvp/zRa/M/pZNg==} + eslint-config-flat-gitignore@2.3.0: + resolution: {integrity: sha512-bg4ZLGgoARg1naWfsINUUb/52Ksw/K22K+T16D38Y8v+/sGwwIYrGvH/JBjOin+RQtxxC9tzNNiy4shnGtGyyQ==} peerDependencies: eslint: ^9.5.0 || ^10.0.0 @@ -4896,8 +5757,8 @@ packages: peerDependencies: eslint: '>=9.0.0' - eslint-plugin-jsdoc@62.8.0: - resolution: {integrity: sha512-hu3r9/6JBmPG6wTcqtYzgZAnjEG2eqRUATfkFscokESg1VDxZM21ZaMire0KjeMwfj+SXvgB4Rvh5LBuesj92w==} + eslint-plugin-jsdoc@62.8.1: + resolution: {integrity: sha512-e9358PdHgvcMF98foNd3L7hVCw70Lt+YcSL7JzlJebB8eT5oRJtW6bHMQKoAwJtw6q0q0w/fRIr2kwnHdFDI6A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 @@ -4996,11 +5857,11 @@ packages: peerDependencies: eslint: ^8.0.0 || ^9.0.0 || ^10.0.0 - eslint-plugin-storybook@10.3.1: - resolution: {integrity: sha512-zWE8cQTJo2Wuw6I/Ag73rP5rLbaypm5p3G2BV74Y7Lc8NwNclAwNi5u+yl9qBQLW2aSXotDW9fjj3Mx+GeEgfA==} + eslint-plugin-storybook@10.3.3: + resolution: {integrity: sha512-jo8wZvKaJlxxrNvf4hCsROJP3CdlpaLiYewAs5Ww+PJxCrLelIi5XVHWOAgBvvr3H9WDKvUw8xuvqPYqAlpkFg==} peerDependencies: eslint: '>=8' - storybook: ^10.3.1 + storybook: ^10.3.3 eslint-plugin-toml@1.3.1: resolution: {integrity: sha512-1l00fBP03HIt9IPV7ZxBi7x0y0NMdEZmakL1jBD6N/FoKBvfKxPw5S8XkmzBecOnFBTn5Z8sNJtL5vdf9cpRMQ==} @@ -5222,6 +6083,10 @@ packages: fflate@0.7.4: resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -5242,6 +6107,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -5249,6 +6117,19 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} @@ -5275,11 +6156,19 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + functional-red-black-tree@1.0.1: resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} @@ -5294,10 +6183,18 @@ packages: resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} engines: {node: '>=18'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stream@5.2.0: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} @@ -5326,6 +6223,10 @@ packages: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} + global-dirs@3.0.1: + resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} + engines: {node: '>=10'} + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -5350,6 +6251,10 @@ packages: peerDependencies: csstype: ^3.0.10 + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -5360,10 +6265,22 @@ packages: resolution: {integrity: sha512-Tz23LR9T9jOGVZm2x1EPdXqwA37G/owYMxRwU0E4miurAtFsPMQ1d2Jc2okUaSjZqAFz2oEn3FLXC5a0a+siyA==} engines: {node: '>=20.0.0'} + has-ansi@4.0.1: + resolution: {integrity: sha512-Qr4RtTm30xvEdqUXbSBVWDu+PrTokJOwe/FU+VdfJPk+MXAPoeOzKpRyrDTnZIJwAkQ4oBLTU53nu0HrkF/Z2A==} + engines: {node: '>=8'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hast-util-from-dom@5.0.1: resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==} @@ -5428,13 +6345,13 @@ packages: highlightjs-vue@1.0.0: resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==} - hono@4.12.8: - resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==} + hono@4.12.9: + resolution: {integrity: sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==} engines: {node: '>=16.9.0'} - html-encoding-sniffer@6.0.0: - resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + hosted-git-info@9.0.2: + resolution: {integrity: sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==} + engines: {node: ^20.17.0 || >=22.9.0} html-entities@2.6.0: resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} @@ -5465,10 +6382,10 @@ packages: i18next-resources-to-backend@1.2.1: resolution: {integrity: sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw==} - i18next@25.10.4: - resolution: {integrity: sha512-XsE/6eawy090meuFU0BTY9BtmWr1m9NSwLr0NK7/A04LA58wdAvDsi9WNOJ40Qb1E9NIPbvnVLZEN2fWDd3/3Q==} + i18next@25.10.10: + resolution: {integrity: sha512-cqUW2Z3EkRx7NqSyywjkgCLK7KLCL6IFVFcONG7nVYIJ3ekZ1/N5jUsihHV6Bq37NfhgtczxJcxduELtjTwkuQ==} peerDependencies: - typescript: ^5 + typescript: ^5 || ^6 peerDependenciesMeta: typescript: optional: true @@ -5524,12 +6441,20 @@ packages: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} engines: {node: '>=12'} + index-to-position@1.2.0: + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} + engines: {node: '>=18'} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + ini@2.0.0: + resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} + engines: {node: '>=10'} + inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -5593,25 +6518,38 @@ packages: is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} hasBin: true + is-installed-globally@0.4.0: + resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} + engines: {node: '>=10'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} - is-potential-custom-element-name@1.0.1: - resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + is-wsl@3.1.1: resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} @@ -5646,8 +6584,8 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true - jotai@2.18.1: - resolution: {integrity: sha512-e0NOzK+yRFwHo7DOp0DS0Ycq74KMEAObDWFGmfEL28PD9nLqBTt3/Ug7jf9ca72x0gC9LQZG9zH+0ISICmy3iA==} + jotai@2.19.0: + resolution: {integrity: sha512-r2wwxEXP1F2JteDLZEOPoIpAHhV89paKsN5GWVYndPNMMP/uVZDcC+fNj0A8NjKgaPWzdyO8Vp8YcYKe0uCEqQ==} engines: {node: '>=12.20.0'} peerDependencies: '@babel/core': '>=7.0.0' @@ -5664,6 +6602,10 @@ packages: react: optional: true + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + js-audio-recorder@1.0.7: resolution: {integrity: sha512-JiDODCElVHGrFyjGYwYyNi7zCbKk9va9C77w+zCPMmi4C6ix7zsX2h3ddHugmo4dOTOTCym9++b/wVW9nC0IaA==} @@ -5691,15 +6633,6 @@ packages: resolution: {integrity: sha512-/2uqY7x6bsrpi3i9LVU6J89352C0rpMk0as8trXxCtvd4kPk1ke/Eyif6wqfSLvoNJqcDG9Vk4UsXgygzCt2xA==} engines: {node: '>=20.0.0'} - jsdom@29.0.1: - resolution: {integrity: sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} - peerDependencies: - canvas: ^3.2.2 - peerDependenciesMeta: - canvas: - optional: true - jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -5739,8 +6672,8 @@ packages: resolution: {integrity: sha512-eQQBjBnsVtGacsG9uJNB8qOr3yA8rga4wAaGG1qRcBzSIvfhERLrWxMAM1hp5fcS6Abo8M4+bUBTekYR0qTPQw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - katex@0.16.40: - resolution: {integrity: sha512-1DJcK/L05k1Y9Gf7wMcyuqFOL6BiY3vY0CFcAM/LPRN04NALxcl6u7lOWNsp3f/bCHWxigzQl6FbR95XJ4R84Q==} + katex@0.16.44: + resolution: {integrity: sha512-EkxoDTk8ufHqHlf9QxGwcxeLkWRR3iOuYfRpfORgYfqc8s13bgb+YtRY59NK5ZpRaCwq1kqA6a5lpX8C/eLphQ==} hasBin: true keyv@4.5.4: @@ -5749,11 +6682,14 @@ packages: khroma@2.1.0: resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} - knip@6.0.2: - resolution: {integrity: sha512-W17Bo5N9AYn0ZkgWHGBmK/01SrSmr3B6iStr3zudDa2eqi+Kc8VmPjSpTYKDV2Uy/kojrlcH/gS1wypAXfXRRA==} + knip@6.1.0: + resolution: {integrity: sha512-n5eVbJP7HXmwTsiJcELWJe2O1ESxyCTNxJzRTIECDYDTM465qnqk7fL2dv6ae3NUFvFWorZvGlh9mcwxwJ5Xgw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + knuth-shuffle-seeded@1.0.6: + resolution: {integrity: sha512-9pFH0SplrfyKyojCLxZfMcvkhf5hH0d+UwR9nTVJ/DDQJGuzcXjTwB7TP7sDfehSudlGGaOLblmEWqv04ERVWg==} + kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} @@ -5888,6 +6824,10 @@ packages: resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} engines: {node: '>=20.0.0'} + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + loader-runner@4.3.1: resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} engines: {node: '>=6.11.5'} @@ -5906,6 +6846,12 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + lodash@4.17.23: resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} @@ -5923,6 +6869,9 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + lowlight@1.20.0: resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} @@ -5968,11 +6917,15 @@ packages: engines: {node: '>= 20'} hasBin: true - marked@17.0.4: - resolution: {integrity: sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==} + marked@17.0.5: + resolution: {integrity: sha512-6hLvc0/JEbRjRgzI6wnT2P1XuM1/RrrDEX0kPt0N7jGm1133g6X7DlxFasUIx+72aKAr904GTxhSLDrd5DIlZg==} engines: {node: '>= 20'} hasBin: true + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mdast-util-directive@3.1.0: resolution: {integrity: sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==} @@ -6042,9 +6995,6 @@ packages: mdn-data@2.23.0: resolution: {integrity: sha512-786vq1+4079JSeu2XdcDjrhi/Ry7BWtjDl9WtGPWLiIHb2T66GvIVflZTBoSNZ5JqTtJGYEVMuFA/lbQlMOyDQ==} - mdn-data@2.27.1: - resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} - memoize-one@5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} @@ -6184,6 +7134,11 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + mime@4.1.0: resolution: {integrity: sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==} engines: {node: '>=16'} @@ -6225,8 +7180,13 @@ packages: mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - mlly@1.8.1: - resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} module-alias@2.3.4: resolution: {integrity: sha512-bOclZt8hkpuGgSSoG07PKmvzTizROilUTvLNyrMqvlC9snhs7y7GzjNWAVbISIOlhCP1T14rH1PDAV9iNyBq/w==} @@ -6299,6 +7259,9 @@ packages: sass: optional: true + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + node-abi@3.89.0: resolution: {integrity: sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==} engines: {node: '>=10'} @@ -6312,6 +7275,10 @@ packages: node-releases@2.0.36: resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + normalize-package-data@8.0.0: + resolution: {integrity: sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==} + engines: {node: ^20.17.0 || >=22.9.0} + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -6374,6 +7341,10 @@ packages: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + openapi-types@12.1.3: resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} @@ -6381,24 +7352,24 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - oxc-parser@0.120.0: - resolution: {integrity: sha512-WyPWZlcIm+Fkte63FGfgFB8mAAk33aH9h5N9lphXVOHSXEBFFsmYdOBedVKly363aWABjZdaj/m9lBfEY4wt+w==} + oxc-parser@0.121.0: + resolution: {integrity: sha512-ek9o58+SCv6AV7nchiAcUJy1DNE2CC5WRdBcO0mF+W4oRjNQfPO7b3pLjTHSFECpHkKGOZSQxx3hk8viIL5YCg==} engines: {node: ^20.19.0 || >=22.12.0} oxc-resolver@11.19.1: resolution: {integrity: sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==} - oxfmt@0.41.0: - resolution: {integrity: sha512-sKLdJZdQ3bw6x9qKiT7+eID4MNEXlDHf5ZacfIircrq6Qwjk0L6t2/JQlZZrVHTXJawK3KaMuBoJnEJPcqCEdg==} + oxfmt@0.42.0: + resolution: {integrity: sha512-QhejGErLSMReNuZ6vxgFHDyGoPbjTRNi6uGHjy0cvIjOQFqD6xmr/T+3L41ixR3NIgzcNiJ6ylQKpvShTgDfqg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - oxlint-tsgolint@0.17.1: - resolution: {integrity: sha512-gJc7hb1ZQFbWjRDYpu1XG+5IRdr1S/Jz/W2ohcpaqIXuDmHU0ujGiM0x05J0nIfwMF3HOEcANi/+j6T0Uecdpg==} + oxlint-tsgolint@0.17.3: + resolution: {integrity: sha512-1eh4bcpOMw0e7+YYVxmhFc2mo/V6hJ2+zfukqf+GprvVn3y94b69M/xNrYLmx5A+VdYe0i/bJ2xOs6Hp/jRmRA==} hasBin: true - oxlint@1.56.0: - resolution: {integrity: sha512-Q+5Mj5PVaH/R6/fhMMFzw4dT+KPB+kQW4kaL8FOIq7tfhlnEVp6+3lcWqFruuTNlUo9srZUW3qH7Id4pskeR6g==} + oxlint@1.57.0: + resolution: {integrity: sha512-DGFsuBX5MFZX9yiDdtKjTrYPq45CZ8Fft6qCltJITYZxfwYjVdGf/6wycGYTACloauwIPxUnYhBVeZbHvleGhw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -6411,6 +7382,10 @@ packages: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-limit@7.3.0: + resolution: {integrity: sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==} + engines: {node: '>=20'} + p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -6418,6 +7393,10 @@ packages: package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + pad-right@0.2.2: + resolution: {integrity: sha512-4cy8M95ioIGolCoMmm2cMntGR1lPLEbOMzOKu8bzjuJP6JpzEMQcDHmh7hHLYGgob+nKe1YHFMaG4V59HQa89g==} + engines: {node: '>=0.10.0'} + pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} @@ -6444,6 +7423,10 @@ packages: parse-imports-exports@0.2.4: resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==} + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + parse-statements@1.0.11: resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==} @@ -6536,6 +7519,16 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + engines: {node: '>=18'} + hasBin: true + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -6622,6 +7615,10 @@ packages: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} @@ -6640,15 +7637,26 @@ packages: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-expr@2.0.6: + resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} + property-information@5.6.0: resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} @@ -6713,8 +7721,8 @@ packages: react: '>= 16.3.0' react-dom: '>= 16.3.0' - react-easy-crop@5.5.6: - resolution: {integrity: sha512-Jw3/ozs8uXj3NpL511Suc4AHY+mLRO23rUgipXvNYKqezcFSYHxe4QXibBymkOoY6oOtLVMPO2HNPRHYvMPyTw==} + react-easy-crop@5.5.7: + resolution: {integrity: sha512-kYo4NtMeXFQB7h1U+h5yhUkE46WQbQdq7if54uDlbMdZHdRgNehfvaFrXnFw5NR1PNoUOJIfTwLnWmEx/MaZnA==} peerDependencies: react: '>=16.4.0' react-dom: '>=16.4.0' @@ -6733,14 +7741,14 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' - react-i18next@16.6.1: - resolution: {integrity: sha512-izjXh+AkBLy3h3xe3sh6Gg1flhFHc3UyzsMftMKYJr2Z7WvAZQIdjjpHypctN41zFoeLdJUNGDgP1+Qich2fYg==} + react-i18next@16.6.6: + resolution: {integrity: sha512-ZgL2HUoW34UKUkOV7uSQFE1CDnRPD+tCR3ywSuWH7u2iapnz86U8Bi3Vrs620qNDzCf1F47NxglCEkchCTDOHw==} peerDependencies: - i18next: '>= 25.6.2' + i18next: '>= 25.10.9' react: '>= 16.8.0' react-dom: '*' react-native: '*' - typescript: ^5 + typescript: ^5 || ^6 peerDependenciesMeta: react-dom: optional: true @@ -6854,6 +7862,14 @@ packages: read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + read-package-up@12.0.0: + resolution: {integrity: sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw==} + engines: {node: '>=20'} + + read-pkg@10.1.0: + resolution: {integrity: sha512-I8g2lArQiP78ll51UeMZojewtYgIRCKCWqZEgOO8c/uefTI+XDXvCSXu3+YNUaTNvZzobrL5+SqHjBrByRRTdg==} + engines: {node: '>=20'} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -6892,6 +7908,9 @@ packages: resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + refractor@3.6.0: resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==} @@ -6899,6 +7918,9 @@ packages: resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + regexp-match-indices@1.0.2: + resolution: {integrity: sha512-DwZuAkt8NF5mKwGGER1EGh2PRqyvhRhhLviH+R8y8dIuaQROlUfXjt4s9ZTXstIsSkptf06BSvwcEmmfheJJWQ==} + regexp-tree@0.1.27: resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} hasBin: true @@ -6949,6 +7971,10 @@ packages: remend@1.3.0: resolution: {integrity: sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw==} + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -6967,6 +7993,10 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -6986,8 +8016,13 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - robust-predicates@3.0.2: - resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + + rolldown@1.0.0-rc.12: + resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true rollup@4.59.0: resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} @@ -7029,10 +8064,6 @@ packages: resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} engines: {node: '>=11.0.0'} - saxes@6.0.0: - resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} - engines: {node: '>=v12.22.7'} - scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -7048,6 +8079,9 @@ packages: resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} engines: {node: ^14.0.0 || >=16.0.0} + seed-random@2.2.0: + resolution: {integrity: sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -7141,20 +8175,29 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + spdx-exceptions@2.5.0: resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + spdx-expression-parse@4.0.0: resolution: {integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==} spdx-license-ids@3.0.23: resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} - srvx@0.11.12: - resolution: {integrity: sha512-AQfrGqntqVPXgP03pvBDN1KyevHC+KmYVqb8vVf4N+aomQqdhaZxjvoVp+AOm4u6x+GgNQY3MVzAUIn+TqwkOA==} + srvx@0.11.13: + resolution: {integrity: sha512-oknN6qduuMPafxKtHucUeG32Q963pjriA5g3/Bl05cwEsUe5VVbIU4qR9LrALHbipSCyBe+VmfDGGydqazDRkw==} engines: {node: '>=20.16.0'} hasBin: true + stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + state-local@1.0.7: resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} @@ -7165,8 +8208,8 @@ packages: resolution: {integrity: sha512-9SN0XIjBBXCT6ZXXVnScJN4KP2RyFg6B8sEoFlugVHMANysfaEni4LTWlvUQQ/R0wgZl1Ovt9KBQbzn21kHoZA==} engines: {node: '>=20.19.0'} - storybook@10.3.1: - resolution: {integrity: sha512-i/CA1dUyVcF6cNL3tgPTQ/G6Evh6r3QdATuiiKObrA3QkEKmt3jrY+WeuQA7FCcmHk/vKabeliNrblaff8aY6Q==} + storybook@10.3.3: + resolution: {integrity: sha512-tMoRAts9EVqf+mEMPLC6z1DPyHbcPe+CV1MhLN55IKsl0HxNjvVGK44rVPSePbltPE6vIsn4bdRj6CCUt8SJwQ==} hasBin: true peerDependencies: prettier: ^2 || ^3 @@ -7180,6 +8223,10 @@ packages: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 + string-argv@0.3.1: + resolution: {integrity: sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==} + engines: {node: '>=0.6.19'} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -7231,6 +8278,9 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + structured-clone-es@2.0.0: + resolution: {integrity: sha512-5UuAHmBLXYPCl22xWJrFuGmIhBKQzxISPVz6E7nmTmTcAOpUzlbjKJsRrCE4vADmMQ0dzeCnlWn9XufnAGf76Q==} + style-to-js@1.1.21: resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} @@ -7275,9 +8325,6 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - symbol-tree@3.2.4: - resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - synckit@0.11.12: resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} engines: {node: ^14.18.0 || >=16.0.0} @@ -7309,10 +8356,6 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - tapable@2.3.0: - resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} - engines: {node: '>=6'} - tapable@2.3.2: resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} engines: {node: '>=6'} @@ -7360,6 +8403,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + tiny-case@1.0.3: + resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} + tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} @@ -7372,6 +8418,9 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.4: resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} engines: {node: '>=18'} @@ -7418,17 +8467,16 @@ packages: resolution: {integrity: sha512-A5F0cM6+mDleacLIEUkmfpkBbnHJFV1d2rprHU2MXNk7mlxHq2zGojA+SRvQD1RoMo9gqjZPWEaKG4v1BQ48lw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + toposort@2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - tough-cookie@6.0.1: - resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} - engines: {node: '>=16'} - - tr46@6.0.0: - resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} - engines: {node: '>=20'} + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -7436,8 +8484,8 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - ts-api-utils@2.4.0: - resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' @@ -7487,6 +8535,25 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + tsx@4.21.0: resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} @@ -7502,8 +8569,16 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-fest@5.4.4: - resolution: {integrity: sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==} + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + type-fest@5.5.0: + resolution: {integrity: sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==} engines: {node: '>=20'} typescript@5.9.3: @@ -7536,13 +8611,13 @@ packages: resolution: {integrity: sha512-jxytwMHhsbdpBXxLAcuu0fzlQeXCNnWdDyRHpvWsUl8vd98UwYdl9YTyn8/HcpcJPC3pwUveefsa3zTxyD/ERg==} engines: {node: '>=20.18.1'} - undici@7.24.6: - resolution: {integrity: sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA==} - engines: {node: '>=20.18.1'} - unicode-trie@2.0.0: resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + unicorn-magic@0.4.0: + resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} + engines: {node: '>=20'} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -7591,6 +8666,9 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + upper-case-first@2.0.2: + resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -7655,6 +8733,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-arity@1.1.0: + resolution: {integrity: sha512-kkyIsXKwemfSy8ZEoaIz06ApApnWsk5hQO0vLjZS6UkBiGiW++Jsyb8vSBoc0WKlffGoGs5yYy/j5pp8zckrFA==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -7666,14 +8747,17 @@ packages: resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} hasBin: true - valibot@1.3.0: - resolution: {integrity: sha512-SItIaOFnWYho/AcRU5gOtyfkTsuDTC3tRv+jy4/py8xERPnvHdM+ybD1iIqWTATVWG1nZetOfwZKq5upBjSqzw==} + valibot@1.3.1: + resolution: {integrity: sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg==} peerDependencies: typescript: '>=5' peerDependenciesMeta: typescript: optional: true + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -7683,15 +8767,14 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vinext@https://pkg.pr.new/vinext@b6a2cac: - resolution: {tarball: https://pkg.pr.new/vinext@b6a2cac} - version: 0.0.5 + vinext@0.0.38: + resolution: {integrity: sha512-zlQswirXCApDgAFq1eoO/YbRlavGE+Bnowz5vXoQa2EmbFhYg52+T8SZs1QWdOqkbZMhpLIV/iaWvHtkRv2t4Q==} engines: {node: '>=22'} hasBin: true peerDependencies: '@mdx-js/rollup': ^3.0.0 '@vitejs/plugin-react': ^5.1.4 || ^6.0.0 - '@vitejs/plugin-rsc': ^0.5.19 + '@vitejs/plugin-rsc': ^0.5.21 react: '>=19.2.0' react-dom: '>=19.2.0' react-server-dom-webpack: ^19.2.4 @@ -7704,41 +8787,31 @@ packages: react-server-dom-webpack: optional: true - vite-dev-rpc@1.1.0: - resolution: {integrity: sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==} - peerDependencies: - vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0 - - vite-hot-client@2.1.0: - resolution: {integrity: sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==} - peerDependencies: - vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 - vite-plugin-commonjs@0.10.4: resolution: {integrity: sha512-eWQuvQKCcx0QYB5e5xfxBNjQKyrjEWZIR9UOkOV6JAgxVhtbZvCOF+FNC2ZijBJ3U3Px04ZMMyyMyFBVWIJ5+g==} vite-plugin-dynamic-import@1.6.0: resolution: {integrity: sha512-TM0sz70wfzTIo9YCxVFwS8OA9lNREsh+0vMHGSkWDTZ7bgd1Yjs5RV8EgB634l/91IsXJReg0xtmuQqP0mf+rg==} - vite-plugin-inspect@11.3.3: - resolution: {integrity: sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==} + vite-plugin-inspect@12.0.0-beta.1: + resolution: {integrity: sha512-ang8DMcQxr2MJRjdvwabkD0uOPFB5/fP4hldZvAqCl82SABXK1zYLyZKGrauCblR61cvDUavxyiHbtD4zTdw0A==} engines: {node: '>=14'} peerDependencies: '@nuxt/kit': '*' - vite: ^6.0.0 || ^7.0.0-0 + vite: ^8.0.0-0 peerDependenciesMeta: '@nuxt/kit': optional: true - vite-plugin-storybook-nextjs@3.2.3: - resolution: {integrity: sha512-NQvkiZKfbGmk0j3mYeTJnGiucV+VOcryCsm/CoE7rBVRrpVntg5lWj+CbosFwHhGPpWQ3I4HJ3nSRzDq0u74Ug==} + vite-plugin-storybook-nextjs@3.2.4: + resolution: {integrity: sha512-shFOJpGQsWDS1FLm8BR8b6FIQC65pFZ5a0IUFGLiBHAX1eRz0N8TOhUJN4p708zfPBLDXqWzj++ocECe8gSoMg==} peerDependencies: next: ^14.1.0 || ^15.0.0 || ^16.0.0 - storybook: ^0.0.0-0 || ^9.0.0 || ^10.0.0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 + storybook: ^0.0.0-0 || ^9.0.0 || ^10.0.0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - vite-plus@0.1.13: - resolution: {integrity: sha512-DP87+eRFhYYDdcjm2nr3DOKt0cv6mIXCNXn+zc59YHgR0Wh7uL2E/55mjusJ7ajwcXenpGW+c4KPeoqhQAbhxg==} + vite-plus@0.1.14: + resolution: {integrity: sha512-p4pWlpZZNiEsHxPWNdeIU9iuPix3ydm3ficb0dXPggoyIkdotfXtvn2NPX9KwfiQImU72EVEs4+VYBZYNcUYrw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -7755,6 +8828,49 @@ packages: peerDependencies: vite: '*' + vite@8.0.3: + resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: 0.27.2 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: 2.8.3 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vitefu@1.1.2: resolution: {integrity: sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==} peerDependencies: @@ -7763,8 +8879,8 @@ packages: vite: optional: true - vitest-canvas-mock@1.1.3: - resolution: {integrity: sha512-zlKJR776Qgd+bcACPh0Pq5MG3xWq+CdkACKY/wX4Jyija0BSz8LH3aCCgwFKYFwtm565+050YFEGG9Ki0gE/Hw==} + vitest-canvas-mock@1.1.4: + resolution: {integrity: sha512-4boWHY+STwAxGl1+uwakNNoQky5EjPLC8HuponXNoAscYyT1h/F7RUvTkl4IyF/MiWr3V8Q626je3Iel3eArqA==} peerDependencies: vitest: ^3.0.0 || ^4.0.0 @@ -7798,10 +8914,6 @@ packages: peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - w3c-xmlserializer@5.0.0: - resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} - engines: {node: '>=18'} - walk-up-path@4.0.0: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} @@ -7816,10 +8928,6 @@ packages: web-vitals@5.1.0: resolution: {integrity: sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==} - webidl-conversions@8.0.1: - resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} - engines: {node: '>=20'} - webpack-sources@3.3.4: resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==} engines: {node: '>=10.13.0'} @@ -7850,14 +8958,6 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} - whatwg-mimetype@5.0.0: - resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} - engines: {node: '>=20'} - - whatwg-url@16.0.1: - resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -7874,18 +8974,6 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.19.0: - resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.20.0: resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} engines: {node: '>=10.0.0'} @@ -7902,16 +8990,17 @@ packages: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} + xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} - xml-name-validator@5.0.0: - resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} - engines: {node: '>=18'} - - xmlchars@2.2.0: - resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} @@ -7945,9 +9034,16 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + yoga-layout@3.2.1: resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + yup@1.7.1: + resolution: {integrity: sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==} + zen-observable@0.10.0: resolution: {integrity: sha512-iI3lT0iojZhKwT5DaFy2Ce42n3yFcLdFyOh01G7H0flMY60P8MJuVFEoJoNwXlmAyQ45GrjL6AcZmmlv8A5rbw==} @@ -8013,27 +9109,27 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@amplitude/analytics-browser@2.37.0': + '@amplitude/analytics-browser@2.38.0': dependencies: - '@amplitude/analytics-core': 2.43.0 - '@amplitude/plugin-autocapture-browser': 1.24.1 - '@amplitude/plugin-custom-enrichment-browser': 0.1.0 - '@amplitude/plugin-network-capture-browser': 1.9.9 - '@amplitude/plugin-page-url-enrichment-browser': 0.7.0 - '@amplitude/plugin-page-view-tracking-browser': 2.9.1 - '@amplitude/plugin-web-vitals-browser': 1.1.24 + '@amplitude/analytics-core': 2.44.0 + '@amplitude/plugin-autocapture-browser': 1.25.0 + '@amplitude/plugin-custom-enrichment-browser': 0.1.2 + '@amplitude/plugin-network-capture-browser': 1.9.11 + '@amplitude/plugin-page-url-enrichment-browser': 0.7.3 + '@amplitude/plugin-page-view-tracking-browser': 2.9.4 + '@amplitude/plugin-web-vitals-browser': 1.1.26 tslib: 2.8.1 - '@amplitude/analytics-client-common@2.4.39': + '@amplitude/analytics-client-common@2.4.41': dependencies: '@amplitude/analytics-connector': 1.6.4 - '@amplitude/analytics-core': 2.43.0 + '@amplitude/analytics-core': 2.44.0 '@amplitude/analytics-types': 2.11.1 tslib: 2.8.1 '@amplitude/analytics-connector@1.6.4': {} - '@amplitude/analytics-core@2.43.0': + '@amplitude/analytics-core@2.44.0': dependencies: '@amplitude/analytics-connector': 1.6.4 '@types/zen-observable': 0.8.3 @@ -8047,96 +9143,100 @@ snapshots: dependencies: js-base64: 3.7.8 - '@amplitude/plugin-autocapture-browser@1.24.1': + '@amplitude/plugin-autocapture-browser@1.25.0': dependencies: - '@amplitude/analytics-core': 2.43.0 + '@amplitude/analytics-core': 2.44.0 tslib: 2.8.1 - '@amplitude/plugin-custom-enrichment-browser@0.1.0': + '@amplitude/plugin-custom-enrichment-browser@0.1.2': dependencies: - '@amplitude/analytics-core': 2.43.0 + '@amplitude/analytics-core': 2.44.0 tslib: 2.8.1 - '@amplitude/plugin-network-capture-browser@1.9.9': + '@amplitude/plugin-network-capture-browser@1.9.11': dependencies: - '@amplitude/analytics-core': 2.43.0 + '@amplitude/analytics-core': 2.44.0 tslib: 2.8.1 - '@amplitude/plugin-page-url-enrichment-browser@0.7.0': + '@amplitude/plugin-page-url-enrichment-browser@0.7.3': dependencies: - '@amplitude/analytics-core': 2.43.0 + '@amplitude/analytics-core': 2.44.0 tslib: 2.8.1 - '@amplitude/plugin-page-view-tracking-browser@2.9.1': + '@amplitude/plugin-page-view-tracking-browser@2.9.4': dependencies: - '@amplitude/analytics-core': 2.43.0 + '@amplitude/analytics-core': 2.44.0 tslib: 2.8.1 - '@amplitude/plugin-session-replay-browser@1.27.1(@amplitude/rrweb@2.0.0-alpha.36)(rollup@4.59.0)': + '@amplitude/plugin-session-replay-browser@1.27.5(@amplitude/rrweb@2.0.0-alpha.37)(rollup@4.59.0)': dependencies: - '@amplitude/analytics-client-common': 2.4.39 - '@amplitude/analytics-core': 2.43.0 + '@amplitude/analytics-client-common': 2.4.41 + '@amplitude/analytics-core': 2.44.0 '@amplitude/analytics-types': 2.11.1 - '@amplitude/rrweb-plugin-console-record': 2.0.0-alpha.36(@amplitude/rrweb@2.0.0-alpha.36) + '@amplitude/rrweb-plugin-console-record': 2.0.0-alpha.36(@amplitude/rrweb@2.0.0-alpha.37) '@amplitude/rrweb-record': 2.0.0-alpha.36 - '@amplitude/session-replay-browser': 1.34.1(@amplitude/rrweb@2.0.0-alpha.36)(rollup@4.59.0) + '@amplitude/session-replay-browser': 1.35.0(@amplitude/rrweb@2.0.0-alpha.37)(rollup@4.59.0) idb-keyval: 6.2.2 tslib: 2.8.1 transitivePeerDependencies: - '@amplitude/rrweb' - rollup - '@amplitude/plugin-web-vitals-browser@1.1.24': + '@amplitude/plugin-web-vitals-browser@1.1.26': dependencies: - '@amplitude/analytics-core': 2.43.0 + '@amplitude/analytics-core': 2.44.0 tslib: 2.8.1 web-vitals: 5.1.0 - '@amplitude/rrdom@2.0.0-alpha.36': + '@amplitude/rrdom@2.0.0-alpha.37': dependencies: - '@amplitude/rrweb-snapshot': 2.0.0-alpha.36 + '@amplitude/rrweb-snapshot': 2.0.0-alpha.37 '@amplitude/rrweb-packer@2.0.0-alpha.36': dependencies: - '@amplitude/rrweb-types': 2.0.0-alpha.36 + '@amplitude/rrweb-types': 2.0.0-alpha.37 fflate: 0.4.8 - '@amplitude/rrweb-plugin-console-record@2.0.0-alpha.36(@amplitude/rrweb@2.0.0-alpha.36)': + '@amplitude/rrweb-plugin-console-record@2.0.0-alpha.36(@amplitude/rrweb@2.0.0-alpha.37)': dependencies: - '@amplitude/rrweb': 2.0.0-alpha.36 + '@amplitude/rrweb': 2.0.0-alpha.37 '@amplitude/rrweb-record@2.0.0-alpha.36': dependencies: - '@amplitude/rrweb': 2.0.0-alpha.36 - '@amplitude/rrweb-types': 2.0.0-alpha.36 + '@amplitude/rrweb': 2.0.0-alpha.37 + '@amplitude/rrweb-types': 2.0.0-alpha.37 - '@amplitude/rrweb-snapshot@2.0.0-alpha.36': + '@amplitude/rrweb-snapshot@2.0.0-alpha.37': dependencies: postcss: 8.5.8 '@amplitude/rrweb-types@2.0.0-alpha.36': {} + '@amplitude/rrweb-types@2.0.0-alpha.37': {} + '@amplitude/rrweb-utils@2.0.0-alpha.36': {} - '@amplitude/rrweb@2.0.0-alpha.36': + '@amplitude/rrweb-utils@2.0.0-alpha.37': {} + + '@amplitude/rrweb@2.0.0-alpha.37': dependencies: - '@amplitude/rrdom': 2.0.0-alpha.36 - '@amplitude/rrweb-snapshot': 2.0.0-alpha.36 - '@amplitude/rrweb-types': 2.0.0-alpha.36 - '@amplitude/rrweb-utils': 2.0.0-alpha.36 + '@amplitude/rrdom': 2.0.0-alpha.37 + '@amplitude/rrweb-snapshot': 2.0.0-alpha.37 + '@amplitude/rrweb-types': 2.0.0-alpha.37 + '@amplitude/rrweb-utils': 2.0.0-alpha.37 '@types/css-font-loading-module': 0.0.7 '@xstate/fsm': 1.6.5 base64-arraybuffer: 1.0.2 mitt: 3.0.1 - '@amplitude/session-replay-browser@1.34.1(@amplitude/rrweb@2.0.0-alpha.36)(rollup@4.59.0)': + '@amplitude/session-replay-browser@1.35.0(@amplitude/rrweb@2.0.0-alpha.37)(rollup@4.59.0)': dependencies: - '@amplitude/analytics-client-common': 2.4.39 - '@amplitude/analytics-core': 2.43.0 + '@amplitude/analytics-client-common': 2.4.41 + '@amplitude/analytics-core': 2.44.0 '@amplitude/analytics-types': 2.11.1 '@amplitude/experiment-core': 0.7.2 '@amplitude/rrweb-packer': 2.0.0-alpha.36 - '@amplitude/rrweb-plugin-console-record': 2.0.0-alpha.36(@amplitude/rrweb@2.0.0-alpha.36) + '@amplitude/rrweb-plugin-console-record': 2.0.0-alpha.36(@amplitude/rrweb@2.0.0-alpha.37) '@amplitude/rrweb-record': 2.0.0-alpha.36 '@amplitude/rrweb-types': 2.0.0-alpha.36 '@amplitude/rrweb-utils': 2.0.0-alpha.36 @@ -8150,34 +9250,34 @@ snapshots: '@amplitude/targeting@0.2.0': dependencies: - '@amplitude/analytics-client-common': 2.4.39 - '@amplitude/analytics-core': 2.43.0 + '@amplitude/analytics-client-common': 2.4.41 + '@amplitude/analytics-core': 2.44.0 '@amplitude/analytics-types': 2.11.1 '@amplitude/experiment-core': 0.7.2 idb: 8.0.0 tslib: 2.8.1 - '@antfu/eslint-config@7.7.3(@eslint-react/eslint-plugin@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@next/eslint-plugin-next@16.2.1)(@typescript-eslint/rule-tester@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.1(typescript@5.9.3))(@typescript-eslint/utils@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(@vue/compiler-sfc@3.5.30)(eslint-plugin-react-hooks@7.0.1(eslint@10.1.0(jiti@1.21.7)))(eslint-plugin-react-refresh@0.5.2(eslint@10.1.0(jiti@1.21.7)))(eslint@10.1.0(jiti@1.21.7))(oxlint@1.56.0(oxlint-tsgolint@0.17.1))(typescript@5.9.3)': + '@antfu/eslint-config@7.7.3(@eslint-react/eslint-plugin@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@next/eslint-plugin-next@16.2.1)(@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3))(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(@vue/compiler-sfc@3.5.31)(eslint-plugin-react-hooks@7.0.1(eslint@10.1.0(jiti@1.21.7)))(eslint-plugin-react-refresh@0.5.2(eslint@10.1.0(jiti@1.21.7)))(eslint@10.1.0(jiti@1.21.7))(oxlint@1.57.0(oxlint-tsgolint@0.17.3))(typescript@5.9.3)': dependencies: '@antfu/install-pkg': 1.1.0 '@clack/prompts': 1.1.0 - '@e18e/eslint-plugin': 0.2.0(eslint@10.1.0(jiti@1.21.7))(oxlint@1.56.0(oxlint-tsgolint@0.17.1)) + '@e18e/eslint-plugin': 0.2.0(eslint@10.1.0(jiti@1.21.7))(oxlint@1.57.0(oxlint-tsgolint@0.17.3)) '@eslint-community/eslint-plugin-eslint-comments': 4.7.1(eslint@10.1.0(jiti@1.21.7)) '@eslint/markdown': 7.5.1 '@stylistic/eslint-plugin': 5.10.0(eslint@10.1.0(jiti@1.21.7)) - '@typescript-eslint/eslint-plugin': 8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/parser': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@vitest/eslint-plugin': 1.6.12(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@vitest/eslint-plugin': 1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) ansis: 4.2.0 cac: 7.0.0 eslint: 10.1.0(jiti@1.21.7) - eslint-config-flat-gitignore: 2.2.1(eslint@10.1.0(jiti@1.21.7)) + eslint-config-flat-gitignore: 2.3.0(eslint@10.1.0(jiti@1.21.7)) eslint-flat-config-utils: 3.0.2 eslint-merge-processors: 2.0.0(eslint@10.1.0(jiti@1.21.7)) eslint-plugin-antfu: 3.2.2(eslint@10.1.0(jiti@1.21.7)) - eslint-plugin-command: 3.5.2(@typescript-eslint/rule-tester@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.1(typescript@5.9.3))(@typescript-eslint/utils@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7)) + eslint-plugin-command: 3.5.2(@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3))(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7)) eslint-plugin-import-lite: 0.5.2(eslint@10.1.0(jiti@1.21.7)) - eslint-plugin-jsdoc: 62.8.0(eslint@10.1.0(jiti@1.21.7)) + eslint-plugin-jsdoc: 62.8.1(eslint@10.1.0(jiti@1.21.7)) eslint-plugin-jsonc: 3.1.2(eslint@10.1.0(jiti@1.21.7)) eslint-plugin-n: 17.24.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) eslint-plugin-no-only-tests: 3.3.0 @@ -8186,10 +9286,10 @@ snapshots: eslint-plugin-regexp: 3.1.0(eslint@10.1.0(jiti@1.21.7)) eslint-plugin-toml: 1.3.1(eslint@10.1.0(jiti@1.21.7)) eslint-plugin-unicorn: 63.0.0(eslint@10.1.0(jiti@1.21.7)) - eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7)) - eslint-plugin-vue: 10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.1.0(jiti@1.21.7)))(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@1.21.7))) + eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7)) + eslint-plugin-vue: 10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.1.0(jiti@1.21.7)))(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@1.21.7))) eslint-plugin-yml: 3.3.1(eslint@10.1.0(jiti@1.21.7)) - eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.30)(eslint@10.1.0(jiti@1.21.7)) + eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.31)(eslint@10.1.0(jiti@1.21.7)) globals: 17.4.0 local-pkg: 1.1.2 parse-gitignore: 2.0.0 @@ -8227,27 +9327,6 @@ snapshots: '@antfu/utils@8.1.1': {} - '@asamuzakjp/css-color@5.1.1': - dependencies: - '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-tokenizer': 4.0.0 - lru-cache: 11.2.7 - optional: true - - '@asamuzakjp/dom-selector@7.0.4': - dependencies: - '@asamuzakjp/nwsapi': 2.3.9 - bidi-js: 1.0.3 - css-tree: 3.2.1 - is-potential-custom-element-name: 1.0.1 - lru-cache: 11.2.7 - optional: true - - '@asamuzakjp/nwsapi@2.3.9': - optional: true - '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -8269,7 +9348,7 @@ snapshots: '@babel/types': 7.29.0 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -8341,7 +9420,7 @@ snapshots: '@babel/parser': 7.29.2 '@babel/template': 7.28.6 '@babel/types': 7.29.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -8378,11 +9457,6 @@ snapshots: '@braintree/sanitize-url@7.1.2': {} - '@bramus/specificity@2.4.2': - dependencies: - css-tree: 3.2.1 - optional: true - '@chevrotain/cst-dts-gen@11.1.2': dependencies: '@chevrotain/gast': 11.1.2 @@ -8400,13 +9474,13 @@ snapshots: '@chevrotain/utils@11.1.2': {} - '@chromatic-com/storybook@5.0.2(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + '@chromatic-com/storybook@5.1.1(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: '@neoconfetti/react': 1.0.0 chromatic: 13.3.5 filesize: 10.1.6 jsonfile: 6.2.0 - storybook: 10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) strip-ansi: 7.2.0 transitivePeerDependencies: - '@chromatic-com/cypress' @@ -8434,7 +9508,7 @@ snapshots: '@code-inspector/core@1.4.5': dependencies: - '@vue/compiler-dom': 3.5.30 + '@vue/compiler-dom': 3.5.31 chalk: 4.1.2 dotenv: 16.6.1 launch-ide: 1.4.3 @@ -8474,55 +9548,135 @@ snapshots: transitivePeerDependencies: - supports-color - '@csstools/color-helpers@6.0.2': + '@colors/colors@1.5.0': optional: true - '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + '@cucumber/ci-environment@13.0.0': {} + + '@cucumber/cucumber-expressions@19.0.0': dependencies: - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-tokenizer': 4.0.0 - optional: true + regexp-match-indices: 1.0.2 - '@csstools/css-color-parser@4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + '@cucumber/cucumber@12.7.0': dependencies: - '@csstools/color-helpers': 6.0.2 - '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-tokenizer': 4.0.0 - optional: true + '@cucumber/ci-environment': 13.0.0 + '@cucumber/cucumber-expressions': 19.0.0 + '@cucumber/gherkin': 38.0.0 + '@cucumber/gherkin-streams': 6.0.0(@cucumber/gherkin@38.0.0)(@cucumber/message-streams@4.0.1(@cucumber/messages@32.0.1))(@cucumber/messages@32.0.1) + '@cucumber/gherkin-utils': 11.0.0 + '@cucumber/html-formatter': 23.0.0(@cucumber/messages@32.0.1) + '@cucumber/junit-xml-formatter': 0.9.0(@cucumber/messages@32.0.1) + '@cucumber/message-streams': 4.0.1(@cucumber/messages@32.0.1) + '@cucumber/messages': 32.0.1 + '@cucumber/pretty-formatter': 1.0.1(@cucumber/cucumber@12.7.0)(@cucumber/messages@32.0.1) + '@cucumber/tag-expressions': 9.1.0 + assertion-error-formatter: 3.0.0 + capital-case: 1.0.4 + chalk: 4.1.2 + cli-table3: 0.6.5 + commander: 14.0.3 + debug: 4.4.3(supports-color@8.1.1) + error-stack-parser: 2.1.4 + figures: 3.2.0 + glob: 13.0.6 + has-ansi: 4.0.1 + indent-string: 4.0.0 + is-installed-globally: 0.4.0 + is-stream: 2.0.1 + knuth-shuffle-seeded: 1.0.6 + lodash.merge: 4.6.2 + lodash.mergewith: 4.6.2 + luxon: 3.7.2 + mime: 3.0.0 + mkdirp: 3.0.1 + mz: 2.7.0 + progress: 2.0.3 + read-package-up: 12.0.0 + semver: 7.7.4 + string-argv: 0.3.1 + supports-color: 8.1.1 + type-fest: 4.41.0 + util-arity: 1.1.0 + yaml: 2.8.3 + yup: 1.7.1 - '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + '@cucumber/gherkin-streams@6.0.0(@cucumber/gherkin@38.0.0)(@cucumber/message-streams@4.0.1(@cucumber/messages@32.0.1))(@cucumber/messages@32.0.1)': dependencies: - '@csstools/css-tokenizer': 4.0.0 - optional: true + '@cucumber/gherkin': 38.0.0 + '@cucumber/message-streams': 4.0.1(@cucumber/messages@32.0.1) + '@cucumber/messages': 32.0.1 + commander: 14.0.0 + source-map-support: 0.5.21 - '@csstools/css-syntax-patches-for-csstree@1.1.2(css-tree@3.2.1)': - optionalDependencies: - css-tree: 3.2.1 - optional: true + '@cucumber/gherkin-utils@11.0.0': + dependencies: + '@cucumber/gherkin': 38.0.0 + '@cucumber/messages': 32.0.1 + '@teppeis/multimaps': 3.0.0 + commander: 14.0.2 + source-map-support: 0.5.21 - '@csstools/css-tokenizer@4.0.0': - optional: true + '@cucumber/gherkin@38.0.0': + dependencies: + '@cucumber/messages': 32.0.1 - '@e18e/eslint-plugin@0.2.0(eslint@10.1.0(jiti@1.21.7))(oxlint@1.56.0(oxlint-tsgolint@0.17.1))': + '@cucumber/html-formatter@23.0.0(@cucumber/messages@32.0.1)': + dependencies: + '@cucumber/messages': 32.0.1 + + '@cucumber/junit-xml-formatter@0.9.0(@cucumber/messages@32.0.1)': + dependencies: + '@cucumber/messages': 32.0.1 + '@cucumber/query': 14.7.0(@cucumber/messages@32.0.1) + '@teppeis/multimaps': 3.0.0 + luxon: 3.7.2 + xmlbuilder: 15.1.1 + + '@cucumber/message-streams@4.0.1(@cucumber/messages@32.0.1)': + dependencies: + '@cucumber/messages': 32.0.1 + + '@cucumber/messages@32.0.1': + dependencies: + class-transformer: 0.5.1 + reflect-metadata: 0.2.2 + + '@cucumber/pretty-formatter@1.0.1(@cucumber/cucumber@12.7.0)(@cucumber/messages@32.0.1)': + dependencies: + '@cucumber/cucumber': 12.7.0 + '@cucumber/messages': 32.0.1 + ansi-styles: 5.2.0 + cli-table3: 0.6.5 + figures: 3.2.0 + ts-dedent: 2.2.0 + + '@cucumber/query@14.7.0(@cucumber/messages@32.0.1)': + dependencies: + '@cucumber/messages': 32.0.1 + '@teppeis/multimaps': 3.0.0 + lodash.sortby: 4.7.0 + + '@cucumber/tag-expressions@9.1.0': {} + + '@e18e/eslint-plugin@0.2.0(eslint@10.1.0(jiti@1.21.7))(oxlint@1.57.0(oxlint-tsgolint@0.17.3))': dependencies: eslint-plugin-depend: 1.5.0(eslint@10.1.0(jiti@1.21.7)) optionalDependencies: eslint: 10.1.0(jiti@1.21.7) - oxlint: 1.56.0(oxlint-tsgolint@0.17.1) + oxlint: 1.57.0(oxlint-tsgolint@0.17.3) '@egoist/tailwindcss-icons@1.9.2(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@iconify/utils': 3.1.0 tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.3) - '@emnapi/core@1.9.0': + '@emnapi/core@1.9.1': dependencies: '@emnapi/wasi-threads': 1.2.0 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.9.0': + '@emnapi/runtime@1.9.1': dependencies: tslib: 2.8.1 optional: true @@ -8537,7 +9691,7 @@ snapshots: '@es-joy/jsdoccomment@0.84.0': dependencies: '@types/estree': 1.0.8 - '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/types': 8.57.2 comment-parser: 1.4.5 esquery: 1.7.0 jsdoc-type-pratt-parser: 7.1.1 @@ -8633,6 +9787,11 @@ snapshots: eslint: 10.1.0(jiti@1.21.7) eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@10.1.0(jiti@2.6.1))': + dependencies: + eslint: 10.1.0(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@9.27.0(jiti@1.21.7))': dependencies: eslint: 9.27.0(jiti@1.21.7) @@ -8642,9 +9801,9 @@ snapshots: '@eslint-react/ast@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) eslint: 10.1.0(jiti@1.21.7) string-ts: 2.3.1 typescript: 5.9.3 @@ -8656,9 +9815,9 @@ snapshots: '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) eslint: 10.1.0(jiti@1.21.7) ts-pattern: 5.9.0 typescript: 5.9.3 @@ -8668,24 +9827,24 @@ snapshots: '@eslint-react/eslint-plugin@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/type-utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) eslint: 10.1.0(jiti@1.21.7) eslint-plugin-react-dom: 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) eslint-plugin-react-naming-convention: 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) eslint-plugin-react-rsc: 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) eslint-plugin-react-web-api: 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) eslint-plugin-react-x: 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - ts-api-utils: 2.4.0(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color '@eslint-react/shared@3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) eslint: 10.1.0(jiti@1.21.7) ts-pattern: 5.9.0 typescript: 5.9.3 @@ -8697,9 +9856,9 @@ snapshots: dependencies: '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) eslint: 10.1.0(jiti@1.21.7) ts-pattern: 5.9.0 typescript: 5.9.3 @@ -8715,7 +9874,7 @@ snapshots: '@eslint/config-array@0.20.1': dependencies: '@eslint/object-schema': 2.1.7 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) minimatch: 3.1.5 transitivePeerDependencies: - supports-color @@ -8723,7 +9882,7 @@ snapshots: '@eslint/config-array@0.23.3': dependencies: '@eslint/object-schema': 3.0.3 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.4 transitivePeerDependencies: - supports-color @@ -8758,7 +9917,7 @@ snapshots: '@eslint/eslintrc@3.3.5': dependencies: ajv: 6.14.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -8769,6 +9928,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@eslint/js@10.0.1(eslint@10.1.0(jiti@2.6.1))': + optionalDependencies: + eslint: 10.1.0(jiti@2.6.1) + '@eslint/js@9.27.0': {} '@eslint/markdown@7.5.1': @@ -8804,9 +9967,6 @@ snapshots: '@eslint/core': 1.1.1 levn: 0.4.1 - '@exodus/bytes@1.15.0': - optional: true - '@floating-ui/core@1.7.5': dependencies: '@floating-ui/utils': 0.2.11 @@ -8862,9 +10022,9 @@ snapshots: dependencies: react: 19.2.4 - '@hono/node-server@1.19.11(hono@4.12.8)': + '@hono/node-server@1.19.11(hono@4.12.9)': dependencies: - hono: 4.12.8 + hono: 4.12.9 '@humanfs/core@0.19.1': {} @@ -8906,11 +10066,11 @@ snapshots: '@antfu/install-pkg': 1.1.0 '@antfu/utils': 8.1.1 '@iconify/types': 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) globals: 15.15.0 kolorist: 1.8.0 local-pkg: 1.1.2 - mlly: 1.8.1 + mlly: 1.8.2 transitivePeerDependencies: - supports-color @@ -8918,7 +10078,7 @@ snapshots: dependencies: '@antfu/install-pkg': 1.1.0 '@iconify/types': 2.0.0 - mlly: 1.8.1 + mlly: 1.8.2 '@img/colour@1.1.0': {} @@ -9004,7 +10164,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.9.0 + '@emnapi/runtime': 1.9.1 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -9020,11 +10180,11 @@ snapshots: dependencies: minipass: 7.1.3 - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.4(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)': + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)': dependencies: glob: 13.0.6 react-docgen-typescript: 2.4.0(typescript@5.9.3) - vite: '@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' optionalDependencies: typescript: 5.9.3 @@ -9279,10 +10439,10 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@napi-rs/wasm-runtime@1.1.1': + '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': dependencies: - '@emnapi/core': 1.9.0 - '@emnapi/runtime': 1.9.0 + '@emnapi/core': 1.9.1 + '@emnapi/runtime': 1.9.1 '@tybys/wasm-util': 0.10.1 optional: true @@ -9339,139 +10499,146 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@nolyfill/hasown@1.0.44': {} + '@nolyfill/is-core-module@1.0.39': {} '@nolyfill/safer-buffer@1.0.44': {} '@nolyfill/side-channel@1.0.44': {} - '@orpc/client@1.13.9': + '@orpc/client@1.13.13': dependencies: - '@orpc/shared': 1.13.9 - '@orpc/standard-server': 1.13.9 - '@orpc/standard-server-fetch': 1.13.9 - '@orpc/standard-server-peer': 1.13.9 + '@orpc/shared': 1.13.13 + '@orpc/standard-server': 1.13.13 + '@orpc/standard-server-fetch': 1.13.13 + '@orpc/standard-server-peer': 1.13.13 transitivePeerDependencies: - '@opentelemetry/api' - '@orpc/contract@1.13.9': + '@orpc/contract@1.13.13': dependencies: - '@orpc/client': 1.13.9 - '@orpc/shared': 1.13.9 + '@orpc/client': 1.13.13 + '@orpc/shared': 1.13.13 '@standard-schema/spec': 1.1.0 openapi-types: 12.1.3 transitivePeerDependencies: - '@opentelemetry/api' - '@orpc/openapi-client@1.13.9': + '@orpc/openapi-client@1.13.13': dependencies: - '@orpc/client': 1.13.9 - '@orpc/contract': 1.13.9 - '@orpc/shared': 1.13.9 - '@orpc/standard-server': 1.13.9 + '@orpc/client': 1.13.13 + '@orpc/contract': 1.13.13 + '@orpc/shared': 1.13.13 + '@orpc/standard-server': 1.13.13 transitivePeerDependencies: - '@opentelemetry/api' - '@orpc/shared@1.13.9': + '@orpc/shared@1.13.13': dependencies: radash: 12.1.1 - type-fest: 5.4.4 + type-fest: 5.5.0 - '@orpc/standard-server-fetch@1.13.9': + '@orpc/standard-server-fetch@1.13.13': dependencies: - '@orpc/shared': 1.13.9 - '@orpc/standard-server': 1.13.9 + '@orpc/shared': 1.13.13 + '@orpc/standard-server': 1.13.13 transitivePeerDependencies: - '@opentelemetry/api' - '@orpc/standard-server-peer@1.13.9': + '@orpc/standard-server-peer@1.13.13': dependencies: - '@orpc/shared': 1.13.9 - '@orpc/standard-server': 1.13.9 + '@orpc/shared': 1.13.13 + '@orpc/standard-server': 1.13.13 transitivePeerDependencies: - '@opentelemetry/api' - '@orpc/standard-server@1.13.9': + '@orpc/standard-server@1.13.13': dependencies: - '@orpc/shared': 1.13.9 + '@orpc/shared': 1.13.13 transitivePeerDependencies: - '@opentelemetry/api' - '@orpc/tanstack-query@1.13.9(@orpc/client@1.13.9)(@tanstack/query-core@5.95.0)': + '@orpc/tanstack-query@1.13.13(@orpc/client@1.13.13)(@tanstack/query-core@5.95.2)': dependencies: - '@orpc/client': 1.13.9 - '@orpc/shared': 1.13.9 - '@tanstack/query-core': 5.95.0 + '@orpc/client': 1.13.13 + '@orpc/shared': 1.13.13 + '@tanstack/query-core': 5.95.2 transitivePeerDependencies: - '@opentelemetry/api' '@ota-meshi/ast-token-store@0.3.0': {} - '@oxc-parser/binding-android-arm-eabi@0.120.0': + '@oxc-parser/binding-android-arm-eabi@0.121.0': optional: true - '@oxc-parser/binding-android-arm64@0.120.0': + '@oxc-parser/binding-android-arm64@0.121.0': optional: true - '@oxc-parser/binding-darwin-arm64@0.120.0': + '@oxc-parser/binding-darwin-arm64@0.121.0': optional: true - '@oxc-parser/binding-darwin-x64@0.120.0': + '@oxc-parser/binding-darwin-x64@0.121.0': optional: true - '@oxc-parser/binding-freebsd-x64@0.120.0': + '@oxc-parser/binding-freebsd-x64@0.121.0': optional: true - '@oxc-parser/binding-linux-arm-gnueabihf@0.120.0': + '@oxc-parser/binding-linux-arm-gnueabihf@0.121.0': optional: true - '@oxc-parser/binding-linux-arm-musleabihf@0.120.0': + '@oxc-parser/binding-linux-arm-musleabihf@0.121.0': optional: true - '@oxc-parser/binding-linux-arm64-gnu@0.120.0': + '@oxc-parser/binding-linux-arm64-gnu@0.121.0': optional: true - '@oxc-parser/binding-linux-arm64-musl@0.120.0': + '@oxc-parser/binding-linux-arm64-musl@0.121.0': optional: true - '@oxc-parser/binding-linux-ppc64-gnu@0.120.0': + '@oxc-parser/binding-linux-ppc64-gnu@0.121.0': optional: true - '@oxc-parser/binding-linux-riscv64-gnu@0.120.0': + '@oxc-parser/binding-linux-riscv64-gnu@0.121.0': optional: true - '@oxc-parser/binding-linux-riscv64-musl@0.120.0': + '@oxc-parser/binding-linux-riscv64-musl@0.121.0': optional: true - '@oxc-parser/binding-linux-s390x-gnu@0.120.0': + '@oxc-parser/binding-linux-s390x-gnu@0.121.0': optional: true - '@oxc-parser/binding-linux-x64-gnu@0.120.0': + '@oxc-parser/binding-linux-x64-gnu@0.121.0': optional: true - '@oxc-parser/binding-linux-x64-musl@0.120.0': + '@oxc-parser/binding-linux-x64-musl@0.121.0': optional: true - '@oxc-parser/binding-openharmony-arm64@0.120.0': + '@oxc-parser/binding-openharmony-arm64@0.121.0': optional: true - '@oxc-parser/binding-wasm32-wasi@0.120.0': + '@oxc-parser/binding-wasm32-wasi@0.121.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': dependencies: - '@napi-rs/wasm-runtime': 1.1.1 + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' optional: true - '@oxc-parser/binding-win32-arm64-msvc@0.120.0': + '@oxc-parser/binding-win32-arm64-msvc@0.121.0': optional: true - '@oxc-parser/binding-win32-ia32-msvc@0.120.0': + '@oxc-parser/binding-win32-ia32-msvc@0.121.0': optional: true - '@oxc-parser/binding-win32-x64-msvc@0.120.0': + '@oxc-parser/binding-win32-x64-msvc@0.121.0': optional: true - '@oxc-project/runtime@0.120.0': {} + '@oxc-project/runtime@0.121.0': {} - '@oxc-project/types@0.120.0': {} + '@oxc-project/types@0.121.0': {} + + '@oxc-project/types@0.122.0': {} '@oxc-resolver/binding-android-arm-eabi@11.19.1': optional: true @@ -9521,9 +10688,12 @@ snapshots: '@oxc-resolver/binding-openharmony-arm64@11.19.1': optional: true - '@oxc-resolver/binding-wasm32-wasi@11.19.1': + '@oxc-resolver/binding-wasm32-wasi@11.19.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': dependencies: - '@napi-rs/wasm-runtime': 1.1.1 + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' optional: true '@oxc-resolver/binding-win32-arm64-msvc@11.19.1': @@ -9535,136 +10705,136 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.19.1': optional: true - '@oxfmt/binding-android-arm-eabi@0.41.0': + '@oxfmt/binding-android-arm-eabi@0.42.0': optional: true - '@oxfmt/binding-android-arm64@0.41.0': + '@oxfmt/binding-android-arm64@0.42.0': optional: true - '@oxfmt/binding-darwin-arm64@0.41.0': + '@oxfmt/binding-darwin-arm64@0.42.0': optional: true - '@oxfmt/binding-darwin-x64@0.41.0': + '@oxfmt/binding-darwin-x64@0.42.0': optional: true - '@oxfmt/binding-freebsd-x64@0.41.0': + '@oxfmt/binding-freebsd-x64@0.42.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.41.0': + '@oxfmt/binding-linux-arm-gnueabihf@0.42.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.41.0': + '@oxfmt/binding-linux-arm-musleabihf@0.42.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.41.0': + '@oxfmt/binding-linux-arm64-gnu@0.42.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.41.0': + '@oxfmt/binding-linux-arm64-musl@0.42.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.41.0': + '@oxfmt/binding-linux-ppc64-gnu@0.42.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.41.0': + '@oxfmt/binding-linux-riscv64-gnu@0.42.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.41.0': + '@oxfmt/binding-linux-riscv64-musl@0.42.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.41.0': + '@oxfmt/binding-linux-s390x-gnu@0.42.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.41.0': + '@oxfmt/binding-linux-x64-gnu@0.42.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.41.0': + '@oxfmt/binding-linux-x64-musl@0.42.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.41.0': + '@oxfmt/binding-openharmony-arm64@0.42.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.41.0': + '@oxfmt/binding-win32-arm64-msvc@0.42.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.41.0': + '@oxfmt/binding-win32-ia32-msvc@0.42.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.41.0': + '@oxfmt/binding-win32-x64-msvc@0.42.0': optional: true - '@oxlint-tsgolint/darwin-arm64@0.17.1': + '@oxlint-tsgolint/darwin-arm64@0.17.3': optional: true - '@oxlint-tsgolint/darwin-x64@0.17.1': + '@oxlint-tsgolint/darwin-x64@0.17.3': optional: true - '@oxlint-tsgolint/linux-arm64@0.17.1': + '@oxlint-tsgolint/linux-arm64@0.17.3': optional: true - '@oxlint-tsgolint/linux-x64@0.17.1': + '@oxlint-tsgolint/linux-x64@0.17.3': optional: true - '@oxlint-tsgolint/win32-arm64@0.17.1': + '@oxlint-tsgolint/win32-arm64@0.17.3': optional: true - '@oxlint-tsgolint/win32-x64@0.17.1': + '@oxlint-tsgolint/win32-x64@0.17.3': optional: true - '@oxlint/binding-android-arm-eabi@1.56.0': + '@oxlint/binding-android-arm-eabi@1.57.0': optional: true - '@oxlint/binding-android-arm64@1.56.0': + '@oxlint/binding-android-arm64@1.57.0': optional: true - '@oxlint/binding-darwin-arm64@1.56.0': + '@oxlint/binding-darwin-arm64@1.57.0': optional: true - '@oxlint/binding-darwin-x64@1.56.0': + '@oxlint/binding-darwin-x64@1.57.0': optional: true - '@oxlint/binding-freebsd-x64@1.56.0': + '@oxlint/binding-freebsd-x64@1.57.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.56.0': + '@oxlint/binding-linux-arm-gnueabihf@1.57.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.56.0': + '@oxlint/binding-linux-arm-musleabihf@1.57.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.56.0': + '@oxlint/binding-linux-arm64-gnu@1.57.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.56.0': + '@oxlint/binding-linux-arm64-musl@1.57.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.56.0': + '@oxlint/binding-linux-ppc64-gnu@1.57.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.56.0': + '@oxlint/binding-linux-riscv64-gnu@1.57.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.56.0': + '@oxlint/binding-linux-riscv64-musl@1.57.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.56.0': + '@oxlint/binding-linux-s390x-gnu@1.57.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.56.0': + '@oxlint/binding-linux-x64-gnu@1.57.0': optional: true - '@oxlint/binding-linux-x64-musl@1.56.0': + '@oxlint/binding-linux-x64-musl@1.57.0': optional: true - '@oxlint/binding-openharmony-arm64@1.56.0': + '@oxlint/binding-openharmony-arm64@1.57.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.56.0': + '@oxlint/binding-win32-arm64-msvc@1.57.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.56.0': + '@oxlint/binding-win32-ia32-msvc@1.57.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.56.0': + '@oxlint/binding-win32-x64-msvc@1.57.0': optional: true '@parcel/watcher-android-arm64@2.5.6': @@ -9730,6 +10900,10 @@ snapshots: '@pkgr/core@0.2.9': {} + '@playwright/test@1.58.2': + dependencies: + playwright: 1.58.2 + '@polka/url@1.0.0-next.29': {} '@preact/signals-core@1.14.0': {} @@ -9902,7 +11076,7 @@ snapshots: '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.19 + '@swc/helpers': 0.5.20 clsx: 2.1.1 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -9913,13 +11087,13 @@ snapshots: '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-stately/flags': 3.1.2 '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.19 + '@swc/helpers': 0.5.20 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) '@react-aria/ssr@3.9.10(react@19.2.4)': dependencies: - '@swc/helpers': 0.5.19 + '@swc/helpers': 0.5.20 react: 19.2.4 '@react-aria/utils@3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': @@ -9928,18 +11102,18 @@ snapshots: '@react-stately/flags': 3.1.2 '@react-stately/utils': 3.11.0(react@19.2.4) '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.19 + '@swc/helpers': 0.5.20 clsx: 2.1.1 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) '@react-stately/flags@3.1.2': dependencies: - '@swc/helpers': 0.5.19 + '@swc/helpers': 0.5.20 '@react-stately/utils@3.11.0(react@19.2.4)': dependencies: - '@swc/helpers': 0.5.19 + '@swc/helpers': 0.5.20 react: 19.2.4 '@react-types/shared@3.33.1(react@19.2.4)': @@ -10032,6 +11206,58 @@ snapshots: '@rgrove/parse-xml@4.2.0': {} + '@rolldown/binding-android-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.12': {} + '@rolldown/pluginutils@1.0.0-rc.5': {} '@rolldown/pluginutils@1.0.0-rc.7': {} @@ -10126,38 +11352,38 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.59.0': optional: true - '@sentry-internal/browser-utils@10.45.0': + '@sentry-internal/browser-utils@10.46.0': dependencies: - '@sentry/core': 10.45.0 + '@sentry/core': 10.46.0 - '@sentry-internal/feedback@10.45.0': + '@sentry-internal/feedback@10.46.0': dependencies: - '@sentry/core': 10.45.0 + '@sentry/core': 10.46.0 - '@sentry-internal/replay-canvas@10.45.0': + '@sentry-internal/replay-canvas@10.46.0': dependencies: - '@sentry-internal/replay': 10.45.0 - '@sentry/core': 10.45.0 + '@sentry-internal/replay': 10.46.0 + '@sentry/core': 10.46.0 - '@sentry-internal/replay@10.45.0': + '@sentry-internal/replay@10.46.0': dependencies: - '@sentry-internal/browser-utils': 10.45.0 - '@sentry/core': 10.45.0 + '@sentry-internal/browser-utils': 10.46.0 + '@sentry/core': 10.46.0 - '@sentry/browser@10.45.0': + '@sentry/browser@10.46.0': dependencies: - '@sentry-internal/browser-utils': 10.45.0 - '@sentry-internal/feedback': 10.45.0 - '@sentry-internal/replay': 10.45.0 - '@sentry-internal/replay-canvas': 10.45.0 - '@sentry/core': 10.45.0 + '@sentry-internal/browser-utils': 10.46.0 + '@sentry-internal/feedback': 10.46.0 + '@sentry-internal/replay': 10.46.0 + '@sentry-internal/replay-canvas': 10.46.0 + '@sentry/core': 10.46.0 - '@sentry/core@10.45.0': {} + '@sentry/core@10.46.0': {} - '@sentry/react@10.45.0(react@19.2.4)': + '@sentry/react@10.46.0(react@19.2.4)': dependencies: - '@sentry/browser': 10.45.0 - '@sentry/core': 10.45.0 + '@sentry/browser': 10.46.0 + '@sentry/core': 10.46.0 react: 19.2.4 '@shuding/opentype.js@1.4.0-beta.0': @@ -10205,15 +11431,15 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/addon-docs@10.3.1(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': + '@storybook/addon-docs@10.3.3(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.4) - '@storybook/csf-plugin': 10.3.1(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + '@storybook/csf-plugin': 10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) '@storybook/icons': 2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@storybook/react-dom-shim': 10.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + '@storybook/react-dom-shim': 10.3.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - storybook: 10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' @@ -10222,41 +11448,41 @@ snapshots: - vite - webpack - '@storybook/addon-links@10.3.1(react@19.2.4)(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + '@storybook/addon-links@10.3.3(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: '@storybook/global': 5.0.0 - storybook: 10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) optionalDependencies: react: 19.2.4 - '@storybook/addon-onboarding@10.3.1(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + '@storybook/addon-onboarding@10.3.3(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: - storybook: 10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@storybook/addon-themes@10.3.1(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + '@storybook/addon-themes@10.3.3(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: - storybook: 10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ts-dedent: 2.2.0 - '@storybook/builder-vite@10.3.1(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': + '@storybook/builder-vite@10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': dependencies: - '@storybook/csf-plugin': 10.3.1(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) - storybook: 10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@storybook/csf-plugin': 10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ts-dedent: 2.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' transitivePeerDependencies: - esbuild - rollup - webpack - '@storybook/csf-plugin@10.3.1(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': + '@storybook/csf-plugin@10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': dependencies: - storybook: 10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) unplugin: 2.3.11 optionalDependencies: esbuild: 0.27.2 rollup: 4.59.0 - vite: '@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' webpack: 5.105.4(esbuild@0.27.2)(uglify-js@3.19.3) '@storybook/global@5.0.0': {} @@ -10266,18 +11492,18 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@storybook/nextjs-vite@10.3.1(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(next@16.2.1(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': + '@storybook/nextjs-vite@10.3.3(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': dependencies: - '@storybook/builder-vite': 10.3.1(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) - '@storybook/react': 10.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) - '@storybook/react-vite': 10.3.1(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) - next: 16.2.1(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) + '@storybook/builder-vite': 10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + '@storybook/react': 10.3.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + '@storybook/react-vite': 10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + next: 16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - storybook: 10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.4) - vite: '@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' - vite-plugin-storybook-nextjs: 3.2.3(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(next@16.2.1(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite-plugin-storybook-nextjs: 3.2.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -10288,27 +11514,27 @@ snapshots: - supports-color - webpack - '@storybook/react-dom-shim@10.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + '@storybook/react-dom-shim@10.3.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - storybook: 10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@storybook/react-vite@10.3.1(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': + '@storybook/react-vite@10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.4(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3) '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - '@storybook/builder-vite': 10.3.1(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) - '@storybook/react': 10.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + '@storybook/builder-vite': 10.3.3(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) + '@storybook/react': 10.3.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) empathic: 2.0.0 magic-string: 0.30.21 react: 19.2.4 react-docgen: 8.0.3 react-dom: 19.2.4(react@19.2.4) resolve: 1.22.11 - storybook: 10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tsconfig-paths: 4.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' transitivePeerDependencies: - esbuild - rollup @@ -10316,15 +11542,15 @@ snapshots: - typescript - webpack - '@storybook/react@10.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)': + '@storybook/react@10.3.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 10.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + '@storybook/react-dom-shim': 10.3.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) react: 19.2.4 react-docgen: 8.0.3 react-docgen-typescript: 2.4.0(typescript@5.9.3) react-dom: 19.2.4(react@19.2.4) - storybook: 10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -10332,7 +11558,7 @@ snapshots: '@streamdown/math@1.0.2(react@19.2.4)': dependencies: - katex: 0.16.40 + katex: 0.16.44 react: 19.2.4 rehype-katex: 7.0.1 remark-math: 6.0.0 @@ -10342,7 +11568,7 @@ snapshots: '@stylistic/eslint-plugin@5.10.0(eslint@10.1.0(jiti@1.21.7))': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@1.21.7)) - '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/types': 8.57.2 eslint: 10.1.0(jiti@1.21.7) eslint-visitor-keys: 4.2.1 espree: 10.4.0 @@ -10355,22 +11581,22 @@ snapshots: dependencies: tslib: 2.8.1 - '@swc/helpers@0.5.19': + '@swc/helpers@0.5.20': dependencies: tslib: 2.8.1 - '@t3-oss/env-core@0.13.11(typescript@5.9.3)(valibot@1.3.0(typescript@5.9.3))(zod@4.3.6)': + '@t3-oss/env-core@0.13.11(typescript@5.9.3)(valibot@1.3.1(typescript@5.9.3))(zod@4.3.6)': optionalDependencies: typescript: 5.9.3 - valibot: 1.3.0(typescript@5.9.3) + valibot: 1.3.1(typescript@5.9.3) zod: 4.3.6 - '@t3-oss/env-nextjs@0.13.11(typescript@5.9.3)(valibot@1.3.0(typescript@5.9.3))(zod@4.3.6)': + '@t3-oss/env-nextjs@0.13.11(typescript@5.9.3)(valibot@1.3.1(typescript@5.9.3))(zod@4.3.6)': dependencies: - '@t3-oss/env-core': 0.13.11(typescript@5.9.3)(valibot@1.3.0(typescript@5.9.3))(zod@4.3.6) + '@t3-oss/env-core': 0.13.11(typescript@5.9.3)(valibot@1.3.1(typescript@5.9.3))(zod@4.3.6) optionalDependencies: typescript: 5.9.3 - valibot: 1.3.0(typescript@5.9.3) + valibot: 1.3.1(typescript@5.9.3) zod: 4.3.6 '@tailwindcss/typography@0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))': @@ -10422,9 +11648,9 @@ snapshots: - csstype - utf-8-validate - '@tanstack/eslint-plugin-query@5.95.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': + '@tanstack/eslint-plugin-query@5.95.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) eslint: 10.1.0(jiti@1.21.7) optionalDependencies: typescript: 5.9.3 @@ -10435,7 +11661,7 @@ snapshots: dependencies: '@tanstack/devtools-event-client': 0.4.3 '@tanstack/pacer-lite': 0.1.1 - '@tanstack/store': 0.9.2 + '@tanstack/store': 0.9.3 '@tanstack/form-devtools@0.2.19(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11)': dependencies: @@ -10455,9 +11681,9 @@ snapshots: '@tanstack/pacer-lite@0.1.1': {} - '@tanstack/query-core@5.95.0': {} + '@tanstack/query-core@5.95.2': {} - '@tanstack/query-devtools@5.95.0': {} + '@tanstack/query-devtools@5.95.2': {} '@tanstack/react-devtools@0.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11)': dependencies: @@ -10487,25 +11713,25 @@ snapshots: '@tanstack/react-form@1.28.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@tanstack/form-core': 1.28.5 - '@tanstack/react-store': 0.9.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/react-store': 0.9.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 transitivePeerDependencies: - react-dom - '@tanstack/react-query-devtools@5.95.0(@tanstack/react-query@5.95.0(react@19.2.4))(react@19.2.4)': + '@tanstack/react-query-devtools@5.95.2(@tanstack/react-query@5.95.2(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/query-devtools': 5.95.0 - '@tanstack/react-query': 5.95.0(react@19.2.4) + '@tanstack/query-devtools': 5.95.2 + '@tanstack/react-query': 5.95.2(react@19.2.4) react: 19.2.4 - '@tanstack/react-query@5.95.0(react@19.2.4)': + '@tanstack/react-query@5.95.2(react@19.2.4)': dependencies: - '@tanstack/query-core': 5.95.0 + '@tanstack/query-core': 5.95.2 react: 19.2.4 - '@tanstack/react-store@0.9.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-store@0.9.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/store': 0.9.2 + '@tanstack/store': 0.9.3 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) use-sync-external-store: 1.6.0(react@19.2.4) @@ -10516,10 +11742,12 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@tanstack/store@0.9.2': {} + '@tanstack/store@0.9.3': {} '@tanstack/virtual-core@3.13.23': {} + '@teppeis/multimaps@3.0.0': {} + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.29.0 @@ -10571,7 +11799,7 @@ snapshots: '@tsslint/compat-eslint@3.0.2(jiti@1.21.7)(typescript@5.9.3)': dependencies: '@tsslint/types': 3.0.2 - '@typescript-eslint/parser': 8.57.1(eslint@9.27.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.2(eslint@9.27.0(jiti@1.21.7))(typescript@5.9.3) eslint: 9.27.0(jiti@1.21.7) transitivePeerDependencies: - jiti @@ -10582,7 +11810,7 @@ snapshots: dependencies: '@tsslint/types': 3.0.2 minimatch: 10.2.4 - ts-api-utils: 2.4.0(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@5.9.3) optionalDependencies: '@tsslint/compat-eslint': 3.0.2(jiti@1.21.7)(typescript@5.9.3) transitivePeerDependencies: @@ -10747,7 +11975,7 @@ snapshots: '@types/d3-transition': 3.0.9 '@types/d3-zoom': 3.0.8 - '@types/debug@4.1.12': + '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 @@ -10805,6 +12033,8 @@ snapshots: dependencies: undici-types: 7.18.2 + '@types/normalize-package-data@2.4.4': {} + '@types/papaparse@5.5.2': dependencies: '@types/node': 25.5.0 @@ -10855,60 +12085,88 @@ snapshots: '@types/zen-observable@0.8.3': {} - '@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/type-utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.1 + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.2 eslint: 10.1.0(jiti@1.21.7) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.4.0(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.1 - debug: 4.4.3 + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.2 + eslint: 10.1.0(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.2 + debug: 4.4.3(supports-color@8.1.1) eslint: 10.1.0(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.57.1(eslint@9.27.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.1 - debug: 4.4.3 + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.2 + debug: 4.4.3(supports-color@8.1.1) + eslint: 10.1.0(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.57.2(eslint@9.27.0(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.2 + debug: 4.4.3(supports-color@8.1.1) eslint: 9.27.0(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.57.1(typescript@5.9.3)': + '@typescript-eslint/project-service@8.57.2(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3) - '@typescript-eslint/types': 8.57.1 - debug: 4.4.3 + '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3) + '@typescript-eslint/types': 8.57.2 + debug: 4.4.3(supports-color@8.1.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/rule-tester@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@typescript-eslint/parser': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) ajv: 6.14.0 eslint: 10.1.0(jiti@1.21.7) json-stable-stringify-without-jsonify: 1.0.1 @@ -10918,90 +12176,113 @@ snapshots: - supports-color - typescript - '@typescript-eslint/scope-manager@8.57.1': + '@typescript-eslint/scope-manager@8.57.2': dependencies: - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/visitor-keys': 8.57.1 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/visitor-keys': 8.57.2 - '@typescript-eslint/tsconfig-utils@8.57.1(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.57.2(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - debug: 4.4.3 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + debug: 4.4.3(supports-color@8.1.1) eslint: 10.1.0(jiti@1.21.7) - ts-api-utils: 2.4.0(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.57.1': {} - - '@typescript-eslint/typescript-estree@8.57.1(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.57.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3) - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/visitor-keys': 8.57.1 - debug: 4.4.3 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3(supports-color@8.1.1) + eslint: 10.1.0(jiti@2.6.1) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.57.2': {} + + '@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.57.2(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3) + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/visitor-keys': 8.57.2 + debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.4 semver: 7.7.4 tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@1.21.7)) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) eslint: 10.1.0(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.57.1': + '@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.57.1 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.57.2': + dependencies: + '@typescript-eslint/types': 8.57.2 eslint-visitor-keys: 5.0.1 - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260322.1': + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260329.1': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260322.1': + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260329.1': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260322.1': + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260329.1': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20260322.1': + '@typescript/native-preview-linux-arm@7.0.0-dev.20260329.1': optional: true - '@typescript/native-preview-linux-x64@7.0.0-dev.20260322.1': + '@typescript/native-preview-linux-x64@7.0.0-dev.20260329.1': optional: true - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260322.1': + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260329.1': optional: true - '@typescript/native-preview-win32-x64@7.0.0-dev.20260322.1': + '@typescript/native-preview-win32-x64@7.0.0-dev.20260329.1': optional: true - '@typescript/native-preview@7.0.0-dev.20260322.1': + '@typescript/native-preview@7.0.0-dev.20260329.1': optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260322.1 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260322.1 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260322.1 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260322.1 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260322.1 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260322.1 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260322.1 + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260329.1 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260329.1 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20260329.1 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260329.1 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20260329.1 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260329.1 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20260329.1 '@ungap/structured-clone@1.3.0': {} @@ -11009,34 +12290,56 @@ snapshots: dependencies: unpic: 4.2.2 - '@unpic/react@1.0.2(next@16.2.1(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@unpic/react@1.0.2(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@unpic/core': 1.0.3 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) optionalDependencies: - next: 16.2.1(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) + next: 16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) '@upsetjs/venn.js@2.0.0': optionalDependencies: d3-selection: 3.0.0 d3-transition: 3.0.1(d3-selection@3.0.0) - '@valibot/to-json-schema@1.6.0(valibot@1.3.0(typescript@5.9.3))': + '@valibot/to-json-schema@1.6.0(valibot@1.3.1(typescript@5.9.3))': dependencies: - valibot: 1.3.0(typescript@5.9.3) + valibot: 1.3.1(typescript@5.9.3) '@vercel/og@0.8.6': dependencies: '@resvg/resvg-wasm': 2.4.0 satori: 0.16.0 - '@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))': + '@vitejs/devtools-kit@0.1.11(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)(ws@8.20.0)': + dependencies: + '@vitejs/devtools-rpc': 0.1.11(typescript@5.9.3)(ws@8.20.0) + birpc: 4.0.0 + ohash: 2.0.11 + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + transitivePeerDependencies: + - typescript + - ws + + '@vitejs/devtools-rpc@0.1.11(typescript@5.9.3)(ws@8.20.0)': + dependencies: + birpc: 4.0.0 + ohash: 2.0.11 + p-limit: 7.3.0 + structured-clone-es: 2.0.0 + valibot: 1.3.1(typescript@5.9.3) + optionalDependencies: + ws: 8.20.0 + transitivePeerDependencies: + - typescript + + '@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: '@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' - '@vitejs/plugin-rsc@0.5.21(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(react@19.2.4)': + '@vitejs/plugin-rsc@0.5.21(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(react@19.2.4)': dependencies: '@rolldown/pluginutils': 1.0.0-rc.5 es-module-lexer: 2.0.0 @@ -11045,18 +12348,18 @@ snapshots: periscopic: 4.0.2 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - srvx: 0.11.12 + srvx: 0.11.13 strip-literal: 3.1.0 turbo-stream: 3.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' - vitefu: 1.1.2(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vitefu: 1.1.2(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) optionalDependencies: react-server-dom-webpack: 19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) - '@vitest/coverage-v8@4.1.0(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))': + '@vitest/coverage-v8@4.1.2(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.0 + '@vitest/utils': 4.1.2 ast-v8-to-istanbul: 1.0.0 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 @@ -11065,16 +12368,31 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: '@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vitest: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' - '@vitest/eslint-plugin@1.6.12(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': + '@vitest/coverage-v8@4.1.2(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3))': dependencies: - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.2 + ast-v8-to-istanbul: 1.0.0 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.2 + obug: 2.1.1 + std-env: 4.0.0 + tinyrainbow: 3.1.0 + vitest: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)' + + '@vitest/eslint-plugin@1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) eslint: 10.1.0(jiti@1.21.7) optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) typescript: 5.9.3 - vitest: '@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vitest: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' transitivePeerDependencies: - supports-color @@ -11090,7 +12408,7 @@ snapshots: dependencies: tinyrainbow: 2.0.0 - '@vitest/pretty-format@4.1.0': + '@vitest/pretty-format@4.1.2': dependencies: tinyrainbow: 3.1.0 @@ -11104,16 +12422,16 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@vitest/utils@4.1.0': + '@vitest/utils@4.1.2': dependencies: - '@vitest/pretty-format': 4.1.0 + '@vitest/pretty-format': 4.1.2 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': + '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': dependencies: - '@oxc-project/runtime': 0.120.0 - '@oxc-project/types': 0.120.0 + '@oxc-project/runtime': 0.121.0 + '@oxc-project/types': 0.122.0 lightningcss: 1.32.0 postcss: 8.5.8 optionalDependencies: @@ -11127,23 +12445,46 @@ snapshots: typescript: 5.9.3 yaml: 2.8.3 - '@voidzero-dev/vite-plus-darwin-arm64@0.1.13': + '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': + dependencies: + '@oxc-project/runtime': 0.121.0 + '@oxc-project/types': 0.122.0 + lightningcss: 1.32.0 + postcss: 8.5.8 + optionalDependencies: + '@types/node': 25.5.0 + esbuild: 0.27.2 + fsevents: 2.3.3 + jiti: 2.6.1 + sass: 1.98.0 + terser: 5.46.1 + tsx: 4.21.0 + typescript: 5.9.3 + yaml: 2.8.3 + + '@voidzero-dev/vite-plus-darwin-arm64@0.1.14': optional: true - '@voidzero-dev/vite-plus-darwin-x64@0.1.13': + '@voidzero-dev/vite-plus-darwin-x64@0.1.14': optional: true - '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.13': + '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.14': optional: true - '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.13': + '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.14': optional: true - '@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': + '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.14': + optional: true + + '@voidzero-dev/vite-plus-linux-x64-musl@0.1.14': + optional: true + + '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@voidzero-dev/vite-plus-core': 0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + '@voidzero-dev/vite-plus-core': 0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) es-module-lexer: 1.7.0 obug: 2.1.1 pixelmatch: 7.1.0 @@ -11153,12 +12494,11 @@ snapshots: tinybench: 2.9.0 tinyexec: 1.0.4 tinyglobby: 0.2.15 - vite: '@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' - ws: 8.19.0 + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + ws: 8.20.0 optionalDependencies: '@types/node': 25.5.0 happy-dom: 20.8.9 - jsdom: 29.0.1(canvas@3.2.2) transitivePeerDependencies: - '@arethetypeswrong/core' - '@tsdown/css' @@ -11180,10 +12520,50 @@ snapshots: - utf-8-validate - yaml - '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.13': + '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@voidzero-dev/vite-plus-core': 0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + es-module-lexer: 1.7.0 + obug: 2.1.1 + pixelmatch: 7.1.0 + pngjs: 7.0.0 + sirv: 3.0.2 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.4 + tinyglobby: 0.2.15 + vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + ws: 8.20.0 + optionalDependencies: + '@types/node': 25.5.0 + happy-dom: 20.8.9 + transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@tsdown/css' + - '@tsdown/exe' + - '@vitejs/devtools' + - bufferutil + - esbuild + - jiti + - less + - publint + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - typescript + - unplugin-unused + - utf-8-validate + - yaml + + '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.14': optional: true - '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.13': + '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.14': optional: true '@volar/language-core@2.4.28': @@ -11200,37 +12580,37 @@ snapshots: path-browserify: 1.0.1 vscode-uri: 3.1.0 - '@vue/compiler-core@3.5.30': + '@vue/compiler-core@3.5.31': dependencies: '@babel/parser': 7.29.2 - '@vue/shared': 3.5.30 + '@vue/shared': 3.5.31 entities: 7.0.1 estree-walker: 2.0.2 source-map-js: 1.2.1 - '@vue/compiler-dom@3.5.30': + '@vue/compiler-dom@3.5.31': dependencies: - '@vue/compiler-core': 3.5.30 - '@vue/shared': 3.5.30 + '@vue/compiler-core': 3.5.31 + '@vue/shared': 3.5.31 - '@vue/compiler-sfc@3.5.30': + '@vue/compiler-sfc@3.5.31': dependencies: '@babel/parser': 7.29.2 - '@vue/compiler-core': 3.5.30 - '@vue/compiler-dom': 3.5.30 - '@vue/compiler-ssr': 3.5.30 - '@vue/shared': 3.5.30 + '@vue/compiler-core': 3.5.31 + '@vue/compiler-dom': 3.5.31 + '@vue/compiler-ssr': 3.5.31 + '@vue/shared': 3.5.31 estree-walker: 2.0.2 magic-string: 0.30.21 postcss: 8.5.8 source-map-js: 1.2.1 - '@vue/compiler-ssr@3.5.30': + '@vue/compiler-ssr@3.5.31': dependencies: - '@vue/compiler-dom': 3.5.30 - '@vue/shared': 3.5.30 + '@vue/compiler-dom': 3.5.31 + '@vue/shared': 3.5.31 - '@vue/shared@3.5.30': {} + '@vue/shared@3.5.31': {} '@webassemblyjs/ast@1.14.1': dependencies: @@ -11330,12 +12710,12 @@ snapshots: acorn@8.16.0: {} - agentation@2.3.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + agentation@3.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): optionalDependencies: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - ahooks@3.9.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + ahooks@3.9.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@babel/runtime': 7.29.2 '@types/js-cookie': 3.0.6 @@ -11377,6 +12757,8 @@ snapshots: dependencies: environment: 1.1.0 + ansi-regex@4.1.1: {} + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -11414,6 +12796,12 @@ snapshots: aria-query@5.3.2: {} + assertion-error-formatter@3.0.0: + dependencies: + diff: 4.0.4 + pad-right: 0.2.2 + repeat-string: 1.6.1 + assertion-error@2.0.1: {} ast-types@0.16.1: @@ -11430,15 +12818,25 @@ snapshots: async@3.2.6: {} + asynckit@0.4.0: {} + autoprefixer@10.4.27(postcss@8.5.8): dependencies: browserslist: 4.28.1 - caniuse-lite: 1.0.30001780 + caniuse-lite: 1.0.30001781 fraction.js: 5.3.4 picocolors: 1.1.1 postcss: 8.5.8 postcss-value-parser: 4.2.0 + axios@1.14.0: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + bail@2.0.2: {} balanced-match@1.0.2: {} @@ -11452,18 +12850,13 @@ snapshots: base64-js@1.5.1: optional: true - baseline-browser-mapping@2.10.8: {} - - bidi-js@1.0.3: - dependencies: - require-from-string: 2.0.2 - optional: true + baseline-browser-mapping@2.10.12: {} binary-extensions@2.3.0: {} birecord@0.1.1: {} - birpc@2.9.0: {} + birpc@4.0.0: {} bl@4.1.0: dependencies: @@ -11478,7 +12871,7 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.4: + brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -11488,9 +12881,9 @@ snapshots: browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.10.8 - caniuse-lite: 1.0.30001780 - electron-to-chromium: 1.5.313 + baseline-browser-mapping: 2.10.12 + caniuse-lite: 1.0.30001781 + electron-to-chromium: 1.5.328 node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) @@ -11512,17 +12905,29 @@ snapshots: dependencies: run-applescript: 7.1.0 + bundle-require@5.1.0(esbuild@0.27.2): + dependencies: + esbuild: 0.27.2 + load-tsconfig: 0.2.5 + bytes@3.1.2: {} + cac@6.7.14: {} + cac@7.0.0: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + callsites@3.1.0: {} camelcase-css@2.0.1: {} camelize@1.0.1: {} - caniuse-lite@1.0.30001780: {} + caniuse-lite@1.0.30001781: {} canvas@3.2.2: dependencies: @@ -11530,6 +12935,12 @@ snapshots: prebuild-install: 7.1.3 optional: true + capital-case@1.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + upper-case-first: 2.0.2 + ccount@2.0.1: {} chai@5.3.3: @@ -11632,6 +13043,8 @@ snapshots: ci-info@4.4.0: {} + class-transformer@0.5.1: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -11648,6 +13061,12 @@ snapshots: dependencies: restore-cursor: 5.1.0 + cli-table3@0.6.5: + dependencies: + string-width: 8.2.0 + optionalDependencies: + '@colors/colors': 1.5.0 + cli-truncate@5.2.0: dependencies: slice-ansi: 8.0.0 @@ -11691,10 +13110,18 @@ snapshots: colorette@2.0.20: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + comma-separated-tokens@1.0.8: {} comma-separated-tokens@2.0.3: {} + commander@14.0.0: {} + + commander@14.0.2: {} + commander@14.0.3: {} commander@2.20.3: {} @@ -11707,12 +13134,16 @@ snapshots: comment-parser@1.4.5: {} + comment-parser@1.4.6: {} + compare-versions@6.1.1: {} confbox@0.1.8: {} confbox@0.2.4: {} + consola@3.4.2: {} + convert-source-map@2.0.0: {} copy-to-clipboard@3.3.3: @@ -11773,12 +13204,6 @@ snapshots: mdn-data: 2.0.30 source-map-js: 1.2.1 - css-tree@3.2.1: - dependencies: - mdn-data: 2.27.1 - source-map-js: 1.2.1 - optional: true - css-what@6.2.2: {} css.escape@1.5.1: {} @@ -11835,7 +13260,7 @@ snapshots: d3-delaunay@6.0.4: dependencies: - delaunator: 5.0.1 + delaunator: 5.1.0 d3-dispatch@3.0.1: {} @@ -11977,19 +13402,13 @@ snapshots: d3: 7.9.0 lodash-es: 4.17.23 - data-urls@7.0.0: - dependencies: - whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1 - transitivePeerDependencies: - - '@noble/hashes' - optional: true - dayjs@1.11.20: {} - debug@4.4.3: + debug@4.4.3(supports-color@8.1.1): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 decimal.js@10.6.0: {} @@ -12020,9 +13439,11 @@ snapshots: defu@6.1.4: {} - delaunator@5.0.1: + delaunator@5.1.0: dependencies: - robust-predicates: 3.0.2 + robust-predicates: 3.0.3 + + delayed-stream@1.0.0: {} dequal@2.0.3: {} @@ -12040,6 +13461,8 @@ snapshots: diff-sequences@29.6.3: {} + diff@4.0.4: {} + dlv@1.1.3: {} doctrine@3.0.0: @@ -12078,6 +13501,12 @@ snapshots: dotenv@16.6.1: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + echarts-for-react@3.0.6(echarts@6.0.0)(react@19.2.4): dependencies: echarts: 6.0.0 @@ -12090,7 +13519,7 @@ snapshots: tslib: 2.3.0 zrender: 6.0.0 - electron-to-chromium@1.5.313: {} + electron-to-chromium@1.5.328: {} elkjs@0.11.1: {} @@ -12128,7 +13557,7 @@ snapshots: enhanced-resolve@5.20.1: dependencies: graceful-fs: 4.2.11 - tapable: 2.3.0 + tapable: 2.3.2 entities@4.5.0: {} @@ -12140,10 +13569,29 @@ snapshots: error-stack-parser-es@1.0.5: {} + error-stack-parser@2.1.4: + dependencies: + stackframe: 1.3.4 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} es-module-lexer@2.0.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: '@nolyfill/hasown@1.0.44' + es-toolkit@1.45.1: {} esast-util-from-estree@2.0.0: @@ -12204,7 +13652,7 @@ snapshots: eslint: 10.1.0(jiti@1.21.7) semver: 7.7.4 - eslint-config-flat-gitignore@2.2.1(eslint@10.1.0(jiti@1.21.7)): + eslint-config-flat-gitignore@2.3.0(eslint@10.1.0(jiti@1.21.7)): dependencies: '@eslint/compat': 2.0.3(eslint@10.1.0(jiti@1.21.7)) eslint: 10.1.0(jiti@1.21.7) @@ -12238,30 +13686,30 @@ snapshots: dependencies: eslint: 10.1.0(jiti@1.21.7) - eslint-plugin-better-tailwindcss@4.3.2(eslint@10.1.0(jiti@1.21.7))(oxlint@1.56.0(oxlint-tsgolint@0.17.1))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))(typescript@5.9.3): + eslint-plugin-better-tailwindcss@4.3.2(eslint@10.1.0(jiti@1.21.7))(oxlint@1.57.0(oxlint-tsgolint@0.17.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))(typescript@5.9.3): dependencies: '@eslint/css-tree': 3.6.9 - '@valibot/to-json-schema': 1.6.0(valibot@1.3.0(typescript@5.9.3)) + '@valibot/to-json-schema': 1.6.0(valibot@1.3.1(typescript@5.9.3)) enhanced-resolve: 5.20.1 jiti: 2.6.1 synckit: 0.11.12 tailwind-csstree: 0.1.5 tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.3) tsconfig-paths-webpack-plugin: 4.2.0 - valibot: 1.3.0(typescript@5.9.3) + valibot: 1.3.1(typescript@5.9.3) optionalDependencies: eslint: 10.1.0(jiti@1.21.7) - oxlint: 1.56.0(oxlint-tsgolint@0.17.1) + oxlint: 1.57.0(oxlint-tsgolint@0.17.3) transitivePeerDependencies: - '@eslint/css' - typescript - eslint-plugin-command@3.5.2(@typescript-eslint/rule-tester@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.1(typescript@5.9.3))(@typescript-eslint/utils@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7)): + eslint-plugin-command@3.5.2(@typescript-eslint/rule-tester@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3))(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7)): dependencies: '@es-joy/jsdoccomment': 0.84.0 - '@typescript-eslint/rule-tester': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/rule-tester': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) eslint: 10.1.0(jiti@1.21.7) eslint-plugin-depend@1.5.0(eslint@10.1.0(jiti@1.21.7)): @@ -12286,13 +13734,13 @@ snapshots: dependencies: eslint: 10.1.0(jiti@1.21.7) - eslint-plugin-jsdoc@62.8.0(eslint@10.1.0(jiti@1.21.7)): + eslint-plugin-jsdoc@62.8.1(eslint@10.1.0(jiti@1.21.7)): dependencies: '@es-joy/jsdoccomment': 0.84.0 '@es-joy/resolve.exports': 1.2.0 are-docs-informative: 0.0.2 comment-parser: 1.4.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint: 10.1.0(jiti@1.21.7) espree: 11.2.0 @@ -12358,7 +13806,7 @@ snapshots: eslint-plugin-no-barrel-files@1.2.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) transitivePeerDependencies: - eslint - supports-color @@ -12368,7 +13816,7 @@ snapshots: eslint-plugin-perfectionist@5.7.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) eslint: 10.1.0(jiti@1.21.7) natural-orderby: 5.0.0 transitivePeerDependencies: @@ -12392,9 +13840,9 @@ snapshots: '@eslint-react/core': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) compare-versions: 6.1.1 eslint: 10.1.0(jiti@1.21.7) ts-pattern: 5.9.0 @@ -12419,10 +13867,10 @@ snapshots: '@eslint-react/core': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/type-utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) compare-versions: 6.1.1 eslint: 10.1.0(jiti@1.21.7) string-ts: 2.3.1 @@ -12440,10 +13888,10 @@ snapshots: '@eslint-react/ast': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/type-utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) eslint: 10.1.0(jiti@1.21.7) ts-pattern: 5.9.0 typescript: 5.9.3 @@ -12456,9 +13904,9 @@ snapshots: '@eslint-react/core': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) birecord: 0.1.1 eslint: 10.1.0(jiti@1.21.7) ts-pattern: 5.9.0 @@ -12472,14 +13920,14 @@ snapshots: '@eslint-react/core': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) '@eslint-react/shared': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) '@eslint-react/var': 3.0.0(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/type-utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) compare-versions: 6.1.1 eslint: 10.1.0(jiti@1.21.7) string-ts: 2.3.1 - ts-api-utils: 2.4.0(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@5.9.3) ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: @@ -12489,7 +13937,7 @@ snapshots: dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@1.21.7)) '@eslint-community/regexpp': 4.12.2 - comment-parser: 1.4.5 + comment-parser: 1.4.6 eslint: 10.1.0(jiti@1.21.7) jsdoc-type-pratt-parser: 7.1.1 refa: 0.12.1 @@ -12509,14 +13957,14 @@ snapshots: minimatch: 10.2.4 scslre: 0.3.0 semver: 7.7.4 - ts-api-utils: 2.4.0(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 - eslint-plugin-storybook@10.3.1(eslint@10.1.0(jiti@1.21.7))(storybook@10.3.1(@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.3.3(eslint@10.1.0(jiti@1.21.7))(storybook@10.3.3(@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.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) eslint: 10.1.0(jiti@1.21.7) - storybook: 10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) transitivePeerDependencies: - supports-color - typescript @@ -12526,7 +13974,7 @@ snapshots: '@eslint/core': 1.1.1 '@eslint/plugin-kit': 0.6.1 '@ota-meshi/ast-token-store': 0.3.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 10.1.0(jiti@1.21.7) toml-eslint-parser: 1.0.3 transitivePeerDependencies: @@ -12552,13 +14000,13 @@ snapshots: semver: 7.7.4 strip-indent: 4.1.1 - eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7)): + eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7)): dependencies: eslint: 10.1.0(jiti@1.21.7) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) - eslint-plugin-vue@10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.1.0(jiti@1.21.7)))(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@1.21.7))): + eslint-plugin-vue@10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.1.0(jiti@1.21.7)))(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@1.21.7))): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@1.21.7)) eslint: 10.1.0(jiti@1.21.7) @@ -12570,14 +14018,14 @@ snapshots: xml-name-validator: 4.0.0 optionalDependencies: '@stylistic/eslint-plugin': 5.10.0(eslint@10.1.0(jiti@1.21.7)) - '@typescript-eslint/parser': 8.57.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) eslint-plugin-yml@3.3.1(eslint@10.1.0(jiti@1.21.7)): dependencies: '@eslint/core': 1.1.1 '@eslint/plugin-kit': 0.6.1 '@ota-meshi/ast-token-store': 0.3.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) diff-sequences: 29.6.3 escape-string-regexp: 5.0.0 eslint: 10.1.0(jiti@1.21.7) @@ -12586,9 +14034,9 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.30)(eslint@10.1.0(jiti@1.21.7)): + eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.31)(eslint@10.1.0(jiti@1.21.7)): dependencies: - '@vue/compiler-sfc': 3.5.30 + '@vue/compiler-sfc': 3.5.31 eslint: 10.1.0(jiti@1.21.7) eslint-scope@5.1.1: @@ -12628,7 +14076,7 @@ snapshots: '@types/estree': 1.0.8 ajv: 6.14.0 cross-spawn: 7.0.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 @@ -12651,6 +14099,43 @@ snapshots: transitivePeerDependencies: - supports-color + eslint@10.1.0(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.3 + '@eslint/config-helpers': 0.5.3 + '@eslint/core': 1.1.1 + '@eslint/plugin-kit': 0.6.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.14.0 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@8.1.1) + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.4 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + eslint@9.27.0(jiti@1.21.7): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.27.0(jiti@1.21.7)) @@ -12669,7 +14154,7 @@ snapshots: ajv: 6.14.0 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -12771,7 +14256,7 @@ snapshots: extract-zip@2.0.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) get-stream: 5.2.0 yauzl: 3.2.1 optionalDependencies: @@ -12827,6 +14312,10 @@ snapshots: fflate@0.7.4: {} + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -12844,6 +14333,12 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.2 + rollup: 4.59.0 + flat-cache@4.0.1: dependencies: flatted: 3.4.2 @@ -12851,6 +14346,16 @@ snapshots: flatted@3.4.2: {} + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: '@nolyfill/hasown@1.0.44' + mime-types: 2.1.35 + format@0.2.2: {} formatly@0.3.0: @@ -12871,9 +14376,14 @@ snapshots: fs-constants@1.0.0: optional: true + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + functional-red-black-tree@1.0.1: {} fzf@0.5.2: {} @@ -12882,8 +14392,26 @@ snapshots: get-east-asian-width@1.5.0: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: '@nolyfill/hasown@1.0.44' + math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stream@5.2.0: dependencies: pump: 3.0.4 @@ -12913,6 +14441,10 @@ snapshots: minipass: 7.1.3 path-scurry: 2.0.2 + global-dirs@3.0.1: + dependencies: + ini: 2.0.0 + globals@14.0.0: {} globals@15.15.0: {} @@ -12927,6 +14459,8 @@ snapshots: dependencies: csstype: 3.2.3 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} hachure-fill@0.5.2: {} @@ -12943,8 +14477,18 @@ snapshots: - bufferutil - utf-8-validate + has-ansi@4.0.1: + dependencies: + ansi-regex: 4.1.1 + has-flag@4.0.0: {} + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hast-util-from-dom@5.0.1: dependencies: '@types/hast': 3.0.4 @@ -13100,14 +14644,11 @@ snapshots: highlightjs-vue@1.0.0: {} - hono@4.12.8: {} + hono@4.12.9: {} - html-encoding-sniffer@6.0.0: + hosted-git-info@9.0.2: dependencies: - '@exodus/bytes': 1.15.0 - transitivePeerDependencies: - - '@noble/hashes' - optional: true + lru-cache: 11.2.7 html-entities@2.6.0: {} @@ -13136,7 +14677,7 @@ snapshots: dependencies: '@babel/runtime': 7.29.2 - i18next@25.10.4(typescript@5.9.3): + i18next@25.10.10(typescript@5.9.3): dependencies: '@babel/runtime': 7.29.2 optionalDependencies: @@ -13182,12 +14723,16 @@ snapshots: indent-string@5.0.0: {} + index-to-position@1.2.0: {} + inherits@2.0.4: optional: true ini@1.3.8: optional: true + ini@2.0.0: {} + inline-style-parser@0.2.7: {} internmap@1.0.1: {} @@ -13238,21 +14783,29 @@ snapshots: is-hexadecimal@2.0.1: {} + is-in-ssh@1.0.0: {} + is-inside-container@1.0.0: dependencies: is-docker: 3.0.0 + is-installed-globally@0.4.0: + dependencies: + global-dirs: 3.0.1 + is-path-inside: 3.0.3 + is-number@7.0.0: {} - is-plain-obj@4.1.0: {} + is-path-inside@3.0.3: {} - is-potential-custom-element-name@1.0.1: - optional: true + is-plain-obj@4.1.0: {} is-reference@3.0.3: dependencies: '@types/estree': 1.0.8 + is-stream@2.0.1: {} + is-wsl@3.1.1: dependencies: is-inside-container: 1.0.0 @@ -13284,13 +14837,15 @@ snapshots: jiti@2.6.1: {} - jotai@2.18.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4): + jotai@2.19.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4): optionalDependencies: '@babel/core': 7.29.0 '@babel/template': 7.28.6 '@types/react': 19.2.14 react: 19.2.4 + joycon@3.1.1: {} + js-audio-recorder@1.0.7: {} js-base64@3.7.8: {} @@ -13309,35 +14864,6 @@ snapshots: jsdoc-type-pratt-parser@7.1.1: {} - jsdom@29.0.1(canvas@3.2.2): - dependencies: - '@asamuzakjp/css-color': 5.1.1 - '@asamuzakjp/dom-selector': 7.0.4 - '@bramus/specificity': 2.4.2 - '@csstools/css-syntax-patches-for-csstree': 1.1.2(css-tree@3.2.1) - '@exodus/bytes': 1.15.0 - css-tree: 3.2.1 - data-urls: 7.0.0 - decimal.js: 10.6.0 - html-encoding-sniffer: 6.0.0 - is-potential-custom-element-name: 1.0.1 - lru-cache: 11.2.7 - parse5: 8.0.0 - saxes: 6.0.0 - symbol-tree: 3.2.4 - tough-cookie: 6.0.1 - undici: 7.24.6 - w3c-xmlserializer: 5.0.0 - webidl-conversions: 8.0.1 - whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1 - xml-name-validator: 5.0.0 - optionalDependencies: - canvas: 3.2.2 - transitivePeerDependencies: - - '@noble/hashes' - optional: true - jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -13368,7 +14894,7 @@ snapshots: jsx-ast-utils-x@0.1.0: {} - katex@0.16.40: + katex@0.16.44: dependencies: commander: 8.3.0 @@ -13378,7 +14904,7 @@ snapshots: khroma@2.1.0: {} - knip@6.0.2: + knip@6.1.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): dependencies: '@nodelib/fs.walk': 1.2.8 fast-glob: 3.3.3 @@ -13386,8 +14912,8 @@ snapshots: get-tsconfig: 4.13.7 jiti: 2.6.1 minimist: 1.2.8 - oxc-parser: 0.120.0 - oxc-resolver: 11.19.1 + oxc-parser: 0.121.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + oxc-resolver: 11.19.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) picocolors: 1.1.1 picomatch: 4.0.4 smol-toml: 1.6.1 @@ -13395,6 +14921,13 @@ snapshots: unbash: 2.2.0 yaml: 2.8.3 zod: 4.3.6 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + knuth-shuffle-seeded@1.0.6: + dependencies: + seed-random: 2.2.0 kolorist@1.8.0: {} @@ -13513,11 +15046,13 @@ snapshots: rfdc: 1.4.1 wrap-ansi: 9.0.2 + load-tsconfig@0.2.5: {} + loader-runner@4.3.1: {} local-pkg@1.1.2: dependencies: - mlly: 1.8.1 + mlly: 1.8.2 pkg-types: 2.3.0 quansync: 0.2.11 @@ -13529,6 +15064,10 @@ snapshots: lodash.merge@4.6.2: {} + lodash.mergewith@4.6.2: {} + + lodash.sortby@4.7.0: {} + lodash@4.17.23: {} log-update@6.1.0: @@ -13547,6 +15086,10 @@ snapshots: loupe@3.2.1: {} + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + lowlight@1.20.0: dependencies: fault: 1.0.4 @@ -13584,7 +15127,9 @@ snapshots: marked@16.4.2: {} - marked@17.0.4: {} + marked@17.0.5: {} + + math-intrinsics@1.1.0: {} mdast-util-directive@3.1.0: dependencies: @@ -13797,9 +15342,6 @@ snapshots: mdn-data@2.23.0: {} - mdn-data@2.27.1: - optional: true - memoize-one@5.2.1: {} merge-stream@2.0.0: {} @@ -13821,7 +15363,7 @@ snapshots: dagre-d3-es: 7.0.14 dayjs: 1.11.20 dompurify: 3.3.2 - katex: 0.16.40 + katex: 0.16.44 khroma: 2.1.0 lodash-es: 4.17.23 marked: 16.4.2 @@ -13928,7 +15470,7 @@ snapshots: dependencies: '@types/katex': 0.16.8 devlop: 1.1.0 - katex: 0.16.40 + katex: 0.16.44 micromark-factory-space: 2.0.1 micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 @@ -14101,8 +15643,8 @@ snapshots: micromark@4.0.2: dependencies: - '@types/debug': 4.1.12 - debug: 4.4.3 + '@types/debug': 4.1.13 + debug: 4.4.3(supports-color@8.1.1) decode-named-character-reference: 1.3.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -14132,6 +15674,8 @@ snapshots: dependencies: mime-db: 1.52.0 + mime@3.0.0: {} + mime@4.1.0: {} mimic-function@5.0.1: {} @@ -14143,7 +15687,7 @@ snapshots: minimatch@10.2.4: dependencies: - brace-expansion: 5.0.4 + brace-expansion: 5.0.5 minimatch@3.1.5: dependencies: @@ -14162,7 +15706,9 @@ snapshots: mkdirp-classic@0.5.3: optional: true - mlly@1.8.1: + mkdirp@3.0.1: {} + + mlly@1.8.2: dependencies: acorn: 8.16.0 pathe: 2.0.3 @@ -14210,12 +15756,12 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - next@16.2.1(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0): + next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0): dependencies: '@next/env': 16.2.1 '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.10.8 - caniuse-lite: 1.0.30001780 + baseline-browser-mapping: 2.10.12 + caniuse-lite: 1.0.30001781 postcss: 8.4.31 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -14229,12 +15775,18 @@ snapshots: '@next/swc-linux-x64-musl': 16.2.1 '@next/swc-win32-arm64-msvc': 16.2.1 '@next/swc-win32-x64-msvc': 16.2.1 + '@playwright/test': 1.58.2 sass: 1.98.0 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + node-abi@3.89.0: dependencies: semver: 7.7.4 @@ -14247,6 +15799,12 @@ snapshots: node-releases@2.0.36: {} + normalize-package-data@8.0.0: + dependencies: + hosted-git-info: 9.0.2 + semver: 7.7.4 + validate-npm-package-license: 3.0.4 + normalize-path@3.0.0: {} normalize-wheel@1.0.1: {} @@ -14255,12 +15813,12 @@ snapshots: dependencies: boolbase: 1.0.0 - nuqs@2.8.9(next@16.2.1(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react@19.2.4): + nuqs@2.8.9(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react@19.2.4): dependencies: '@standard-schema/spec': 1.0.0 react: 19.2.4 optionalDependencies: - next: 16.2.1(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) + next: 16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) object-assign@4.1.1: {} @@ -14293,6 +15851,15 @@ snapshots: is-inside-container: 1.0.0 wsl-utils: 0.1.0 + open@11.0.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + openapi-types@12.1.3: {} optionator@0.9.4: @@ -14304,32 +15871,35 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - oxc-parser@0.120.0: + oxc-parser@0.121.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): dependencies: - '@oxc-project/types': 0.120.0 + '@oxc-project/types': 0.121.0 optionalDependencies: - '@oxc-parser/binding-android-arm-eabi': 0.120.0 - '@oxc-parser/binding-android-arm64': 0.120.0 - '@oxc-parser/binding-darwin-arm64': 0.120.0 - '@oxc-parser/binding-darwin-x64': 0.120.0 - '@oxc-parser/binding-freebsd-x64': 0.120.0 - '@oxc-parser/binding-linux-arm-gnueabihf': 0.120.0 - '@oxc-parser/binding-linux-arm-musleabihf': 0.120.0 - '@oxc-parser/binding-linux-arm64-gnu': 0.120.0 - '@oxc-parser/binding-linux-arm64-musl': 0.120.0 - '@oxc-parser/binding-linux-ppc64-gnu': 0.120.0 - '@oxc-parser/binding-linux-riscv64-gnu': 0.120.0 - '@oxc-parser/binding-linux-riscv64-musl': 0.120.0 - '@oxc-parser/binding-linux-s390x-gnu': 0.120.0 - '@oxc-parser/binding-linux-x64-gnu': 0.120.0 - '@oxc-parser/binding-linux-x64-musl': 0.120.0 - '@oxc-parser/binding-openharmony-arm64': 0.120.0 - '@oxc-parser/binding-wasm32-wasi': 0.120.0 - '@oxc-parser/binding-win32-arm64-msvc': 0.120.0 - '@oxc-parser/binding-win32-ia32-msvc': 0.120.0 - '@oxc-parser/binding-win32-x64-msvc': 0.120.0 + '@oxc-parser/binding-android-arm-eabi': 0.121.0 + '@oxc-parser/binding-android-arm64': 0.121.0 + '@oxc-parser/binding-darwin-arm64': 0.121.0 + '@oxc-parser/binding-darwin-x64': 0.121.0 + '@oxc-parser/binding-freebsd-x64': 0.121.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.121.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.121.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.121.0 + '@oxc-parser/binding-linux-arm64-musl': 0.121.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.121.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.121.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.121.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.121.0 + '@oxc-parser/binding-linux-x64-gnu': 0.121.0 + '@oxc-parser/binding-linux-x64-musl': 0.121.0 + '@oxc-parser/binding-openharmony-arm64': 0.121.0 + '@oxc-parser/binding-wasm32-wasi': 0.121.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + '@oxc-parser/binding-win32-arm64-msvc': 0.121.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.121.0 + '@oxc-parser/binding-win32-x64-msvc': 0.121.0 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' - oxc-resolver@11.19.1: + oxc-resolver@11.19.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): optionalDependencies: '@oxc-resolver/binding-android-arm-eabi': 11.19.1 '@oxc-resolver/binding-android-arm64': 11.19.1 @@ -14347,77 +15917,88 @@ snapshots: '@oxc-resolver/binding-linux-x64-gnu': 11.19.1 '@oxc-resolver/binding-linux-x64-musl': 11.19.1 '@oxc-resolver/binding-openharmony-arm64': 11.19.1 - '@oxc-resolver/binding-wasm32-wasi': 11.19.1 + '@oxc-resolver/binding-wasm32-wasi': 11.19.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) '@oxc-resolver/binding-win32-arm64-msvc': 11.19.1 '@oxc-resolver/binding-win32-ia32-msvc': 11.19.1 '@oxc-resolver/binding-win32-x64-msvc': 11.19.1 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' - oxfmt@0.41.0: + oxfmt@0.42.0: dependencies: tinypool: 2.1.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.41.0 - '@oxfmt/binding-android-arm64': 0.41.0 - '@oxfmt/binding-darwin-arm64': 0.41.0 - '@oxfmt/binding-darwin-x64': 0.41.0 - '@oxfmt/binding-freebsd-x64': 0.41.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.41.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.41.0 - '@oxfmt/binding-linux-arm64-gnu': 0.41.0 - '@oxfmt/binding-linux-arm64-musl': 0.41.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.41.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.41.0 - '@oxfmt/binding-linux-riscv64-musl': 0.41.0 - '@oxfmt/binding-linux-s390x-gnu': 0.41.0 - '@oxfmt/binding-linux-x64-gnu': 0.41.0 - '@oxfmt/binding-linux-x64-musl': 0.41.0 - '@oxfmt/binding-openharmony-arm64': 0.41.0 - '@oxfmt/binding-win32-arm64-msvc': 0.41.0 - '@oxfmt/binding-win32-ia32-msvc': 0.41.0 - '@oxfmt/binding-win32-x64-msvc': 0.41.0 + '@oxfmt/binding-android-arm-eabi': 0.42.0 + '@oxfmt/binding-android-arm64': 0.42.0 + '@oxfmt/binding-darwin-arm64': 0.42.0 + '@oxfmt/binding-darwin-x64': 0.42.0 + '@oxfmt/binding-freebsd-x64': 0.42.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.42.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.42.0 + '@oxfmt/binding-linux-arm64-gnu': 0.42.0 + '@oxfmt/binding-linux-arm64-musl': 0.42.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.42.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.42.0 + '@oxfmt/binding-linux-riscv64-musl': 0.42.0 + '@oxfmt/binding-linux-s390x-gnu': 0.42.0 + '@oxfmt/binding-linux-x64-gnu': 0.42.0 + '@oxfmt/binding-linux-x64-musl': 0.42.0 + '@oxfmt/binding-openharmony-arm64': 0.42.0 + '@oxfmt/binding-win32-arm64-msvc': 0.42.0 + '@oxfmt/binding-win32-ia32-msvc': 0.42.0 + '@oxfmt/binding-win32-x64-msvc': 0.42.0 - oxlint-tsgolint@0.17.1: + oxlint-tsgolint@0.17.3: optionalDependencies: - '@oxlint-tsgolint/darwin-arm64': 0.17.1 - '@oxlint-tsgolint/darwin-x64': 0.17.1 - '@oxlint-tsgolint/linux-arm64': 0.17.1 - '@oxlint-tsgolint/linux-x64': 0.17.1 - '@oxlint-tsgolint/win32-arm64': 0.17.1 - '@oxlint-tsgolint/win32-x64': 0.17.1 + '@oxlint-tsgolint/darwin-arm64': 0.17.3 + '@oxlint-tsgolint/darwin-x64': 0.17.3 + '@oxlint-tsgolint/linux-arm64': 0.17.3 + '@oxlint-tsgolint/linux-x64': 0.17.3 + '@oxlint-tsgolint/win32-arm64': 0.17.3 + '@oxlint-tsgolint/win32-x64': 0.17.3 - oxlint@1.56.0(oxlint-tsgolint@0.17.1): + oxlint@1.57.0(oxlint-tsgolint@0.17.3): optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.56.0 - '@oxlint/binding-android-arm64': 1.56.0 - '@oxlint/binding-darwin-arm64': 1.56.0 - '@oxlint/binding-darwin-x64': 1.56.0 - '@oxlint/binding-freebsd-x64': 1.56.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.56.0 - '@oxlint/binding-linux-arm-musleabihf': 1.56.0 - '@oxlint/binding-linux-arm64-gnu': 1.56.0 - '@oxlint/binding-linux-arm64-musl': 1.56.0 - '@oxlint/binding-linux-ppc64-gnu': 1.56.0 - '@oxlint/binding-linux-riscv64-gnu': 1.56.0 - '@oxlint/binding-linux-riscv64-musl': 1.56.0 - '@oxlint/binding-linux-s390x-gnu': 1.56.0 - '@oxlint/binding-linux-x64-gnu': 1.56.0 - '@oxlint/binding-linux-x64-musl': 1.56.0 - '@oxlint/binding-openharmony-arm64': 1.56.0 - '@oxlint/binding-win32-arm64-msvc': 1.56.0 - '@oxlint/binding-win32-ia32-msvc': 1.56.0 - '@oxlint/binding-win32-x64-msvc': 1.56.0 - oxlint-tsgolint: 0.17.1 + '@oxlint/binding-android-arm-eabi': 1.57.0 + '@oxlint/binding-android-arm64': 1.57.0 + '@oxlint/binding-darwin-arm64': 1.57.0 + '@oxlint/binding-darwin-x64': 1.57.0 + '@oxlint/binding-freebsd-x64': 1.57.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.57.0 + '@oxlint/binding-linux-arm-musleabihf': 1.57.0 + '@oxlint/binding-linux-arm64-gnu': 1.57.0 + '@oxlint/binding-linux-arm64-musl': 1.57.0 + '@oxlint/binding-linux-ppc64-gnu': 1.57.0 + '@oxlint/binding-linux-riscv64-gnu': 1.57.0 + '@oxlint/binding-linux-riscv64-musl': 1.57.0 + '@oxlint/binding-linux-s390x-gnu': 1.57.0 + '@oxlint/binding-linux-x64-gnu': 1.57.0 + '@oxlint/binding-linux-x64-musl': 1.57.0 + '@oxlint/binding-openharmony-arm64': 1.57.0 + '@oxlint/binding-win32-arm64-msvc': 1.57.0 + '@oxlint/binding-win32-ia32-msvc': 1.57.0 + '@oxlint/binding-win32-x64-msvc': 1.57.0 + oxlint-tsgolint: 0.17.3 p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 + p-limit@7.3.0: + dependencies: + yocto-queue: 1.2.2 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 package-manager-detector@1.6.0: {} + pad-right@0.2.2: + dependencies: + repeat-string: 1.6.1 + pako@0.2.9: {} papaparse@5.5.3: {} @@ -14456,6 +16037,12 @@ snapshots: dependencies: parse-statements: 1.0.11 + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.29.0 + index-to-position: 1.2.0 + type-fest: 4.41.0 + parse-statements@1.0.11: {} parse5-htmlparser2-tree-adapter@7.1.0: @@ -14531,7 +16118,7 @@ snapshots: pkg-types@1.3.1: dependencies: confbox: 0.1.8 - mlly: 1.8.1 + mlly: 1.8.2 pathe: 2.0.3 pkg-types@2.3.0: @@ -14540,6 +16127,14 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 + playwright-core@1.58.2: {} + + playwright@1.58.2: + dependencies: + playwright-core: 1.58.2 + optionalDependencies: + fsevents: 2.3.2 + pluralize@8.0.0: {} pngjs@7.0.0: {} @@ -14558,7 +16153,7 @@ snapshots: portfinder@1.0.38: dependencies: async: 3.2.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -14587,6 +16182,15 @@ snapshots: tsx: 4.21.0 yaml: 2.8.3 + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 2.6.1 + postcss: 8.5.8 + tsx: 4.21.0 + yaml: 2.8.3 + postcss-nested@6.2.0(postcss@8.5.8): dependencies: postcss: 8.5.8 @@ -14621,6 +16225,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + powershell-utils@0.1.0: {} + prebuild-install@7.1.3: dependencies: detect-libc: 2.1.2 @@ -14647,18 +16253,24 @@ snapshots: prismjs@1.30.0: {} + progress@2.0.3: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 + property-expr@2.0.6: {} + property-information@5.6.0: dependencies: xtend: 4.0.2 property-information@7.1.0: {} + proxy-from-env@2.1.0: {} + pump@3.0.4: dependencies: end-of-stream: 1.4.5 @@ -14731,7 +16343,7 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - react-easy-crop@5.5.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-easy-crop@5.5.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: normalize-wheel: 1.0.1 react: 19.2.4 @@ -14749,11 +16361,11 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - react-i18next@16.6.1(i18next@25.10.4(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3): + react-i18next@16.6.6(i18next@25.10.10(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3): dependencies: '@babel/runtime': 7.29.2 html-parse-stringify: 3.0.1 - i18next: 25.10.4(typescript@5.9.3) + i18next: 25.10.10(typescript@5.9.3) react: 19.2.4 use-sync-external-store: 1.6.0(react@19.2.4) optionalDependencies: @@ -14881,6 +16493,20 @@ snapshots: dependencies: pify: 2.3.0 + read-package-up@12.0.0: + dependencies: + find-up-simple: 1.0.1 + read-pkg: 10.1.0 + type-fest: 5.5.0 + + read-pkg@10.1.0: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 8.0.0 + parse-json: 8.3.0 + type-fest: 5.5.0 + unicorn-magic: 0.4.0 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -14940,6 +16566,8 @@ snapshots: dependencies: '@eslint-community/regexpp': 4.12.2 + reflect-metadata@0.2.2: {} + refractor@3.6.0: dependencies: hastscript: 6.0.0 @@ -14951,6 +16579,10 @@ snapshots: '@eslint-community/regexpp': 4.12.2 refa: 0.12.1 + regexp-match-indices@1.0.2: + dependencies: + regexp-tree: 0.1.27 + regexp-tree@0.1.27: {} regjsparser@0.13.0: @@ -14967,7 +16599,7 @@ snapshots: '@types/katex': 0.16.8 hast-util-from-html-isomorphic: 2.0.0 hast-util-to-text: 4.0.2 - katex: 0.16.40 + katex: 0.16.44 unist-util-visit-parents: 6.0.2 vfile: 6.0.3 @@ -15057,6 +16689,8 @@ snapshots: remend@1.3.0: {} + repeat-string@1.6.1: {} + require-from-string@2.0.2: {} reselect@5.1.1: {} @@ -15067,6 +16701,8 @@ snapshots: resolve-from@4.0.0: {} + resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} resolve@1.22.11: @@ -15084,7 +16720,31 @@ snapshots: rfdc@1.4.1: {} - robust-predicates@3.0.2: {} + robust-predicates@3.0.3: {} + + rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): + dependencies: + '@oxc-project/types': 0.122.0 + '@rolldown/pluginutils': 1.0.0-rc.12 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-x64': 1.0.0-rc.12 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' rollup@4.59.0: dependencies: @@ -15163,11 +16823,6 @@ snapshots: sax@1.6.0: {} - saxes@6.0.0: - dependencies: - xmlchars: 2.2.0 - optional: true - scheduler@0.27.0: {} schema-utils@4.3.3: @@ -15185,6 +16840,8 @@ snapshots: refa: 0.12.1 regexp-ast-analysis: 0.7.1 + seed-random@2.2.0: {} + semver@6.3.1: {} semver@7.7.4: {} @@ -15291,8 +16948,18 @@ snapshots: space-separated-tokens@2.0.2: {} + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.23 + spdx-exceptions@2.5.0: {} + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.23 + spdx-expression-parse@4.0.0: dependencies: spdx-exceptions: 2.5.0 @@ -15300,7 +16967,9 @@ snapshots: spdx-license-ids@3.0.23: {} - srvx@0.11.12: {} + srvx@0.11.13: {} + + stackframe@1.3.4: {} state-local@1.0.7: {} @@ -15308,7 +16977,7 @@ snapshots: std-semver@1.0.8: {} - storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -15321,7 +16990,7 @@ snapshots: recast: 0.23.11 semver: 7.7.4 use-sync-external-store: 1.6.0(react@19.2.4) - ws: 8.19.0 + ws: 8.20.0 transitivePeerDependencies: - '@testing-library/dom' - bufferutil @@ -15334,7 +17003,7 @@ snapshots: clsx: 2.1.1 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 - marked: 17.0.4 + marked: 17.0.5 mermaid: 11.13.0 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -15352,6 +17021,8 @@ snapshots: transitivePeerDependencies: - supports-color + string-argv@0.3.1: {} + string-argv@0.3.2: {} string-ts@2.3.1: {} @@ -15396,6 +17067,8 @@ snapshots: dependencies: js-tokens: 9.0.1 + structured-clone-es@2.0.0: {} + style-to-js@1.1.21: dependencies: style-to-object: 1.0.14 @@ -15443,9 +17116,6 @@ snapshots: picocolors: 1.1.1 sax: 1.6.0 - symbol-tree@3.2.4: - optional: true - synckit@0.11.12: dependencies: '@pkgr/core': 0.2.9 @@ -15488,8 +17158,6 @@ snapshots: - tsx - yaml - tapable@2.3.0: {} - tapable@2.3.2: {} tar-fs@2.1.4: @@ -15559,6 +17227,8 @@ snapshots: dependencies: any-promise: 1.3.0 + tiny-case@1.0.3: {} + tiny-inflate@1.0.3: {} tiny-invariant@1.2.0: {} @@ -15567,6 +17237,8 @@ snapshots: tinybench@2.9.0: {} + tinyexec@0.3.2: {} + tinyexec@1.0.4: {} tinyglobby@0.2.15: @@ -15603,23 +17275,17 @@ snapshots: dependencies: eslint-visitor-keys: 5.0.1 + toposort@2.0.2: {} + totalist@3.0.1: {} - tough-cookie@6.0.1: - dependencies: - tldts: 7.0.27 - optional: true - - tr46@6.0.0: - dependencies: - punycode: 2.3.1 - optional: true + tree-kill@1.2.2: {} trim-lines@3.0.1: {} trough@2.2.0: {} - ts-api-utils@2.4.0(typescript@5.9.3): + ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -15644,7 +17310,7 @@ snapshots: dependencies: chalk: 4.1.2 enhanced-resolve: 5.20.1 - tapable: 2.3.0 + tapable: 2.3.2 tsconfig-paths: 4.2.0 tsconfig-paths@4.2.0: @@ -15659,6 +17325,34 @@ snapshots: tslib@2.8.1: {} + tsup@8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.2) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3(supports-color@8.1.1) + esbuild: 0.27.2 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.3) + resolve-from: 5.0.0 + rollup: 4.59.0 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.8 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + tsx@4.21.0: dependencies: esbuild: 0.27.2 @@ -15677,7 +17371,11 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-fest@5.4.4: + type-fest@2.19.0: {} + + type-fest@4.41.0: {} + + type-fest@5.5.0: dependencies: tagged-tag: 1.0.0 @@ -15706,14 +17404,13 @@ snapshots: undici@7.24.0: {} - undici@7.24.6: - optional: true - unicode-trie@2.0.0: dependencies: pako: 0.2.9 tiny-inflate: 1.0.3 + unicorn-magic@0.4.0: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -15783,6 +17480,10 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + upper-case-first@2.0.2: + dependencies: + tslib: 2.8.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -15832,16 +17533,23 @@ snapshots: dependencies: react: 19.2.4 + util-arity@1.1.0: {} + util-deprecate@1.0.2: {} uuid@11.1.0: {} uuid@13.0.0: {} - valibot@1.3.0(typescript@5.9.3): + valibot@1.3.1(typescript@5.9.3): optionalDependencies: typescript: 5.9.3 + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 @@ -15857,37 +17565,27 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vinext@https://pkg.pr.new/vinext@b6a2cac(33c71b051bfc49f90bf5d8b6a8976975): + vinext@0.0.38(f5786d681f520e26604259e094ebaa46): dependencies: - '@unpic/react': 1.0.2(next@16.2.1(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@unpic/react': 1.0.2(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@vercel/og': 0.8.6 - '@vitejs/plugin-react': 6.0.1(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) + '@vitejs/plugin-react': 6.0.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) magic-string: 0.30.21 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) rsc-html-stream: 0.0.7 - vite: '@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' vite-plugin-commonjs: 0.10.4 - vite-tsconfig-paths: 6.1.1(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3) + vite-tsconfig-paths: 6.1.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3) optionalDependencies: '@mdx-js/rollup': 3.1.1(rollup@4.59.0) - '@vitejs/plugin-rsc': 0.5.21(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(react@19.2.4) + '@vitejs/plugin-rsc': 0.5.21(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(react@19.2.4) react-server-dom-webpack: 19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) transitivePeerDependencies: - next - supports-color - typescript - vite-dev-rpc@1.1.0(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)): - dependencies: - birpc: 2.9.0 - vite: '@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' - vite-hot-client: 2.1.0(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) - - vite-hot-client@2.1.0(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)): - dependencies: - vite: '@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' - vite-plugin-commonjs@0.10.4: dependencies: acorn: 8.16.0 @@ -15901,54 +17599,57 @@ snapshots: fast-glob: 3.3.3 magic-string: 0.30.21 - vite-plugin-inspect@11.3.3(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)): + vite-plugin-inspect@12.0.0-beta.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)(ws@8.20.0): dependencies: + '@vitejs/devtools-kit': 0.1.11(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3)(ws@8.20.0) ansis: 4.2.0 - debug: 4.4.3 error-stack-parser-es: 1.0.5 + obug: 2.1.1 ohash: 2.0.11 - open: 10.2.0 + open: 11.0.0 perfect-debounce: 2.1.0 sirv: 3.0.2 unplugin-utils: 0.3.1 - vite: '@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' - vite-dev-rpc: 1.1.0(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' transitivePeerDependencies: - - supports-color + - typescript + - ws - vite-plugin-storybook-nextjs@3.2.3(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(next@16.2.1(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(storybook@10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3): + vite-plugin-storybook-nextjs@3.2.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(next@16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3): dependencies: '@next/env': 16.0.0 image-size: 2.0.2 magic-string: 0.30.21 module-alias: 2.3.4 - next: 16.2.1(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) - storybook: 10.3.1(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.2.1(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) + storybook: 10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ts-dedent: 2.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' - vite-tsconfig-paths: 5.1.4(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3) + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite-tsconfig-paths: 5.1.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3) transitivePeerDependencies: - supports-color - typescript - vite-plus@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3): + vite-plus@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3): dependencies: - '@oxc-project/types': 0.120.0 - '@voidzero-dev/vite-plus-core': 0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) - '@voidzero-dev/vite-plus-test': 0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + '@oxc-project/types': 0.122.0 + '@voidzero-dev/vite-plus-core': 0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + '@voidzero-dev/vite-plus-test': 0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) cac: 7.0.0 cross-spawn: 7.0.6 - oxfmt: 0.41.0 - oxlint: 1.56.0(oxlint-tsgolint@0.17.1) - oxlint-tsgolint: 0.17.1 + oxfmt: 0.42.0 + oxlint: 1.57.0(oxlint-tsgolint@0.17.3) + oxlint-tsgolint: 0.17.3 picocolors: 1.1.1 optionalDependencies: - '@voidzero-dev/vite-plus-darwin-arm64': 0.1.13 - '@voidzero-dev/vite-plus-darwin-x64': 0.1.13 - '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.13 - '@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.13 - '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.13 - '@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.13 + '@voidzero-dev/vite-plus-darwin-arm64': 0.1.14 + '@voidzero-dev/vite-plus-darwin-x64': 0.1.14 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.14 + '@voidzero-dev/vite-plus-linux-arm64-musl': 0.1.14 + '@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.14 + '@voidzero-dev/vite-plus-linux-x64-musl': 0.1.14 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.14 + '@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.14 transitivePeerDependencies: - '@arethetypeswrong/core' - '@edge-runtime/vm' @@ -15977,36 +17678,104 @@ snapshots: - vite - yaml - vite-tsconfig-paths@5.1.4(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3): + vite-plus@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3): dependencies: - debug: 4.4.3 + '@oxc-project/types': 0.122.0 + '@voidzero-dev/vite-plus-core': 0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + '@voidzero-dev/vite-plus-test': 0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) + cac: 7.0.0 + cross-spawn: 7.0.6 + oxfmt: 0.42.0 + oxlint: 1.57.0(oxlint-tsgolint@0.17.3) + oxlint-tsgolint: 0.17.3 + picocolors: 1.1.1 + optionalDependencies: + '@voidzero-dev/vite-plus-darwin-arm64': 0.1.14 + '@voidzero-dev/vite-plus-darwin-x64': 0.1.14 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.14 + '@voidzero-dev/vite-plus-linux-arm64-musl': 0.1.14 + '@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.14 + '@voidzero-dev/vite-plus-linux-x64-musl': 0.1.14 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.14 + '@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.14 + transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@edge-runtime/vm' + - '@opentelemetry/api' + - '@tsdown/css' + - '@tsdown/exe' + - '@types/node' + - '@vitejs/devtools' + - '@vitest/ui' + - bufferutil + - esbuild + - happy-dom + - jiti + - jsdom + - less + - publint + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - typescript + - unplugin-unused + - utf-8-validate + - vite + - yaml + + vite-tsconfig-paths@5.1.4(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3): + dependencies: + debug: 4.4.3(supports-color@8.1.1) globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: '@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' transitivePeerDependencies: - supports-color - typescript - vite-tsconfig-paths@6.1.1(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3): + vite-tsconfig-paths@6.1.1(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(typescript@5.9.3): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) - vite: '@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' transitivePeerDependencies: - supports-color - typescript - vitefu@1.1.2(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)): + vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.8 + rolldown: 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + tinyglobby: 0.2.15 optionalDependencies: - vite: '@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + '@types/node': 25.5.0 + esbuild: 0.27.2 + fsevents: 2.3.3 + jiti: 2.6.1 + sass: 1.98.0 + terser: 5.46.1 + tsx: 4.21.0 + yaml: 2.8.3 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' - vitest-canvas-mock@1.1.3(@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)): + vitefu@1.1.2(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)): + optionalDependencies: + vite: '@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + + vitest-canvas-mock@1.1.4(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)): dependencies: cssfontparser: 1.2.1 moo-color: 1.0.3 - vitest: '@voidzero-dev/vite-plus-test@0.1.13(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.13(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(jsdom@29.0.1(canvas@3.2.2))(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' + vitest: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' void-elements@3.1.0: {} @@ -16029,7 +17798,7 @@ snapshots: vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@1.21.7)): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 10.1.0(jiti@1.21.7) eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 @@ -16039,11 +17808,6 @@ snapshots: transitivePeerDependencies: - supports-color - w3c-xmlserializer@5.0.0: - dependencies: - xml-name-validator: 5.0.0 - optional: true - walk-up-path@4.0.0: {} watchpack@2.5.1: @@ -16055,9 +17819,6 @@ snapshots: web-vitals@5.1.0: {} - webidl-conversions@8.0.1: - optional: true - webpack-sources@3.3.4: {} webpack-virtual-modules@0.6.2: {} @@ -16102,18 +17863,6 @@ snapshots: whatwg-mimetype@4.0.0: {} - whatwg-mimetype@5.0.0: - optional: true - - whatwg-url@16.0.1: - dependencies: - '@exodus/bytes': 1.15.0 - tr46: 6.0.0 - webidl-conversions: 8.0.1 - transitivePeerDependencies: - - '@noble/hashes' - optional: true - which@2.0.2: dependencies: isexe: 2.0.0 @@ -16128,21 +17877,20 @@ snapshots: wrappy@1.0.2: {} - ws@8.19.0: {} - ws@8.20.0: {} wsl-utils@0.1.0: dependencies: is-wsl: 3.1.1 + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.1 + powershell-utils: 0.1.0 + xml-name-validator@4.0.0: {} - xml-name-validator@5.0.0: - optional: true - - xmlchars@2.2.0: - optional: true + xmlbuilder@15.1.1: {} xtend@4.0.2: {} @@ -16168,8 +17916,17 @@ snapshots: yocto-queue@0.1.0: {} + yocto-queue@1.2.2: {} + yoga-layout@3.2.1: {} + yup@1.7.1: + dependencies: + property-expr: 2.0.6 + tiny-case: 1.0.3 + toposort: 2.0.2 + type-fest: 2.19.0 + zen-observable@0.10.0: {} zimmerframe@1.1.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000000..dece6f3f4f --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,257 @@ +packages: + - web + - e2e + - sdks/nodejs-client +overrides: + "@lexical/code": npm:lexical-code-no-prism@0.41.0 + "@monaco-editor/loader": 1.7.0 + "@nolyfill/safe-buffer": npm:safe-buffer@^5.2.1 + array-includes: npm:@nolyfill/array-includes@^1.0.44 + array.prototype.findlast: npm:@nolyfill/array.prototype.findlast@^1.0.44 + array.prototype.findlastindex: npm:@nolyfill/array.prototype.findlastindex@^1.0.44 + array.prototype.flat: npm:@nolyfill/array.prototype.flat@^1.0.44 + array.prototype.flatmap: npm:@nolyfill/array.prototype.flatmap@^1.0.44 + array.prototype.tosorted: npm:@nolyfill/array.prototype.tosorted@^1.0.44 + assert: npm:@nolyfill/assert@^1.0.26 + brace-expansion@<2.0.2: 2.0.2 + canvas: ^3.2.2 + devalue@<5.3.2: 5.3.2 + dompurify@>=3.1.3 <=3.3.1: 3.3.2 + es-iterator-helpers: npm:@nolyfill/es-iterator-helpers@^1.0.21 + esbuild@<0.27.2: 0.27.2 + flatted@<=3.4.1: 3.4.2 + glob@>=10.2.0 <10.5.0: 11.1.0 + hasown: npm:@nolyfill/hasown@^1.0.44 + is-arguments: npm:@nolyfill/is-arguments@^1.0.44 + is-core-module: npm:@nolyfill/is-core-module@^1.0.39 + is-generator-function: npm:@nolyfill/is-generator-function@^1.0.44 + is-typed-array: npm:@nolyfill/is-typed-array@^1.0.44 + isarray: npm:@nolyfill/isarray@^1.0.44 + object.assign: npm:@nolyfill/object.assign@^1.0.44 + object.entries: npm:@nolyfill/object.entries@^1.0.44 + object.fromentries: npm:@nolyfill/object.fromentries@^1.0.44 + object.groupby: npm:@nolyfill/object.groupby@^1.0.44 + object.values: npm:@nolyfill/object.values@^1.0.44 + pbkdf2: ~3.1.5 + pbkdf2@<3.1.3: 3.1.3 + picomatch@<2.3.2: 2.3.2 + picomatch@>=4.0.0 <4.0.4: 4.0.4 + prismjs: ~1.30 + prismjs@<1.30.0: 1.30.0 + rollup@>=4.0.0 <4.59.0: 4.59.0 + safe-buffer: ^5.2.1 + safe-regex-test: npm:@nolyfill/safe-regex-test@^1.0.44 + safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44 + side-channel: npm:@nolyfill/side-channel@^1.0.44 + smol-toml@<1.6.1: 1.6.1 + solid-js: 1.9.11 + string-width: ~8.2.0 + string.prototype.includes: npm:@nolyfill/string.prototype.includes@^1.0.44 + string.prototype.matchall: npm:@nolyfill/string.prototype.matchall@^1.0.44 + string.prototype.repeat: npm:@nolyfill/string.prototype.repeat@^1.0.44 + string.prototype.trimend: npm:@nolyfill/string.prototype.trimend@^1.0.44 + svgo@>=3.0.0 <3.3.3: 3.3.3 + tar@<=7.5.10: 7.5.11 + typed-array-buffer: npm:@nolyfill/typed-array-buffer@^1.0.44 + undici@>=7.0.0 <7.24.0: 7.24.0 + vite: npm:@voidzero-dev/vite-plus-core@0.1.14 + vitest: npm:@voidzero-dev/vite-plus-test@0.1.14 + which-typed-array: npm:@nolyfill/which-typed-array@^1.0.44 + yaml@>=2.0.0 <2.8.3: 2.8.3 + yauzl@<3.2.1: 3.2.1 +ignoredBuiltDependencies: + - canvas + - core-js-pure +onlyBuiltDependencies: + - "@parcel/watcher" + - esbuild + - sharp +catalog: + "@amplitude/analytics-browser": 2.38.0 + "@amplitude/plugin-session-replay-browser": 1.27.5 + "@antfu/eslint-config": 7.7.3 + "@base-ui/react": 1.3.0 + "@chromatic-com/storybook": 5.1.1 + "@cucumber/cucumber": 12.7.0 + "@egoist/tailwindcss-icons": 1.9.2 + "@emoji-mart/data": 1.2.1 + "@eslint-react/eslint-plugin": 3.0.0 + "@eslint/js": ^10.0.1 + "@floating-ui/react": 0.27.19 + "@formatjs/intl-localematcher": 0.8.2 + "@headlessui/react": 2.2.9 + "@heroicons/react": 2.2.0 + "@hono/node-server": 1.19.11 + "@iconify-json/heroicons": 1.2.3 + "@iconify-json/ri": 1.2.10 + "@lexical/code": 0.42.0 + "@lexical/link": 0.42.0 + "@lexical/list": 0.42.0 + "@lexical/react": 0.42.0 + "@lexical/selection": 0.42.0 + "@lexical/text": 0.42.0 + "@lexical/utils": 0.42.0 + "@mdx-js/loader": 3.1.1 + "@mdx-js/react": 3.1.1 + "@mdx-js/rollup": 3.1.1 + "@monaco-editor/react": 4.7.0 + "@next/eslint-plugin-next": 16.2.1 + "@next/mdx": 16.2.1 + "@orpc/client": 1.13.13 + "@orpc/contract": 1.13.13 + "@orpc/openapi-client": 1.13.13 + "@orpc/tanstack-query": 1.13.13 + "@playwright/test": 1.58.2 + "@remixicon/react": 4.9.0 + "@rgrove/parse-xml": 4.2.0 + "@sentry/react": 10.46.0 + "@storybook/addon-docs": 10.3.3 + "@storybook/addon-links": 10.3.3 + "@storybook/addon-onboarding": 10.3.3 + "@storybook/addon-themes": 10.3.3 + "@storybook/nextjs-vite": 10.3.3 + "@storybook/react": 10.3.3 + "@streamdown/math": 1.0.2 + "@svgdotjs/svg.js": 3.2.5 + "@t3-oss/env-nextjs": 0.13.11 + "@tailwindcss/typography": 0.5.19 + "@tanstack/eslint-plugin-query": 5.95.2 + "@tanstack/react-devtools": 0.10.0 + "@tanstack/react-form": 1.28.5 + "@tanstack/react-form-devtools": 0.2.19 + "@tanstack/react-query": 5.95.2 + "@tanstack/react-query-devtools": 5.95.2 + "@testing-library/dom": 10.4.1 + "@testing-library/jest-dom": 6.9.1 + "@testing-library/react": 16.3.2 + "@testing-library/user-event": 14.6.1 + "@tsslint/cli": 3.0.2 + "@tsslint/compat-eslint": 3.0.2 + "@tsslint/config": 3.0.2 + "@types/js-cookie": 3.0.6 + "@types/js-yaml": 4.0.9 + "@types/negotiator": 0.6.4 + "@types/node": 25.5.0 + "@types/postcss-js": 4.1.0 + "@types/qs": 6.15.0 + "@types/react": 19.2.14 + "@types/react-dom": 19.2.3 + "@types/react-syntax-highlighter": 15.5.13 + "@types/react-window": 1.8.8 + "@types/sortablejs": 1.15.9 + "@typescript-eslint/eslint-plugin": ^8.57.2 + "@typescript-eslint/parser": 8.57.2 + "@typescript/native-preview": 7.0.0-dev.20260329.1 + "@vitejs/plugin-react": 6.0.1 + "@vitejs/plugin-rsc": 0.5.21 + "@vitest/coverage-v8": 4.1.2 + abcjs: 6.6.2 + agentation: 3.0.2 + ahooks: 3.9.7 + autoprefixer: 10.4.27 + axios: ^1.14.0 + class-variance-authority: 0.7.1 + clsx: 2.1.1 + cmdk: 1.1.1 + code-inspector-plugin: 1.4.5 + copy-to-clipboard: 3.3.3 + cron-parser: 5.5.0 + dayjs: 1.11.20 + decimal.js: 10.6.0 + dompurify: 3.3.3 + echarts: 6.0.0 + echarts-for-react: 3.0.6 + elkjs: 0.11.1 + embla-carousel-autoplay: 8.6.0 + embla-carousel-react: 8.6.0 + emoji-mart: 5.6.0 + es-toolkit: 1.45.1 + eslint: 10.1.0 + eslint-markdown: 0.6.0 + eslint-plugin-better-tailwindcss: 4.3.2 + eslint-plugin-hyoban: 0.14.1 + eslint-plugin-markdown-preferences: 0.40.3 + eslint-plugin-no-barrel-files: 1.2.2 + eslint-plugin-react-hooks: 7.0.1 + eslint-plugin-react-refresh: 0.5.2 + eslint-plugin-sonarjs: 4.0.2 + eslint-plugin-storybook: 10.3.3 + fast-deep-equal: 3.1.3 + foxact: 0.3.0 + happy-dom: 20.8.9 + hono: 4.12.9 + html-entities: 2.6.0 + html-to-image: 1.11.13 + husky: 9.1.7 + i18next: 25.10.10 + i18next-resources-to-backend: 1.2.1 + iconify-import-svg: 0.1.2 + immer: 11.1.4 + jotai: 2.19.0 + js-audio-recorder: 1.0.7 + js-cookie: 3.0.5 + js-yaml: 4.1.1 + jsonschema: 1.5.0 + katex: 0.16.44 + knip: 6.1.0 + ky: 1.14.3 + lamejs: 1.2.1 + lexical: 0.42.0 + lint-staged: 16.4.0 + mermaid: 11.13.0 + mime: 4.1.0 + mitt: 3.0.1 + negotiator: 1.0.0 + next: 16.2.1 + next-themes: 0.4.6 + nuqs: 2.8.9 + pinyin-pro: 3.28.0 + postcss: 8.5.8 + postcss-js: 5.1.0 + qrcode.react: 4.2.0 + qs: 6.15.0 + react: 19.2.4 + react-18-input-autosize: 3.0.0 + react-dom: 19.2.4 + react-easy-crop: 5.5.7 + react-hotkeys-hook: 5.2.4 + react-i18next: 16.6.6 + react-multi-email: 1.0.25 + react-papaparse: 4.4.0 + react-pdf-highlighter: 8.0.0-rc.0 + react-server-dom-webpack: 19.2.4 + react-sortablejs: 6.1.4 + react-syntax-highlighter: 15.6.6 + react-textarea-autosize: 8.5.9 + react-window: 1.8.11 + reactflow: 11.11.4 + remark-breaks: 4.0.0 + remark-directive: 4.0.0 + sass: 1.98.0 + scheduler: 0.27.0 + sharp: 0.34.5 + sortablejs: 1.15.7 + std-semver: 1.0.8 + storybook: 10.3.3 + streamdown: 2.5.0 + string-ts: 2.3.1 + tailwind-merge: 2.6.1 + tailwindcss: 3.4.19 + taze: 19.10.0 + tldts: 7.0.27 + tsup: ^8.5.1 + tsx: 4.21.0 + typescript: 5.9.3 + uglify-js: 3.19.3 + unist-util-visit: 5.1.0 + use-context-selector: 2.0.0 + uuid: 13.0.0 + vinext: 0.0.38 + vite: npm:@voidzero-dev/vite-plus-core@0.1.14 + vite-plugin-inspect: 12.0.0-beta.1 + vite-plus: 0.1.14 + vitest: npm:@voidzero-dev/vite-plus-test@0.1.14 + vitest-canvas-mock: 1.1.4 + zod: 4.3.6 + zundo: 2.3.0 + zustand: 5.0.12 diff --git a/sdks/nodejs-client/README.md b/sdks/nodejs-client/README.md index f8c2803c08..7051bbc788 100644 --- a/sdks/nodejs-client/README.md +++ b/sdks/nodejs-client/README.md @@ -100,6 +100,10 @@ Notes: - Chat/completion require a stable `user` identifier in the request payload. - For streaming responses, iterate the returned AsyncIterable. Use `stream.toText()` to collect text. +## Maintainers + +This package is published from the repository workspace. Install dependencies from the repository root with `pnpm install`, then use `./scripts/publish.sh` for dry runs and publishing so `catalog:` dependencies are resolved before release. + ## License This SDK is released under the MIT License. diff --git a/sdks/nodejs-client/package.json b/sdks/nodejs-client/package.json index 7168d33c24..63fa6799b1 100644 --- a/sdks/nodejs-client/package.json +++ b/sdks/nodejs-client/package.json @@ -54,24 +54,17 @@ "publish:npm": "./scripts/publish.sh" }, "dependencies": { - "axios": "^1.13.6" + "axios": "catalog:" }, "devDependencies": { - "@eslint/js": "^10.0.1", - "@types/node": "^25.4.0", - "@typescript-eslint/eslint-plugin": "^8.57.0", - "@typescript-eslint/parser": "^8.57.0", - "@vitest/coverage-v8": "4.0.18", - "eslint": "^10.0.3", - "tsup": "^8.5.1", - "typescript": "^5.9.3", - "vitest": "^4.0.18" - }, - "pnpm": { - "overrides": { - "flatted@<=3.4.1": "3.4.2", - "picomatch@>=4.0.0 <4.0.4": "4.0.4", - "rollup@>=4.0.0 <4.59.0": "4.59.0" - } + "@eslint/js": "catalog:", + "@types/node": "catalog:", + "@typescript-eslint/eslint-plugin": "catalog:", + "@typescript-eslint/parser": "catalog:", + "@vitest/coverage-v8": "catalog:", + "eslint": "catalog:", + "tsup": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/sdks/nodejs-client/pnpm-lock.yaml b/sdks/nodejs-client/pnpm-lock.yaml deleted file mode 100644 index 30d3cf61ee..0000000000 --- a/sdks/nodejs-client/pnpm-lock.yaml +++ /dev/null @@ -1,2255 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -overrides: - flatted@<=3.4.1: 3.4.2 - picomatch@>=4.0.0 <4.0.4: 4.0.4 - rollup@>=4.0.0 <4.59.0: 4.59.0 - -importers: - - .: - dependencies: - axios: - specifier: ^1.13.6 - version: 1.13.6 - devDependencies: - '@eslint/js': - specifier: ^10.0.1 - version: 10.0.1(eslint@10.0.3) - '@types/node': - specifier: ^25.4.0 - version: 25.4.0 - '@typescript-eslint/eslint-plugin': - specifier: ^8.57.0 - version: 8.57.0(@typescript-eslint/parser@8.57.0(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^8.57.0 - version: 8.57.0(eslint@10.0.3)(typescript@5.9.3) - '@vitest/coverage-v8': - specifier: 4.0.18 - version: 4.0.18(vitest@4.0.18(@types/node@25.4.0)) - eslint: - specifier: ^10.0.3 - version: 10.0.3 - tsup: - specifier: ^8.5.1 - version: 8.5.1(postcss@8.5.8)(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - vitest: - specifier: ^4.0.18 - version: 4.0.18(@types/node@25.4.0) - -packages: - - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.29.0': - resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} - engines: {node: '>=6.9.0'} - - '@bcoe/v8-coverage@1.0.2': - resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} - engines: {node: '>=18'} - - '@esbuild/aix-ppc64@0.27.3': - resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.27.3': - resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.27.3': - resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.27.3': - resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.27.3': - resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.3': - resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.27.3': - resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.3': - resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.27.3': - resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.27.3': - resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.27.3': - resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.27.3': - resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.27.3': - resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.27.3': - resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.3': - resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.27.3': - resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.27.3': - resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.27.3': - resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.3': - resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.27.3': - resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.3': - resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.27.3': - resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.27.3': - resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.27.3': - resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.27.3': - resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.27.3': - resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.2': - resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/config-array@0.23.3': - resolution: {integrity: sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/config-helpers@0.5.3': - resolution: {integrity: sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/core@1.1.1': - resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/js@10.0.1': - resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - peerDependencies: - eslint: ^10.0.0 - peerDependenciesMeta: - eslint: - optional: true - - '@eslint/object-schema@3.0.3': - resolution: {integrity: sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/plugin-kit@0.6.1': - resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} - engines: {node: '>=18.18.0'} - - '@humanfs/node@0.16.7': - resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} - engines: {node: '>=18.18.0'} - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} - engines: {node: '>=18.18'} - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - - '@rollup/rollup-android-arm-eabi@4.59.0': - resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.59.0': - resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.59.0': - resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.59.0': - resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.59.0': - resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.59.0': - resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': - resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.59.0': - resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.59.0': - resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.59.0': - resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-loong64-gnu@4.59.0': - resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-loong64-musl@4.59.0': - resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-ppc64-gnu@4.59.0': - resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-ppc64-musl@4.59.0': - resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.59.0': - resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-riscv64-musl@4.59.0': - resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.59.0': - resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.59.0': - resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.59.0': - resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-openbsd-x64@4.59.0': - resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.59.0': - resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.59.0': - resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.59.0': - resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.59.0': - resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.59.0': - resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} - cpu: [x64] - os: [win32] - - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - - '@types/esrecurse@4.3.1': - resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/node@25.4.0': - resolution: {integrity: sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==} - - '@typescript-eslint/eslint-plugin@8.57.0': - resolution: {integrity: sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.57.0 - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/parser@8.57.0': - resolution: {integrity: sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/project-service@8.57.0': - resolution: {integrity: sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/scope-manager@8.57.0': - resolution: {integrity: sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.57.0': - resolution: {integrity: sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/type-utils@8.57.0': - resolution: {integrity: sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/types@8.57.0': - resolution: {integrity: sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.57.0': - resolution: {integrity: sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/utils@8.57.0': - resolution: {integrity: sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/visitor-keys@8.57.0': - resolution: {integrity: sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@vitest/coverage-v8@4.0.18': - resolution: {integrity: sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==} - peerDependencies: - '@vitest/browser': 4.0.18 - vitest: 4.0.18 - peerDependenciesMeta: - '@vitest/browser': - optional: true - - '@vitest/expect@4.0.18': - resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} - - '@vitest/mocker@4.0.18': - resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} - peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/pretty-format@4.0.18': - resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} - - '@vitest/runner@4.0.18': - resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} - - '@vitest/snapshot@4.0.18': - resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} - - '@vitest/spy@4.0.18': - resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} - - '@vitest/utils@4.0.18': - resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} - hasBin: true - - ajv@6.14.0: - resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} - - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - - ast-v8-to-istanbul@0.3.12: - resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==} - - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - - axios@1.13.6: - resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} - - balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} - - brace-expansion@5.0.4: - resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} - engines: {node: 18 || 20 || >=22} - - bundle-require@5.1.0: - resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - peerDependencies: - esbuild: '>=0.18' - - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - chai@6.2.2: - resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} - engines: {node: '>=18'} - - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} - - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - - consola@3.4.2: - resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} - engines: {node: ^14.18.0 || >=16.10.0} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - - esbuild@0.27.3: - resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} - engines: {node: '>=18'} - hasBin: true - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-scope@9.1.2: - resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@5.0.1: - resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - eslint@10.0.3: - resolution: {integrity: sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - - espree@11.2.0: - resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - esquery@1.7.0: - resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - expect-type@1.3.0: - resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} - engines: {node: '>=12.0.0'} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: 4.0.4 - peerDependenciesMeta: - picomatch: - optional: true - - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - fix-dts-default-cjs-exports@1.0.1: - resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} - - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} - - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - - follow-redirects@1.15.11: - resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} - engines: {node: '>= 6'} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} - engines: {node: '>= 4'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - istanbul-lib-coverage@3.2.2: - resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} - engines: {node: '>=8'} - - istanbul-lib-report@3.0.1: - resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} - engines: {node: '>=10'} - - istanbul-reports@3.2.0: - resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} - engines: {node: '>=8'} - - joycon@3.1.1: - resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} - engines: {node: '>=10'} - - js-tokens@10.0.0: - resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - lilconfig@3.1.3: - resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} - engines: {node: '>=14'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - load-tsconfig@0.2.5: - resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - magicast@0.5.2: - resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} - - make-dir@4.0.0: - resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} - engines: {node: '>=10'} - - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - - minimatch@10.2.4: - resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} - engines: {node: 18 || 20 || >=22} - - mlly@1.8.1: - resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - obug@2.1.1: - resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - - pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} - - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - - postcss-load-config@6.0.1: - resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} - engines: {node: '>= 18'} - peerDependencies: - jiti: '>=1.21.0' - postcss: '>=8.0.9' - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - jiti: - optional: true - postcss: - optional: true - tsx: - optional: true - yaml: - optional: true - - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} - engines: {node: ^10 || ^12 || >=14} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} - - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - - rollup@4.59.0: - resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} - engines: {node: '>=10'} - hasBin: true - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - source-map@0.7.6: - resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} - engines: {node: '>= 12'} - - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - - sucrase@3.35.1: - resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - - thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} - engines: {node: '>=18'} - - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} - - tinyrainbow@3.0.3: - resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} - engines: {node: '>=14.0.0'} - - tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - - ts-api-utils@2.4.0: - resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' - - ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - - tsup@8.5.1: - resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - '@microsoft/api-extractor': ^7.36.0 - '@swc/core': ^1 - postcss: ^8.4.12 - typescript: '>=4.5.0' - peerDependenciesMeta: - '@microsoft/api-extractor': - optional: true - '@swc/core': - optional: true - postcss: - optional: true - typescript: - optional: true - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - - ufo@1.6.3: - resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} - - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - jiti: '>=1.21.0' - less: ^4.0.0 - lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitest@4.0.18: - resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.18 - '@vitest/browser-preview': 4.0.18 - '@vitest/browser-webdriverio': 4.0.18 - '@vitest/ui': 4.0.18 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@opentelemetry/api': - optional: true - '@types/node': - optional: true - '@vitest/browser-playwright': - optional: true - '@vitest/browser-preview': - optional: true - '@vitest/browser-webdriverio': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - -snapshots: - - '@babel/helper-string-parser@7.27.1': {} - - '@babel/helper-validator-identifier@7.28.5': {} - - '@babel/parser@7.29.0': - dependencies: - '@babel/types': 7.29.0 - - '@babel/types@7.29.0': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - - '@bcoe/v8-coverage@1.0.2': {} - - '@esbuild/aix-ppc64@0.27.3': - optional: true - - '@esbuild/android-arm64@0.27.3': - optional: true - - '@esbuild/android-arm@0.27.3': - optional: true - - '@esbuild/android-x64@0.27.3': - optional: true - - '@esbuild/darwin-arm64@0.27.3': - optional: true - - '@esbuild/darwin-x64@0.27.3': - optional: true - - '@esbuild/freebsd-arm64@0.27.3': - optional: true - - '@esbuild/freebsd-x64@0.27.3': - optional: true - - '@esbuild/linux-arm64@0.27.3': - optional: true - - '@esbuild/linux-arm@0.27.3': - optional: true - - '@esbuild/linux-ia32@0.27.3': - optional: true - - '@esbuild/linux-loong64@0.27.3': - optional: true - - '@esbuild/linux-mips64el@0.27.3': - optional: true - - '@esbuild/linux-ppc64@0.27.3': - optional: true - - '@esbuild/linux-riscv64@0.27.3': - optional: true - - '@esbuild/linux-s390x@0.27.3': - optional: true - - '@esbuild/linux-x64@0.27.3': - optional: true - - '@esbuild/netbsd-arm64@0.27.3': - optional: true - - '@esbuild/netbsd-x64@0.27.3': - optional: true - - '@esbuild/openbsd-arm64@0.27.3': - optional: true - - '@esbuild/openbsd-x64@0.27.3': - optional: true - - '@esbuild/openharmony-arm64@0.27.3': - optional: true - - '@esbuild/sunos-x64@0.27.3': - optional: true - - '@esbuild/win32-arm64@0.27.3': - optional: true - - '@esbuild/win32-ia32@0.27.3': - optional: true - - '@esbuild/win32-x64@0.27.3': - optional: true - - '@eslint-community/eslint-utils@4.9.1(eslint@10.0.3)': - dependencies: - eslint: 10.0.3 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.2': {} - - '@eslint/config-array@0.23.3': - dependencies: - '@eslint/object-schema': 3.0.3 - debug: 4.4.3 - minimatch: 10.2.4 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.5.3': - dependencies: - '@eslint/core': 1.1.1 - - '@eslint/core@1.1.1': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/js@10.0.1(eslint@10.0.3)': - optionalDependencies: - eslint: 10.0.3 - - '@eslint/object-schema@3.0.3': {} - - '@eslint/plugin-kit@0.6.1': - dependencies: - '@eslint/core': 1.1.1 - levn: 0.4.1 - - '@humanfs/core@0.19.1': {} - - '@humanfs/node@0.16.7': - dependencies: - '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.4.3 - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.4.3': {} - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@rollup/rollup-android-arm-eabi@4.59.0': - optional: true - - '@rollup/rollup-android-arm64@4.59.0': - optional: true - - '@rollup/rollup-darwin-arm64@4.59.0': - optional: true - - '@rollup/rollup-darwin-x64@4.59.0': - optional: true - - '@rollup/rollup-freebsd-arm64@4.59.0': - optional: true - - '@rollup/rollup-freebsd-x64@4.59.0': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.59.0': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.59.0': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.59.0': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.59.0': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.59.0': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-x64-musl@4.59.0': - optional: true - - '@rollup/rollup-openbsd-x64@4.59.0': - optional: true - - '@rollup/rollup-openharmony-arm64@4.59.0': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.59.0': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.59.0': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.59.0': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.59.0': - optional: true - - '@standard-schema/spec@1.1.0': {} - - '@types/chai@5.2.3': - dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 - - '@types/deep-eql@4.0.2': {} - - '@types/esrecurse@4.3.1': {} - - '@types/estree@1.0.8': {} - - '@types/json-schema@7.0.15': {} - - '@types/node@25.4.0': - dependencies: - undici-types: 7.18.2 - - '@typescript-eslint/eslint-plugin@8.57.0(@typescript-eslint/parser@8.57.0(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3)': - dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.57.0(eslint@10.0.3)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.0 - '@typescript-eslint/type-utils': 8.57.0(eslint@10.0.3)(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.0(eslint@10.0.3)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.0 - eslint: 10.0.3 - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.57.0(eslint@10.0.3)(typescript@5.9.3)': - dependencies: - '@typescript-eslint/scope-manager': 8.57.0 - '@typescript-eslint/types': 8.57.0 - '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.0 - debug: 4.4.3 - eslint: 10.0.3 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.57.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.57.0(typescript@5.9.3) - '@typescript-eslint/types': 8.57.0 - debug: 4.4.3 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@8.57.0': - dependencies: - '@typescript-eslint/types': 8.57.0 - '@typescript-eslint/visitor-keys': 8.57.0 - - '@typescript-eslint/tsconfig-utils@8.57.0(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@typescript-eslint/type-utils@8.57.0(eslint@10.0.3)(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 8.57.0 - '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.0(eslint@10.0.3)(typescript@5.9.3) - debug: 4.4.3 - eslint: 10.0.3 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@8.57.0': {} - - '@typescript-eslint/typescript-estree@8.57.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.57.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.57.0(typescript@5.9.3) - '@typescript-eslint/types': 8.57.0 - '@typescript-eslint/visitor-keys': 8.57.0 - debug: 4.4.3 - minimatch: 10.2.4 - semver: 7.7.4 - tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.57.0(eslint@10.0.3)(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3) - '@typescript-eslint/scope-manager': 8.57.0 - '@typescript-eslint/types': 8.57.0 - '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3) - eslint: 10.0.3 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/visitor-keys@8.57.0': - dependencies: - '@typescript-eslint/types': 8.57.0 - eslint-visitor-keys: 5.0.1 - - '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@25.4.0))': - dependencies: - '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.0.18 - ast-v8-to-istanbul: 0.3.12 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-reports: 3.2.0 - magicast: 0.5.2 - obug: 2.1.1 - std-env: 3.10.0 - tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@25.4.0) - - '@vitest/expect@4.0.18': - dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 - chai: 6.2.2 - tinyrainbow: 3.0.3 - - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.4.0))': - dependencies: - '@vitest/spy': 4.0.18 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.3.1(@types/node@25.4.0) - - '@vitest/pretty-format@4.0.18': - dependencies: - tinyrainbow: 3.0.3 - - '@vitest/runner@4.0.18': - dependencies: - '@vitest/utils': 4.0.18 - pathe: 2.0.3 - - '@vitest/snapshot@4.0.18': - dependencies: - '@vitest/pretty-format': 4.0.18 - magic-string: 0.30.21 - pathe: 2.0.3 - - '@vitest/spy@4.0.18': {} - - '@vitest/utils@4.0.18': - dependencies: - '@vitest/pretty-format': 4.0.18 - tinyrainbow: 3.0.3 - - acorn-jsx@5.3.2(acorn@8.16.0): - dependencies: - acorn: 8.16.0 - - acorn@8.16.0: {} - - ajv@6.14.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - any-promise@1.3.0: {} - - assertion-error@2.0.1: {} - - ast-v8-to-istanbul@0.3.12: - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - estree-walker: 3.0.3 - js-tokens: 10.0.0 - - asynckit@0.4.0: {} - - axios@1.13.6: - dependencies: - follow-redirects: 1.15.11 - form-data: 4.0.5 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - - balanced-match@4.0.4: {} - - brace-expansion@5.0.4: - dependencies: - balanced-match: 4.0.4 - - bundle-require@5.1.0(esbuild@0.27.3): - dependencies: - esbuild: 0.27.3 - load-tsconfig: 0.2.5 - - cac@6.7.14: {} - - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - chai@6.2.2: {} - - chokidar@4.0.3: - dependencies: - readdirp: 4.1.2 - - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - - commander@4.1.1: {} - - confbox@0.1.8: {} - - consola@3.4.2: {} - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - deep-is@0.1.4: {} - - delayed-stream@1.0.0: {} - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - - es-module-lexer@1.7.0: {} - - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - - esbuild@0.27.3: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.3 - '@esbuild/android-arm': 0.27.3 - '@esbuild/android-arm64': 0.27.3 - '@esbuild/android-x64': 0.27.3 - '@esbuild/darwin-arm64': 0.27.3 - '@esbuild/darwin-x64': 0.27.3 - '@esbuild/freebsd-arm64': 0.27.3 - '@esbuild/freebsd-x64': 0.27.3 - '@esbuild/linux-arm': 0.27.3 - '@esbuild/linux-arm64': 0.27.3 - '@esbuild/linux-ia32': 0.27.3 - '@esbuild/linux-loong64': 0.27.3 - '@esbuild/linux-mips64el': 0.27.3 - '@esbuild/linux-ppc64': 0.27.3 - '@esbuild/linux-riscv64': 0.27.3 - '@esbuild/linux-s390x': 0.27.3 - '@esbuild/linux-x64': 0.27.3 - '@esbuild/netbsd-arm64': 0.27.3 - '@esbuild/netbsd-x64': 0.27.3 - '@esbuild/openbsd-arm64': 0.27.3 - '@esbuild/openbsd-x64': 0.27.3 - '@esbuild/openharmony-arm64': 0.27.3 - '@esbuild/sunos-x64': 0.27.3 - '@esbuild/win32-arm64': 0.27.3 - '@esbuild/win32-ia32': 0.27.3 - '@esbuild/win32-x64': 0.27.3 - - escape-string-regexp@4.0.0: {} - - eslint-scope@9.1.2: - dependencies: - '@types/esrecurse': 4.3.1 - '@types/estree': 1.0.8 - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@5.0.1: {} - - eslint@10.0.3: - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.3 - '@eslint/config-helpers': 0.5.3 - '@eslint/core': 1.1.1 - '@eslint/plugin-kit': 0.6.1 - '@humanfs/node': 0.16.7 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 - ajv: 6.14.0 - cross-spawn: 7.0.6 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - eslint-scope: 9.1.2 - eslint-visitor-keys: 5.0.1 - espree: 11.2.0 - esquery: 1.7.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.4 - natural-compare: 1.4.0 - optionator: 0.9.4 - transitivePeerDependencies: - - supports-color - - espree@11.2.0: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 5.0.1 - - esquery@1.7.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.8 - - esutils@2.0.3: {} - - expect-type@1.3.0: {} - - fast-deep-equal@3.1.3: {} - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fdir@6.5.0(picomatch@4.0.4): - optionalDependencies: - picomatch: 4.0.4 - - file-entry-cache@8.0.0: - dependencies: - flat-cache: 4.0.1 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - fix-dts-default-cjs-exports@1.0.1: - dependencies: - magic-string: 0.30.21 - mlly: 1.8.1 - rollup: 4.59.0 - - flat-cache@4.0.1: - dependencies: - flatted: 3.4.2 - keyv: 4.5.4 - - flatted@3.4.2: {} - - follow-redirects@1.15.11: {} - - form-data@4.0.5: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.2 - mime-types: 2.1.35 - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - gopd@1.2.0: {} - - has-flag@4.0.0: {} - - has-symbols@1.1.0: {} - - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - html-escaper@2.0.2: {} - - ignore@5.3.2: {} - - ignore@7.0.5: {} - - imurmurhash@0.1.4: {} - - is-extglob@2.1.1: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - isexe@2.0.0: {} - - istanbul-lib-coverage@3.2.2: {} - - istanbul-lib-report@3.0.1: - dependencies: - istanbul-lib-coverage: 3.2.2 - make-dir: 4.0.0 - supports-color: 7.2.0 - - istanbul-reports@3.2.0: - dependencies: - html-escaper: 2.0.2 - istanbul-lib-report: 3.0.1 - - joycon@3.1.1: {} - - js-tokens@10.0.0: {} - - json-buffer@3.0.1: {} - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - lilconfig@3.1.3: {} - - lines-and-columns@1.2.4: {} - - load-tsconfig@0.2.5: {} - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - magicast@0.5.2: - dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - source-map-js: 1.2.1 - - make-dir@4.0.0: - dependencies: - semver: 7.7.4 - - math-intrinsics@1.1.0: {} - - mime-db@1.52.0: {} - - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - - minimatch@10.2.4: - dependencies: - brace-expansion: 5.0.4 - - mlly@1.8.1: - dependencies: - acorn: 8.16.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.3 - - ms@2.1.3: {} - - mz@2.7.0: - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 - - nanoid@3.3.11: {} - - natural-compare@1.4.0: {} - - object-assign@4.1.1: {} - - obug@2.1.1: {} - - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - path-exists@4.0.0: {} - - path-key@3.1.1: {} - - pathe@2.0.3: {} - - picocolors@1.1.1: {} - - picomatch@4.0.4: {} - - pirates@4.0.7: {} - - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.8.1 - pathe: 2.0.3 - - postcss-load-config@6.0.1(postcss@8.5.8): - dependencies: - lilconfig: 3.1.3 - optionalDependencies: - postcss: 8.5.8 - - postcss@8.5.8: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - prelude-ls@1.2.1: {} - - proxy-from-env@1.1.0: {} - - punycode@2.3.1: {} - - readdirp@4.1.2: {} - - resolve-from@5.0.0: {} - - rollup@4.59.0: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.59.0 - '@rollup/rollup-android-arm64': 4.59.0 - '@rollup/rollup-darwin-arm64': 4.59.0 - '@rollup/rollup-darwin-x64': 4.59.0 - '@rollup/rollup-freebsd-arm64': 4.59.0 - '@rollup/rollup-freebsd-x64': 4.59.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 - '@rollup/rollup-linux-arm-musleabihf': 4.59.0 - '@rollup/rollup-linux-arm64-gnu': 4.59.0 - '@rollup/rollup-linux-arm64-musl': 4.59.0 - '@rollup/rollup-linux-loong64-gnu': 4.59.0 - '@rollup/rollup-linux-loong64-musl': 4.59.0 - '@rollup/rollup-linux-ppc64-gnu': 4.59.0 - '@rollup/rollup-linux-ppc64-musl': 4.59.0 - '@rollup/rollup-linux-riscv64-gnu': 4.59.0 - '@rollup/rollup-linux-riscv64-musl': 4.59.0 - '@rollup/rollup-linux-s390x-gnu': 4.59.0 - '@rollup/rollup-linux-x64-gnu': 4.59.0 - '@rollup/rollup-linux-x64-musl': 4.59.0 - '@rollup/rollup-openbsd-x64': 4.59.0 - '@rollup/rollup-openharmony-arm64': 4.59.0 - '@rollup/rollup-win32-arm64-msvc': 4.59.0 - '@rollup/rollup-win32-ia32-msvc': 4.59.0 - '@rollup/rollup-win32-x64-gnu': 4.59.0 - '@rollup/rollup-win32-x64-msvc': 4.59.0 - fsevents: 2.3.3 - - semver@7.7.4: {} - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - siginfo@2.0.0: {} - - source-map-js@1.2.1: {} - - source-map@0.7.6: {} - - stackback@0.0.2: {} - - std-env@3.10.0: {} - - sucrase@3.35.1: - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - commander: 4.1.1 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.7 - tinyglobby: 0.2.15 - ts-interface-checker: 0.1.13 - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - thenify-all@1.6.0: - dependencies: - thenify: 3.3.1 - - thenify@3.3.1: - dependencies: - any-promise: 1.3.0 - - tinybench@2.9.0: {} - - tinyexec@0.3.2: {} - - tinyexec@1.0.2: {} - - tinyglobby@0.2.15: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - - tinyrainbow@3.0.3: {} - - tree-kill@1.2.2: {} - - ts-api-utils@2.4.0(typescript@5.9.3): - dependencies: - typescript: 5.9.3 - - ts-interface-checker@0.1.13: {} - - tsup@8.5.1(postcss@8.5.8)(typescript@5.9.3): - dependencies: - bundle-require: 5.1.0(esbuild@0.27.3) - cac: 6.7.14 - chokidar: 4.0.3 - consola: 3.4.2 - debug: 4.4.3 - esbuild: 0.27.3 - fix-dts-default-cjs-exports: 1.0.1 - joycon: 3.1.1 - picocolors: 1.1.1 - postcss-load-config: 6.0.1(postcss@8.5.8) - resolve-from: 5.0.0 - rollup: 4.59.0 - source-map: 0.7.6 - sucrase: 3.35.1 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tree-kill: 1.2.2 - optionalDependencies: - postcss: 8.5.8 - typescript: 5.9.3 - transitivePeerDependencies: - - jiti - - supports-color - - tsx - - yaml - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - typescript@5.9.3: {} - - ufo@1.6.3: {} - - undici-types@7.18.2: {} - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - vite@7.3.1(@types/node@25.4.0): - dependencies: - esbuild: 0.27.3 - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - postcss: 8.5.8 - rollup: 4.59.0 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 25.4.0 - fsevents: 2.3.3 - - vitest@4.0.18(@types/node@25.4.0): - dependencies: - '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.4.0)) - '@vitest/pretty-format': 4.0.18 - '@vitest/runner': 4.0.18 - '@vitest/snapshot': 4.0.18 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 - es-module-lexer: 1.7.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 3.10.0 - tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.4.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 25.4.0 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - - word-wrap@1.2.5: {} - - yocto-queue@0.1.0: {} diff --git a/sdks/nodejs-client/pnpm-workspace.yaml b/sdks/nodejs-client/pnpm-workspace.yaml deleted file mode 100644 index efc037aa84..0000000000 --- a/sdks/nodejs-client/pnpm-workspace.yaml +++ /dev/null @@ -1,2 +0,0 @@ -onlyBuiltDependencies: - - esbuild diff --git a/sdks/nodejs-client/scripts/publish.sh b/sdks/nodejs-client/scripts/publish.sh index 043cac046d..5f8e73f8c0 100755 --- a/sdks/nodejs-client/scripts/publish.sh +++ b/sdks/nodejs-client/scripts/publish.sh @@ -5,10 +5,12 @@ # A beautiful and reliable script to publish the SDK to npm # # Usage: -# ./scripts/publish.sh # Normal publish +# ./scripts/publish.sh # Normal publish # ./scripts/publish.sh --dry-run # Test without publishing # ./scripts/publish.sh --skip-tests # Skip tests (not recommended) # +# This script requires pnpm because the workspace uses catalog: dependencies. +# set -euo pipefail @@ -62,11 +64,27 @@ divider() { echo -e "${DIM}─────────────────────────────────────────────────────────────────${NC}" } +run_npm() { + env \ + -u npm_config_npm_globalconfig \ + -u NPM_CONFIG_NPM_GLOBALCONFIG \ + -u npm_config_verify_deps_before_run \ + -u NPM_CONFIG_VERIFY_DEPS_BEFORE_RUN \ + -u npm_config__jsr_registry \ + -u NPM_CONFIG__JSR_REGISTRY \ + -u npm_config_catalog \ + -u NPM_CONFIG_CATALOG \ + -u npm_config_overrides \ + -u NPM_CONFIG_OVERRIDES \ + npm "$@" +} + # ============================================================================ # Configuration # ============================================================================ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || (cd "$SCRIPT_DIR/../../.." && pwd))" DRY_RUN=false SKIP_TESTS=false @@ -123,23 +141,23 @@ main() { error "npm is not installed" exit 1 fi - NPM_VERSION=$(npm -v) + NPM_VERSION=$(run_npm -v) success "npm: v$NPM_VERSION" - # Check pnpm (optional, for local dev) - if command -v pnpm &> /dev/null; then - PNPM_VERSION=$(pnpm -v) - success "pnpm: v$PNPM_VERSION" - else - info "pnpm not found (optional)" + if ! command -v pnpm &> /dev/null; then + error "pnpm is required because this workspace publishes catalog: dependencies" + info "Install pnpm with Corepack: corepack enable" + exit 1 fi + PNPM_VERSION=$(pnpm -v) + success "pnpm: v$PNPM_VERSION" # Check npm login status - if ! npm whoami &> /dev/null; then + if ! run_npm whoami &> /dev/null; then error "Not logged in to npm. Run 'npm login' first." exit 1 fi - NPM_USER=$(npm whoami) + NPM_USER=$(run_npm whoami) success "Logged in as: ${BOLD}$NPM_USER${NC}" # ======================================================================== @@ -154,11 +172,11 @@ main() { success "Version: ${BOLD}$PACKAGE_VERSION${NC}" # Check if version already exists on npm - if npm view "$PACKAGE_NAME@$PACKAGE_VERSION" version &> /dev/null; then + if run_npm view "$PACKAGE_NAME@$PACKAGE_VERSION" version &> /dev/null; then error "Version $PACKAGE_VERSION already exists on npm!" echo "" info "Current published versions:" - npm view "$PACKAGE_NAME" versions --json 2>/dev/null | tail -5 + run_npm view "$PACKAGE_NAME" versions --json 2>/dev/null | tail -5 echo "" warning "Please update the version in package.json before publishing." exit 1 @@ -170,11 +188,7 @@ main() { # ======================================================================== step "Step 3/6: Installing dependencies..." - if command -v pnpm &> /dev/null; then - pnpm install --frozen-lockfile 2>/dev/null || pnpm install - else - npm ci 2>/dev/null || npm install - fi + pnpm --dir "$REPO_ROOT" install --frozen-lockfile 2>/dev/null || pnpm --dir "$REPO_ROOT" install success "Dependencies installed" # ======================================================================== @@ -185,11 +199,7 @@ main() { if [[ "$SKIP_TESTS" == true ]]; then warning "Skipping tests (--skip-tests flag)" else - if command -v pnpm &> /dev/null; then - pnpm test - else - npm test - fi + pnpm test success "All tests passed" fi @@ -201,11 +211,7 @@ main() { # Clean previous build rm -rf dist - if command -v pnpm &> /dev/null; then - pnpm run build - else - npm run build - fi + pnpm run build success "Build completed" # Verify build output @@ -223,15 +229,32 @@ main() { # Step 6: Publish # ======================================================================== step "Step 6/6: Publishing to npm..." - + + PACK_DIR="$(mktemp -d)" + trap 'rm -rf "$PACK_DIR"' EXIT + + pnpm pack --pack-destination "$PACK_DIR" >/dev/null + PACKAGE_TARBALL="$(find "$PACK_DIR" -maxdepth 1 -name '*.tgz' | head -n 1)" + + if [[ -z "$PACKAGE_TARBALL" ]]; then + error "Pack failed - no tarball generated" + exit 1 + fi + + if tar -xOf "$PACKAGE_TARBALL" package/package.json | grep -q '"catalog:'; then + error "Packed manifest still contains catalog: references" + exit 1 + fi + divider echo -e "${CYAN}Package contents:${NC}" - npm pack --dry-run 2>&1 | head -30 + tar -tzf "$PACKAGE_TARBALL" | head -30 divider if [[ "$DRY_RUN" == true ]]; then warning "DRY-RUN: Skipping actual publish" echo "" + info "Packed artifact: $PACKAGE_TARBALL" info "To publish for real, run without --dry-run flag" else echo "" @@ -239,7 +262,7 @@ main() { echo -e "${DIM}Press Enter to continue, or Ctrl+C to cancel...${NC}" read -r - npm publish --access public + pnpm publish --access public --no-git-checks echo "" success "🎉 Successfully published ${BOLD}$PACKAGE_NAME@$PACKAGE_VERSION${NC} to npm!" diff --git a/web/taze.config.js b/taze.config.js similarity index 95% rename from web/taze.config.js rename to taze.config.js index 7ffd76f94e..d21756e207 100644 --- a/web/taze.config.js +++ b/taze.config.js @@ -10,6 +10,7 @@ export default defineConfig({ // We can not upgrade these yet 'tailwind-merge', 'tailwindcss', + 'typescript', ], write: true, diff --git a/web/.dockerignore b/web/.dockerignore deleted file mode 100644 index 91437a2259..0000000000 --- a/web/.dockerignore +++ /dev/null @@ -1,32 +0,0 @@ -.env -.env.* - -# Logs -logs -*.log* - -# node -node_modules -dist -build -coverage -.husky -.next -.pnpm-store - -# vscode -.vscode - -# webstorm -.idea -*.iml -*.iws -*.ipr - - -# Jetbrains -.idea - -# git -.git -.gitignore \ No newline at end of file diff --git a/web/Dockerfile b/web/Dockerfile index b54bae706c..75024db4f3 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -19,21 +19,27 @@ ENV NEXT_PUBLIC_BASE_PATH="$NEXT_PUBLIC_BASE_PATH" # install packages FROM base AS packages -WORKDIR /app/web +WORKDIR /app -COPY package.json pnpm-lock.yaml /app/web/ +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml /app/ +COPY web/package.json /app/web/ +COPY e2e/package.json /app/e2e/ +COPY sdks/nodejs-client/package.json /app/sdks/nodejs-client/ # Use packageManager from package.json RUN corepack install -RUN pnpm install --frozen-lockfile +# Install only the web workspace to keep image builds from pulling in +# unrelated workspace dependencies such as e2e tooling. +RUN pnpm install --filter ./web... --frozen-lockfile # build resources FROM base AS builder -WORKDIR /app/web -COPY --from=packages /app/web/ . +WORKDIR /app +COPY --from=packages /app/ . COPY . . +WORKDIR /app/web ENV NODE_OPTIONS="--max-old-space-size=4096" RUN pnpm build @@ -64,13 +70,13 @@ RUN addgroup -S -g ${dify_uid} dify && \ chown -R dify:dify /app -WORKDIR /app/web +WORKDIR /app -COPY --from=builder --chown=dify:dify /app/web/public ./public +COPY --from=builder --chown=dify:dify /app/web/public ./web/public COPY --from=builder --chown=dify:dify /app/web/.next/standalone ./ -COPY --from=builder --chown=dify:dify /app/web/.next/static ./.next/static +COPY --from=builder --chown=dify:dify /app/web/.next/static ./web/.next/static -COPY --chown=dify:dify --chmod=755 docker/entrypoint.sh ./entrypoint.sh +COPY --chown=dify:dify --chmod=755 web/docker/entrypoint.sh ./entrypoint.sh ARG COMMIT_SHA ENV COMMIT_SHA=${COMMIT_SHA} diff --git a/web/Dockerfile.dockerignore b/web/Dockerfile.dockerignore new file mode 100644 index 0000000000..9801003d89 --- /dev/null +++ b/web/Dockerfile.dockerignore @@ -0,0 +1,34 @@ +** +!package.json +!pnpm-lock.yaml +!pnpm-workspace.yaml +!.nvmrc +!web/ +!web/** +!e2e/ +!e2e/package.json +!sdks/ +!sdks/nodejs-client/ +!sdks/nodejs-client/package.json + +.git +node_modules +.pnpm-store +web/.env +web/.env.* +web/logs +web/*.log* +web/node_modules +web/dist +web/build +web/coverage +web/.husky +web/.next +web/.pnpm-store +web/.vscode +web/.idea +web/*.iml +web/*.iws +web/*.ipr +e2e/node_modules +sdks/nodejs-client/node_modules diff --git a/web/README.md b/web/README.md index 14ca856875..2d69a94dbd 100644 --- a/web/README.md +++ b/web/README.md @@ -24,18 +24,24 @@ For example, use `vp install` instead of `pnpm install` and `vp test` instead of > > Learn more: [Corepack] +Run the following commands from the repository root. + First, install the dependencies: ```bash pnpm install ``` +> [!NOTE] +> JavaScript dependencies are managed by the workspace files at the repository root: `package.json`, `pnpm-lock.yaml`, `pnpm-workspace.yaml`, and `.nvmrc`. +> Install dependencies from the repository root, then run frontend scripts from `web/`. + Then, configure the environment variables. -Create a file named `.env.local` in the current directory and copy the contents from `.env.example`. +Create `web/.env.local` and copy the contents from `web/.env.example`. Modify the values of these environment variables according to your requirements: ```bash -cp .env.example .env.local +cp web/.env.example web/.env.local ``` > [!IMPORTANT] @@ -46,16 +52,16 @@ cp .env.example .env.local Finally, run the development server: ```bash -pnpm run dev +pnpm -C web run dev # or if you are using vinext which provides a better development experience -pnpm run dev:vinext +pnpm -C web run dev:vinext # (optional) start the dev proxy server so that you can use online API in development -pnpm run dev:proxy +pnpm -C web run dev:proxy ``` Open with your browser to see the result. -You can start editing the file under folder `app`. +You can start editing the files under `web/app`. The page auto-updates as you edit the file. ## Deploy @@ -65,19 +71,25 @@ The page auto-updates as you edit the file. First, build the app for production: ```bash -pnpm run build +pnpm -C web run build ``` Then, start the server: ```bash -pnpm run start +pnpm -C web run start +``` + +If you build the Docker image manually, use the repository root as the build context: + +```bash +docker build -f web/Dockerfile -t dify-web . ``` If you want to customize the host and port: ```bash -pnpm run start --port=3001 --host=0.0.0.0 +pnpm -C web run start --port=3001 --host=0.0.0.0 ``` ## Storybook @@ -87,7 +99,7 @@ This project uses [Storybook] for UI component development. To start the storybook server, run: ```bash -pnpm storybook +pnpm -C web storybook ``` Open with your browser to see the result. @@ -112,7 +124,7 @@ We use [Vitest] and [React Testing Library] for Unit Testing. Run test: ```bash -pnpm test +pnpm -C web test ``` > [!NOTE] diff --git a/web/app/components/app/configuration/config/config-audio.spec.tsx b/web/app/components/app/configuration/config/config-audio.spec.tsx index a3e5c7c149..cad0b40b95 100644 --- a/web/app/components/app/configuration/config/config-audio.spec.tsx +++ b/web/app/components/app/configuration/config/config-audio.spec.tsx @@ -1,4 +1,3 @@ -import type { Mock } from 'vitest' import type { FeatureStoreState } from '@/app/components/base/features/store' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' @@ -28,7 +27,7 @@ type SetupOptions = { } let mockFeatureStoreState: FeatureStoreState -let mockSetFeatures: Mock +let mockSetFeatures = vi.fn() const mockStore = { getState: vi.fn<() => FeatureStoreState>(() => mockFeatureStoreState), } diff --git a/web/app/components/app/configuration/config/config-document.spec.tsx b/web/app/components/app/configuration/config/config-document.spec.tsx index 2aa87717fc..300acb7ce7 100644 --- a/web/app/components/app/configuration/config/config-document.spec.tsx +++ b/web/app/components/app/configuration/config/config-document.spec.tsx @@ -1,4 +1,3 @@ -import type { Mock } from 'vitest' import type { FeatureStoreState } from '@/app/components/base/features/store' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' @@ -28,7 +27,7 @@ type SetupOptions = { } let mockFeatureStoreState: FeatureStoreState -let mockSetFeatures: Mock +let mockSetFeatures = vi.fn() const mockStore = { getState: vi.fn<() => FeatureStoreState>(() => mockFeatureStoreState), } diff --git a/web/app/components/app/configuration/config/index.spec.tsx b/web/app/components/app/configuration/config/index.spec.tsx index 875e583397..b24c719b99 100644 --- a/web/app/components/app/configuration/config/index.spec.tsx +++ b/web/app/components/app/configuration/config/index.spec.tsx @@ -1,4 +1,3 @@ -import type { Mock } from 'vitest' import type { ModelConfig, PromptVariable } from '@/models/debug' import type { ToolItem } from '@/types/app' import { render, screen } from '@testing-library/react' @@ -74,10 +73,10 @@ type MockContext = { history: boolean query: boolean } - showHistoryModal: Mock + showHistoryModal: () => void modelConfig: ModelConfig - setModelConfig: Mock - setPrevPromptConfig: Mock + setModelConfig: (modelConfig: ModelConfig) => void + setPrevPromptConfig: (configs: ModelConfig['configs']) => void } const createPromptVariable = (overrides: Partial = {}): PromptVariable => ({ @@ -142,7 +141,7 @@ const createContextValue = (overrides: Partial = {}): MockContext = ...overrides, }) -const mockUseContext = useContextSelector.useContext as Mock +const mockUseContext = vi.mocked(useContextSelector.useContext) const renderConfig = (contextOverrides: Partial = {}) => { const contextValue = createContextValue(contextOverrides) diff --git a/web/app/components/apps/hooks/__tests__/use-dsl-drag-drop.spec.ts b/web/app/components/apps/hooks/__tests__/use-dsl-drag-drop.spec.ts index 58fed4caa8..00e2d69ab2 100644 --- a/web/app/components/apps/hooks/__tests__/use-dsl-drag-drop.spec.ts +++ b/web/app/components/apps/hooks/__tests__/use-dsl-drag-drop.spec.ts @@ -1,16 +1,15 @@ -import type { Mock } from 'vitest' import { act, renderHook } from '@testing-library/react' import { useDSLDragDrop } from '../use-dsl-drag-drop' describe('useDSLDragDrop', () => { let container: HTMLDivElement - let mockOnDSLFileDropped: Mock + let mockOnDSLFileDropped = vi.fn<(file: File) => void>() beforeEach(() => { vi.clearAllMocks() container = document.createElement('div') document.body.appendChild(container) - mockOnDSLFileDropped = vi.fn() + mockOnDSLFileDropped = vi.fn<(file: File) => void>() }) afterEach(() => { diff --git a/web/app/components/base/carousel/__tests__/index.spec.tsx b/web/app/components/base/carousel/__tests__/index.spec.tsx index cc45256937..e409b85757 100644 --- a/web/app/components/base/carousel/__tests__/index.spec.tsx +++ b/web/app/components/base/carousel/__tests__/index.spec.tsx @@ -11,15 +11,15 @@ type EmblaEventName = 'reInit' | 'select' type EmblaListener = (api: MockEmblaApi | undefined) => void type MockEmblaApi = { - scrollPrev: Mock - scrollNext: Mock - scrollTo: Mock - selectedScrollSnap: Mock - canScrollPrev: Mock - canScrollNext: Mock - slideNodes: Mock - on: Mock - off: Mock + scrollPrev: Mock<() => void> + scrollNext: Mock<() => void> + scrollTo: Mock<(index: number) => void> + selectedScrollSnap: Mock<() => number> + canScrollPrev: Mock<() => boolean> + canScrollNext: Mock<() => boolean> + slideNodes: Mock<() => HTMLDivElement[]> + on: Mock<(event: EmblaEventName, callback: EmblaListener) => void> + off: Mock<(event: EmblaEventName, callback: EmblaListener) => void> } let mockCanScrollPrev = false @@ -33,19 +33,19 @@ const mockCarouselRef = vi.fn() const mockedUseEmblaCarousel = vi.mocked(useEmblaCarousel) const createMockEmblaApi = (): MockEmblaApi => ({ - scrollPrev: vi.fn(), - scrollNext: vi.fn(), - scrollTo: vi.fn(), - selectedScrollSnap: vi.fn(() => mockSelectedIndex), - canScrollPrev: vi.fn(() => mockCanScrollPrev), - canScrollNext: vi.fn(() => mockCanScrollNext), - slideNodes: vi.fn(() => - Array.from({ length: mockSlideCount }).fill(document.createElement('div')), + scrollPrev: vi.fn<() => void>(), + scrollNext: vi.fn<() => void>(), + scrollTo: vi.fn<(index: number) => void>(), + selectedScrollSnap: vi.fn<() => number>(() => mockSelectedIndex), + canScrollPrev: vi.fn<() => boolean>(() => mockCanScrollPrev), + canScrollNext: vi.fn<() => boolean>(() => mockCanScrollNext), + slideNodes: vi.fn<() => HTMLDivElement[]>(() => + Array.from({ length: mockSlideCount }, () => document.createElement('div')), ), - on: vi.fn((event: EmblaEventName, callback: EmblaListener) => { + on: vi.fn<(event: EmblaEventName, callback: EmblaListener) => void>((event, callback) => { listeners[event].push(callback) }), - off: vi.fn((event: EmblaEventName, callback: EmblaListener) => { + off: vi.fn<(event: EmblaEventName, callback: EmblaListener) => void>((event, callback) => { listeners[event] = listeners[event].filter(listener => listener !== callback) }), }) diff --git a/web/app/components/billing/upgrade-btn/__tests__/index.spec.tsx b/web/app/components/billing/upgrade-btn/__tests__/index.spec.tsx index 1840f1d33c..7eda24b944 100644 --- a/web/app/components/billing/upgrade-btn/__tests__/index.spec.tsx +++ b/web/app/components/billing/upgrade-btn/__tests__/index.spec.tsx @@ -1,4 +1,3 @@ -import type { Mock } from 'vitest' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import UpgradeBtn from '../index' @@ -14,14 +13,16 @@ vi.mock('@/context/modal-context', () => ({ }), })) +type GtagHandler = (command: string, action: string, payload: { loc: string }) => void + // Typed window accessor for gtag tracking tests -const gtagWindow = window as unknown as Record -let mockGtag: Mock | undefined +const gtagWindow = window as unknown as { gtag?: GtagHandler } +let mockGtag = vi.fn() describe('UpgradeBtn', () => { beforeEach(() => { vi.clearAllMocks() - mockGtag = vi.fn() + mockGtag = vi.fn() gtagWindow.gtag = mockGtag }) diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index 940aad9f4c..020e960d73 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -56,6 +56,7 @@ export default antfu( }, }, e18e: false, + pnpm: false, }, { plugins: { diff --git a/web/next.config.ts b/web/next.config.ts index 838aa561a3..aa4d9318f4 100644 --- a/web/next.config.ts +++ b/web/next.config.ts @@ -10,7 +10,6 @@ const nextConfig: NextConfig = { basePath: env.NEXT_PUBLIC_BASE_PATH, transpilePackages: ['@t3-oss/env-core', '@t3-oss/env-nextjs', 'echarts', 'zrender'], turbopack: { - root: process.cwd(), rules: codeInspectorPlugin({ bundler: 'turbopack', }), diff --git a/web/package.json b/web/package.json index 65372ef5f5..9ed21fdb22 100644 --- a/web/package.json +++ b/web/package.json @@ -3,7 +3,6 @@ "type": "module", "version": "1.13.3", "private": true, - "packageManager": "pnpm@10.32.1", "imports": { "#i18n": { "react-server": "./i18n-config/lib.server.ts", @@ -22,9 +21,6 @@ "and_uc >= 15.5", "and_qq >= 14.9" ], - "engines": { - "node": "^22.22.1" - }, "scripts": { "analyze": "next experimental-analyze", "analyze-component": "node ./scripts/analyze-component.js", @@ -58,258 +54,189 @@ "uglify-embed": "node ./bin/uglify-embed" }, "dependencies": { - "@amplitude/analytics-browser": "2.37.0", - "@amplitude/plugin-session-replay-browser": "1.27.1", - "@base-ui/react": "1.3.0", - "@emoji-mart/data": "1.2.1", - "@floating-ui/react": "0.27.19", - "@formatjs/intl-localematcher": "0.8.2", - "@headlessui/react": "2.2.9", - "@heroicons/react": "2.2.0", - "@lexical/code": "0.42.0", - "@lexical/link": "0.42.0", - "@lexical/list": "0.42.0", - "@lexical/react": "0.42.0", - "@lexical/selection": "0.42.0", - "@lexical/text": "0.42.0", - "@lexical/utils": "0.42.0", - "@monaco-editor/react": "4.7.0", - "@orpc/client": "1.13.9", - "@orpc/contract": "1.13.9", - "@orpc/openapi-client": "1.13.9", - "@orpc/tanstack-query": "1.13.9", - "@remixicon/react": "4.9.0", - "@sentry/react": "10.45.0", - "@streamdown/math": "1.0.2", - "@svgdotjs/svg.js": "3.2.5", - "@t3-oss/env-nextjs": "0.13.11", - "@tailwindcss/typography": "0.5.19", - "@tanstack/react-form": "1.28.5", - "@tanstack/react-query": "5.95.0", - "abcjs": "6.6.2", - "ahooks": "3.9.6", - "class-variance-authority": "0.7.1", - "clsx": "2.1.1", - "cmdk": "1.1.1", - "copy-to-clipboard": "3.3.3", - "cron-parser": "5.5.0", - "dayjs": "1.11.20", - "decimal.js": "10.6.0", - "dompurify": "3.3.3", - "echarts": "6.0.0", - "echarts-for-react": "3.0.6", - "elkjs": "0.11.1", - "embla-carousel-autoplay": "8.6.0", - "embla-carousel-react": "8.6.0", - "emoji-mart": "5.6.0", - "es-toolkit": "1.45.1", - "fast-deep-equal": "3.1.3", - "foxact": "0.3.0", - "html-entities": "2.6.0", - "html-to-image": "1.11.13", - "i18next": "25.10.4", - "i18next-resources-to-backend": "1.2.1", - "immer": "11.1.4", - "jotai": "2.18.1", - "js-audio-recorder": "1.0.7", - "js-cookie": "3.0.5", - "js-yaml": "4.1.1", - "jsonschema": "1.5.0", - "katex": "0.16.40", - "ky": "1.14.3", - "lamejs": "1.2.1", - "lexical": "0.42.0", - "mermaid": "11.13.0", - "mime": "4.1.0", - "mitt": "3.0.1", - "negotiator": "1.0.0", - "next": "16.2.1", - "next-themes": "0.4.6", - "nuqs": "2.8.9", - "pinyin-pro": "3.28.0", - "qrcode.react": "4.2.0", - "qs": "6.15.0", - "react": "19.2.4", - "react-18-input-autosize": "3.0.0", - "react-dom": "19.2.4", - "react-easy-crop": "5.5.6", - "react-hotkeys-hook": "5.2.4", - "react-i18next": "16.6.1", - "react-multi-email": "1.0.25", - "react-papaparse": "4.4.0", - "react-pdf-highlighter": "8.0.0-rc.0", - "react-sortablejs": "6.1.4", - "react-syntax-highlighter": "15.6.6", - "react-textarea-autosize": "8.5.9", - "react-window": "1.8.11", - "reactflow": "11.11.4", - "remark-breaks": "4.0.0", - "remark-directive": "4.0.0", - "scheduler": "0.27.0", - "sharp": "0.34.5", - "sortablejs": "1.15.7", - "std-semver": "1.0.8", - "streamdown": "2.5.0", - "string-ts": "2.3.1", - "tailwind-merge": "2.6.1", - "tldts": "7.0.27", - "unist-util-visit": "5.1.0", - "use-context-selector": "2.0.0", - "uuid": "13.0.0", - "zod": "4.3.6", - "zundo": "2.3.0", - "zustand": "5.0.12" + "@amplitude/analytics-browser": "catalog:", + "@amplitude/plugin-session-replay-browser": "catalog:", + "@base-ui/react": "catalog:", + "@emoji-mart/data": "catalog:", + "@floating-ui/react": "catalog:", + "@formatjs/intl-localematcher": "catalog:", + "@headlessui/react": "catalog:", + "@heroicons/react": "catalog:", + "@lexical/code": "catalog:", + "@lexical/link": "catalog:", + "@lexical/list": "catalog:", + "@lexical/react": "catalog:", + "@lexical/selection": "catalog:", + "@lexical/text": "catalog:", + "@lexical/utils": "catalog:", + "@monaco-editor/react": "catalog:", + "@orpc/client": "catalog:", + "@orpc/contract": "catalog:", + "@orpc/openapi-client": "catalog:", + "@orpc/tanstack-query": "catalog:", + "@remixicon/react": "catalog:", + "@sentry/react": "catalog:", + "@streamdown/math": "catalog:", + "@svgdotjs/svg.js": "catalog:", + "@t3-oss/env-nextjs": "catalog:", + "@tailwindcss/typography": "catalog:", + "@tanstack/react-form": "catalog:", + "@tanstack/react-query": "catalog:", + "abcjs": "catalog:", + "ahooks": "catalog:", + "class-variance-authority": "catalog:", + "clsx": "catalog:", + "cmdk": "catalog:", + "copy-to-clipboard": "catalog:", + "cron-parser": "catalog:", + "dayjs": "catalog:", + "decimal.js": "catalog:", + "dompurify": "catalog:", + "echarts": "catalog:", + "echarts-for-react": "catalog:", + "elkjs": "catalog:", + "embla-carousel-autoplay": "catalog:", + "embla-carousel-react": "catalog:", + "emoji-mart": "catalog:", + "es-toolkit": "catalog:", + "fast-deep-equal": "catalog:", + "foxact": "catalog:", + "html-entities": "catalog:", + "html-to-image": "catalog:", + "i18next": "catalog:", + "i18next-resources-to-backend": "catalog:", + "immer": "catalog:", + "jotai": "catalog:", + "js-audio-recorder": "catalog:", + "js-cookie": "catalog:", + "js-yaml": "catalog:", + "jsonschema": "catalog:", + "katex": "catalog:", + "ky": "catalog:", + "lamejs": "catalog:", + "lexical": "catalog:", + "mermaid": "catalog:", + "mime": "catalog:", + "mitt": "catalog:", + "negotiator": "catalog:", + "next": "catalog:", + "next-themes": "catalog:", + "nuqs": "catalog:", + "pinyin-pro": "catalog:", + "qrcode.react": "catalog:", + "qs": "catalog:", + "react": "catalog:", + "react-18-input-autosize": "catalog:", + "react-dom": "catalog:", + "react-easy-crop": "catalog:", + "react-hotkeys-hook": "catalog:", + "react-i18next": "catalog:", + "react-multi-email": "catalog:", + "react-papaparse": "catalog:", + "react-pdf-highlighter": "catalog:", + "react-sortablejs": "catalog:", + "react-syntax-highlighter": "catalog:", + "react-textarea-autosize": "catalog:", + "react-window": "catalog:", + "reactflow": "catalog:", + "remark-breaks": "catalog:", + "remark-directive": "catalog:", + "scheduler": "catalog:", + "sharp": "catalog:", + "sortablejs": "catalog:", + "std-semver": "catalog:", + "streamdown": "catalog:", + "string-ts": "catalog:", + "tailwind-merge": "catalog:", + "tldts": "catalog:", + "unist-util-visit": "catalog:", + "use-context-selector": "catalog:", + "uuid": "catalog:", + "zod": "catalog:", + "zundo": "catalog:", + "zustand": "catalog:" }, "devDependencies": { - "@antfu/eslint-config": "7.7.3", - "@chromatic-com/storybook": "5.0.2", - "@egoist/tailwindcss-icons": "1.9.2", - "@eslint-react/eslint-plugin": "3.0.0", - "@hono/node-server": "1.19.11", - "@iconify-json/heroicons": "1.2.3", - "@iconify-json/ri": "1.2.10", - "@mdx-js/loader": "3.1.1", - "@mdx-js/react": "3.1.1", - "@mdx-js/rollup": "3.1.1", - "@next/eslint-plugin-next": "16.2.1", - "@next/mdx": "16.2.1", - "@rgrove/parse-xml": "4.2.0", - "@storybook/addon-docs": "10.3.1", - "@storybook/addon-links": "10.3.1", - "@storybook/addon-onboarding": "10.3.1", - "@storybook/addon-themes": "10.3.1", - "@storybook/nextjs-vite": "10.3.1", - "@storybook/react": "10.3.1", - "@tanstack/eslint-plugin-query": "5.95.0", - "@tanstack/react-devtools": "0.10.0", - "@tanstack/react-form-devtools": "0.2.19", - "@tanstack/react-query-devtools": "5.95.0", - "@testing-library/dom": "10.4.1", - "@testing-library/jest-dom": "6.9.1", - "@testing-library/react": "16.3.2", - "@testing-library/user-event": "14.6.1", - "@tsslint/cli": "3.0.2", - "@tsslint/compat-eslint": "3.0.2", - "@tsslint/config": "3.0.2", - "@types/js-cookie": "3.0.6", - "@types/js-yaml": "4.0.9", - "@types/negotiator": "0.6.4", - "@types/node": "25.5.0", - "@types/postcss-js": "4.1.0", - "@types/qs": "6.15.0", - "@types/react": "19.2.14", - "@types/react-dom": "19.2.3", - "@types/react-syntax-highlighter": "15.5.13", - "@types/react-window": "1.8.8", - "@types/sortablejs": "1.15.9", - "@typescript-eslint/parser": "8.57.1", - "@typescript/native-preview": "7.0.0-dev.20260322.1", - "@vitejs/plugin-react": "6.0.1", - "@vitejs/plugin-rsc": "0.5.21", - "@vitest/coverage-v8": "4.1.0", - "agentation": "2.3.3", - "autoprefixer": "10.4.27", - "code-inspector-plugin": "1.4.5", - "eslint": "10.1.0", - "eslint-markdown": "0.6.0", - "eslint-plugin-better-tailwindcss": "4.3.2", - "eslint-plugin-hyoban": "0.14.1", - "eslint-plugin-markdown-preferences": "0.40.3", - "eslint-plugin-no-barrel-files": "1.2.2", - "eslint-plugin-react-hooks": "7.0.1", - "eslint-plugin-react-refresh": "0.5.2", - "eslint-plugin-sonarjs": "4.0.2", - "eslint-plugin-storybook": "10.3.1", - "happy-dom": "20.8.9", - "hono": "4.12.8", - "husky": "9.1.7", - "iconify-import-svg": "0.1.2", - "knip": "6.0.2", - "lint-staged": "16.4.0", - "postcss": "8.5.8", - "postcss-js": "5.1.0", - "react-server-dom-webpack": "19.2.4", - "sass": "1.98.0", - "storybook": "10.3.1", - "tailwindcss": "3.4.19", - "taze": "19.10.0", - "tsx": "4.21.0", - "typescript": "5.9.3", - "uglify-js": "3.19.3", - "vinext": "https://pkg.pr.new/vinext@b6a2cac", - "vite": "npm:@voidzero-dev/vite-plus-core@0.1.13", - "vite-plugin-inspect": "11.3.3", - "vite-plus": "0.1.13", - "vitest": "npm:@voidzero-dev/vite-plus-test@0.1.13", - "vitest-canvas-mock": "1.1.3" - }, - "pnpm": { - "overrides": { - "@lexical/code": "npm:lexical-code-no-prism@0.41.0", - "@monaco-editor/loader": "1.7.0", - "@nolyfill/safe-buffer": "npm:safe-buffer@^5.2.1", - "array-includes": "npm:@nolyfill/array-includes@^1.0.44", - "array.prototype.findlast": "npm:@nolyfill/array.prototype.findlast@^1.0.44", - "array.prototype.findlastindex": "npm:@nolyfill/array.prototype.findlastindex@^1.0.44", - "array.prototype.flat": "npm:@nolyfill/array.prototype.flat@^1.0.44", - "array.prototype.flatmap": "npm:@nolyfill/array.prototype.flatmap@^1.0.44", - "array.prototype.tosorted": "npm:@nolyfill/array.prototype.tosorted@^1.0.44", - "assert": "npm:@nolyfill/assert@^1.0.26", - "brace-expansion@<2.0.2": "2.0.2", - "canvas": "^3.2.2", - "devalue@<5.3.2": "5.3.2", - "dompurify@>=3.1.3 <=3.3.1": "3.3.2", - "es-iterator-helpers": "npm:@nolyfill/es-iterator-helpers@^1.0.21", - "esbuild@<0.27.2": "0.27.2", - "glob@>=10.2.0 <10.5.0": "11.1.0", - "hasown": "npm:@nolyfill/hasown@^1.0.44", - "is-arguments": "npm:@nolyfill/is-arguments@^1.0.44", - "is-core-module": "npm:@nolyfill/is-core-module@^1.0.39", - "is-generator-function": "npm:@nolyfill/is-generator-function@^1.0.44", - "is-typed-array": "npm:@nolyfill/is-typed-array@^1.0.44", - "isarray": "npm:@nolyfill/isarray@^1.0.44", - "object.assign": "npm:@nolyfill/object.assign@^1.0.44", - "object.entries": "npm:@nolyfill/object.entries@^1.0.44", - "object.fromentries": "npm:@nolyfill/object.fromentries@^1.0.44", - "object.groupby": "npm:@nolyfill/object.groupby@^1.0.44", - "object.values": "npm:@nolyfill/object.values@^1.0.44", - "pbkdf2": "~3.1.5", - "pbkdf2@<3.1.3": "3.1.3", - "picomatch@<2.3.2": "2.3.2", - "picomatch@>=4.0.0 <4.0.4": "4.0.4", - "prismjs": "~1.30", - "prismjs@<1.30.0": "1.30.0", - "rollup@>=4.0.0 <4.59.0": "4.59.0", - "safe-buffer": "^5.2.1", - "safe-regex-test": "npm:@nolyfill/safe-regex-test@^1.0.44", - "safer-buffer": "npm:@nolyfill/safer-buffer@^1.0.44", - "side-channel": "npm:@nolyfill/side-channel@^1.0.44", - "smol-toml@<1.6.1": "1.6.1", - "solid-js": "1.9.11", - "string-width": "~8.2.0", - "string.prototype.includes": "npm:@nolyfill/string.prototype.includes@^1.0.44", - "string.prototype.matchall": "npm:@nolyfill/string.prototype.matchall@^1.0.44", - "string.prototype.repeat": "npm:@nolyfill/string.prototype.repeat@^1.0.44", - "string.prototype.trimend": "npm:@nolyfill/string.prototype.trimend@^1.0.44", - "svgo@>=3.0.0 <3.3.3": "3.3.3", - "tar@<=7.5.10": "7.5.11", - "typed-array-buffer": "npm:@nolyfill/typed-array-buffer@^1.0.44", - "undici@>=7.0.0 <7.24.0": "7.24.0", - "vite": "npm:@voidzero-dev/vite-plus-core@0.1.13", - "vitest": "npm:@voidzero-dev/vite-plus-test@0.1.13", - "which-typed-array": "npm:@nolyfill/which-typed-array@^1.0.44", - "yaml@>=2.0.0 <2.8.3": "2.8.3", - "yauzl@<3.2.1": "3.2.1" - }, - "ignoredBuiltDependencies": [ - "canvas", - "core-js-pure" - ], - "onlyBuiltDependencies": [ - "@parcel/watcher", - "esbuild", - "sharp" - ] + "@antfu/eslint-config": "catalog:", + "@chromatic-com/storybook": "catalog:", + "@egoist/tailwindcss-icons": "catalog:", + "@eslint-react/eslint-plugin": "catalog:", + "@hono/node-server": "catalog:", + "@iconify-json/heroicons": "catalog:", + "@iconify-json/ri": "catalog:", + "@mdx-js/loader": "catalog:", + "@mdx-js/react": "catalog:", + "@mdx-js/rollup": "catalog:", + "@next/eslint-plugin-next": "catalog:", + "@next/mdx": "catalog:", + "@rgrove/parse-xml": "catalog:", + "@storybook/addon-docs": "catalog:", + "@storybook/addon-links": "catalog:", + "@storybook/addon-onboarding": "catalog:", + "@storybook/addon-themes": "catalog:", + "@storybook/nextjs-vite": "catalog:", + "@storybook/react": "catalog:", + "@tanstack/eslint-plugin-query": "catalog:", + "@tanstack/react-devtools": "catalog:", + "@tanstack/react-form-devtools": "catalog:", + "@tanstack/react-query-devtools": "catalog:", + "@testing-library/dom": "catalog:", + "@testing-library/jest-dom": "catalog:", + "@testing-library/react": "catalog:", + "@testing-library/user-event": "catalog:", + "@tsslint/cli": "catalog:", + "@tsslint/compat-eslint": "catalog:", + "@tsslint/config": "catalog:", + "@types/js-cookie": "catalog:", + "@types/js-yaml": "catalog:", + "@types/negotiator": "catalog:", + "@types/node": "catalog:", + "@types/postcss-js": "catalog:", + "@types/qs": "catalog:", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "@types/react-syntax-highlighter": "catalog:", + "@types/react-window": "catalog:", + "@types/sortablejs": "catalog:", + "@typescript-eslint/parser": "catalog:", + "@typescript/native-preview": "catalog:", + "@vitejs/plugin-react": "catalog:", + "@vitejs/plugin-rsc": "catalog:", + "@vitest/coverage-v8": "catalog:", + "agentation": "catalog:", + "autoprefixer": "catalog:", + "code-inspector-plugin": "catalog:", + "eslint": "catalog:", + "eslint-markdown": "catalog:", + "eslint-plugin-better-tailwindcss": "catalog:", + "eslint-plugin-hyoban": "catalog:", + "eslint-plugin-markdown-preferences": "catalog:", + "eslint-plugin-no-barrel-files": "catalog:", + "eslint-plugin-react-hooks": "catalog:", + "eslint-plugin-react-refresh": "catalog:", + "eslint-plugin-sonarjs": "catalog:", + "eslint-plugin-storybook": "catalog:", + "happy-dom": "catalog:", + "hono": "catalog:", + "husky": "catalog:", + "iconify-import-svg": "catalog:", + "knip": "catalog:", + "lint-staged": "catalog:", + "postcss": "catalog:", + "postcss-js": "catalog:", + "react-server-dom-webpack": "catalog:", + "sass": "catalog:", + "storybook": "catalog:", + "tailwindcss": "catalog:", + "tsx": "catalog:", + "typescript": "catalog:", + "uglify-js": "catalog:", + "vinext": "catalog:", + "vite": "catalog:", + "vite-plugin-inspect": "catalog:", + "vite-plus": "catalog:", + "vitest": "catalog:", + "vitest-canvas-mock": "catalog:" }, "lint-staged": { "*": "eslint --fix --pass-on-unpruned-suppressions" diff --git a/web/scripts/copy-and-start.mjs b/web/scripts/copy-and-start.mjs index 9525c6b45f..932a88a32d 100644 --- a/web/scripts/copy-and-start.mjs +++ b/web/scripts/copy-and-start.mjs @@ -8,21 +8,6 @@ import { spawn } from 'node:child_process' import { cp, mkdir, stat } from 'node:fs/promises' import path from 'node:path' -// Configuration for directories to copy -const DIRS_TO_COPY = [ - { - src: path.join('.next', 'static'), - dest: path.join('.next', 'standalone', '.next', 'static'), - }, - { - src: 'public', - dest: path.join('.next', 'standalone', 'public'), - }, -] - -// Path to the server script -const SERVER_SCRIPT_PATH = path.join('.next', 'standalone', 'server.js') - // Function to check if a path exists const pathExists = async (path) => { try { @@ -40,6 +25,23 @@ const pathExists = async (path) => { } } +const STANDALONE_ROOT_CANDIDATES = [ + path.join('.next', 'standalone', 'web'), + path.join('.next', 'standalone'), +] + +const getStandaloneRoot = async () => { + for (const standaloneRoot of STANDALONE_ROOT_CANDIDATES) { + const serverScriptPath = path.join(standaloneRoot, 'server.js') + if (await pathExists(serverScriptPath)) + return standaloneRoot + } + + throw new Error( + `Unable to find Next standalone server entry. Checked: ${STANDALONE_ROOT_CANDIDATES.join(', ')}`, + ) +} + // Function to recursively copy directories const copyDir = async (src, dest) => { console.debug(`Copying directory from ${src} to ${dest}`) @@ -48,9 +50,20 @@ const copyDir = async (src, dest) => { } // Process each directory copy operation -const copyAllDirs = async () => { +const copyAllDirs = async (standaloneRoot) => { + const dirsToCopy = [ + { + src: path.join('.next', 'static'), + dest: path.join(standaloneRoot, '.next', 'static'), + }, + { + src: 'public', + dest: path.join(standaloneRoot, 'public'), + }, + ] + console.debug('Starting directory copy operations') - for (const { src, dest } of DIRS_TO_COPY) { + for (const { src, dest } of dirsToCopy) { try { // Instead of pre-creating destination directory, we ensure parent directory exists const destParent = path.dirname(dest) @@ -75,19 +88,22 @@ const copyAllDirs = async () => { // Run copy operations and start server const main = async () => { console.debug('Starting copy-and-start script') - await copyAllDirs() + const standaloneRoot = await getStandaloneRoot() + const serverScriptPath = path.join(standaloneRoot, 'server.js') + + await copyAllDirs(standaloneRoot) // Start server const port = process.env.npm_config_port || process.env.PORT || '3000' const host = process.env.npm_config_host || process.env.HOSTNAME || '0.0.0.0' console.info(`Starting server on ${host}:${port}`) - console.debug(`Server script path: ${SERVER_SCRIPT_PATH}`) + console.debug(`Server script path: ${serverScriptPath}`) console.debug(`Environment variables - PORT: ${port}, HOSTNAME: ${host}`) const server = spawn( process.execPath, - [SERVER_SCRIPT_PATH], + [serverScriptPath], { env: { ...process.env, diff --git a/web/vitest.setup.ts b/web/vitest.setup.ts index ac26ac5d25..b17a59bab6 100644 --- a/web/vitest.setup.ts +++ b/web/vitest.setup.ts @@ -1,8 +1,11 @@ +import * as jestDomMatchers from '@testing-library/jest-dom/matchers' import { act, cleanup } from '@testing-library/react' import * as React from 'react' import '@testing-library/jest-dom/vitest' import 'vitest-canvas-mock' +expect.extend(jestDomMatchers) + // Suppress act() warnings from @headlessui/react internal Transition component // These warnings are caused by Headless UI's internal async state updates, not our code const originalConsoleError = console.error From ae9a16a397f709568725ce220547fa79400ea6ee Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Mon, 30 Mar 2026 21:06:55 +0800 Subject: [PATCH 006/199] fix: upgrade langfuse SDK to v3+ for LLM-as-judge support (#34265) Signed-off-by: majiayu000 <1835304752@qq.com> --- api/core/app/workflow/layers/observability.py | 3 +- .../aliyun_trace/data_exporter/traceclient.py | 16 +- .../arize_phoenix_trace.py | 10 +- api/core/ops/langfuse_trace/langfuse_trace.py | 107 +++++++++-- api/core/ops/tencent_trace/client.py | 22 ++- api/enterprise/telemetry/exporter.py | 9 +- api/extensions/ext_sentry.py | 15 +- api/pyproject.toml | 34 ++-- .../ops/langfuse_trace/test_langfuse_trace.py | 16 +- api/uv.lock | 168 +++++++++--------- 10 files changed, 257 insertions(+), 143 deletions(-) diff --git a/api/core/app/workflow/layers/observability.py b/api/core/app/workflow/layers/observability.py index 8565c3076c..c4ed54a140 100644 --- a/api/core/app/workflow/layers/observability.py +++ b/api/core/app/workflow/layers/observability.py @@ -8,6 +8,7 @@ associates with the node span. """ import logging +from contextvars import Token from dataclasses import dataclass from typing import cast, final @@ -35,7 +36,7 @@ logger = logging.getLogger(__name__) @dataclass(slots=True) class _NodeSpanContext: span: "Span" - token: object + token: Token[context_api.Context] @final diff --git a/api/core/ops/aliyun_trace/data_exporter/traceclient.py b/api/core/ops/aliyun_trace/data_exporter/traceclient.py index 0e00e90520..67d5163b0f 100644 --- a/api/core/ops/aliyun_trace/data_exporter/traceclient.py +++ b/api/core/ops/aliyun_trace/data_exporter/traceclient.py @@ -16,7 +16,13 @@ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExport from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.util.instrumentation import InstrumentationScope -from opentelemetry.semconv.resource import ResourceAttributes +from opentelemetry.semconv._incubating.attributes.deployment_attributes import ( # type: ignore[import-untyped] + DEPLOYMENT_ENVIRONMENT, +) +from opentelemetry.semconv._incubating.attributes.host_attributes import ( # type: ignore[import-untyped] + HOST_NAME, +) +from opentelemetry.semconv.attributes import service_attributes from opentelemetry.trace import Link, SpanContext, TraceFlags from configs import dify_config @@ -45,10 +51,10 @@ class TraceClient: self.endpoint = endpoint self.resource = Resource( attributes={ - ResourceAttributes.SERVICE_NAME: service_name, - ResourceAttributes.SERVICE_VERSION: f"dify-{dify_config.project.version}-{dify_config.COMMIT_SHA}", - ResourceAttributes.DEPLOYMENT_ENVIRONMENT: f"{dify_config.DEPLOY_ENV}-{dify_config.EDITION}", - ResourceAttributes.HOST_NAME: socket.gethostname(), + service_attributes.SERVICE_NAME: service_name, + service_attributes.SERVICE_VERSION: f"dify-{dify_config.project.version}-{dify_config.COMMIT_SHA}", + DEPLOYMENT_ENVIRONMENT: f"{dify_config.DEPLOY_ENV}-{dify_config.EDITION}", + HOST_NAME: socket.gethostname(), ACS_ARMS_SERVICE_FEATURE: "genai_app", } ) diff --git a/api/core/ops/arize_phoenix_trace/arize_phoenix_trace.py b/api/core/ops/arize_phoenix_trace/arize_phoenix_trace.py index 39d97e2882..902f58e6b7 100644 --- a/api/core/ops/arize_phoenix_trace/arize_phoenix_trace.py +++ b/api/core/ops/arize_phoenix_trace/arize_phoenix_trace.py @@ -19,7 +19,7 @@ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExport from opentelemetry.sdk import trace as trace_sdk from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace.export import SimpleSpanProcessor -from opentelemetry.semconv.trace import SpanAttributes as OTELSpanAttributes +from opentelemetry.semconv.attributes import exception_attributes from opentelemetry.trace import Span, Status, StatusCode, set_span_in_context, use_span from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator from opentelemetry.util.types import AttributeValue @@ -134,10 +134,10 @@ def set_span_status(current_span: Span, error: Exception | str | None = None): if not exception_message: exception_message = repr(error) attributes: dict[str, AttributeValue] = { - OTELSpanAttributes.EXCEPTION_TYPE: exception_type, - OTELSpanAttributes.EXCEPTION_MESSAGE: exception_message, - OTELSpanAttributes.EXCEPTION_ESCAPED: False, - OTELSpanAttributes.EXCEPTION_STACKTRACE: error_string, + exception_attributes.EXCEPTION_TYPE: exception_type, + exception_attributes.EXCEPTION_MESSAGE: exception_message, + exception_attributes.EXCEPTION_ESCAPED: False, + exception_attributes.EXCEPTION_STACKTRACE: error_string, } current_span.add_event(name="exception", attributes=attributes) else: diff --git a/api/core/ops/langfuse_trace/langfuse_trace.py b/api/core/ops/langfuse_trace/langfuse_trace.py index 3644b6b4c2..9be2ce1bdf 100644 --- a/api/core/ops/langfuse_trace/langfuse_trace.py +++ b/api/core/ops/langfuse_trace/langfuse_trace.py @@ -1,9 +1,19 @@ import logging import os -from datetime import datetime, timedelta +import uuid +from datetime import UTC, datetime, timedelta from graphon.enums import BuiltinNodeTypes from langfuse import Langfuse +from langfuse.api import ( + CreateGenerationBody, + CreateSpanBody, + IngestionEvent_GenerationCreate, + IngestionEvent_SpanCreate, + IngestionEvent_TraceCreate, + TraceBody, +) +from langfuse.api.commons.types.usage import Usage from sqlalchemy.orm import sessionmaker from core.ops.base_trace_instance import BaseTraceInstance @@ -396,18 +406,61 @@ class LangFuseDataTrace(BaseTraceInstance): ) self.add_span(langfuse_span_data=name_generation_span_data) + def _make_event_id(self) -> str: + return str(uuid.uuid4()) + + def _now_iso(self) -> str: + return datetime.now(UTC).isoformat() + def add_trace(self, langfuse_trace_data: LangfuseTrace | None = None): - format_trace_data = filter_none_values(langfuse_trace_data.model_dump()) if langfuse_trace_data else {} + data = filter_none_values(langfuse_trace_data.model_dump()) if langfuse_trace_data else {} try: - self.langfuse_client.trace(**format_trace_data) + body = TraceBody( + id=data.get("id"), + name=data.get("name"), + user_id=data.get("user_id"), + input=data.get("input"), + output=data.get("output"), + metadata=data.get("metadata"), + session_id=data.get("session_id"), + version=data.get("version"), + release=data.get("release"), + tags=data.get("tags"), + public=data.get("public"), + ) + event = IngestionEvent_TraceCreate( + body=body, + id=self._make_event_id(), + timestamp=self._now_iso(), + ) + self.langfuse_client.api.ingestion.batch(batch=[event]) logger.debug("LangFuse Trace created successfully") except Exception as e: raise ValueError(f"LangFuse Failed to create trace: {str(e)}") def add_span(self, langfuse_span_data: LangfuseSpan | None = None): - format_span_data = filter_none_values(langfuse_span_data.model_dump()) if langfuse_span_data else {} + data = filter_none_values(langfuse_span_data.model_dump()) if langfuse_span_data else {} try: - self.langfuse_client.span(**format_span_data) + body = CreateSpanBody( + id=data.get("id"), + trace_id=data.get("trace_id"), + name=data.get("name"), + start_time=data.get("start_time"), + end_time=data.get("end_time"), + input=data.get("input"), + output=data.get("output"), + metadata=data.get("metadata"), + level=data.get("level"), + status_message=data.get("status_message"), + parent_observation_id=data.get("parent_observation_id"), + version=data.get("version"), + ) + event = IngestionEvent_SpanCreate( + body=body, + id=self._make_event_id(), + timestamp=self._now_iso(), + ) + self.langfuse_client.api.ingestion.batch(batch=[event]) logger.debug("LangFuse Span created successfully") except Exception as e: raise ValueError(f"LangFuse Failed to create span: {str(e)}") @@ -418,11 +471,45 @@ class LangFuseDataTrace(BaseTraceInstance): span.end(**format_span_data) def add_generation(self, langfuse_generation_data: LangfuseGeneration | None = None): - format_generation_data = ( - filter_none_values(langfuse_generation_data.model_dump()) if langfuse_generation_data else {} - ) + data = filter_none_values(langfuse_generation_data.model_dump()) if langfuse_generation_data else {} try: - self.langfuse_client.generation(**format_generation_data) + usage_data = data.pop("usage", None) + usage = None + if usage_data: + usage = Usage( + input=usage_data.get("input", 0) or 0, + output=usage_data.get("output", 0) or 0, + total=usage_data.get("total", 0) or 0, + unit=usage_data.get("unit"), + input_cost=usage_data.get("inputCost"), + output_cost=usage_data.get("outputCost"), + total_cost=usage_data.get("totalCost"), + ) + + body = CreateGenerationBody( + id=data.get("id"), + trace_id=data.get("trace_id"), + name=data.get("name"), + start_time=data.get("start_time"), + end_time=data.get("end_time"), + model=data.get("model"), + model_parameters=data.get("model_parameters"), + input=data.get("input"), + output=data.get("output"), + usage=usage, + metadata=data.get("metadata"), + level=data.get("level"), + status_message=data.get("status_message"), + parent_observation_id=data.get("parent_observation_id"), + version=data.get("version"), + completion_start_time=data.get("completion_start_time"), + ) + event = IngestionEvent_GenerationCreate( + body=body, + id=self._make_event_id(), + timestamp=self._now_iso(), + ) + self.langfuse_client.api.ingestion.batch(batch=[event]) logger.debug("LangFuse Generation created successfully") except Exception as e: raise ValueError(f"LangFuse Failed to create generation: {str(e)}") @@ -443,7 +530,7 @@ class LangFuseDataTrace(BaseTraceInstance): def get_project_key(self): try: - projects = self.langfuse_client.client.projects.get() + projects = self.langfuse_client.api.projects.get() return projects.data[0].id except Exception as e: logger.debug("LangFuse get project key failed: %s", str(e)) diff --git a/api/core/ops/tencent_trace/client.py b/api/core/ops/tencent_trace/client.py index c39093bf4c..be06ab4a36 100644 --- a/api/core/ops/tencent_trace/client.py +++ b/api/core/ops/tencent_trace/client.py @@ -26,7 +26,13 @@ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExport from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor -from opentelemetry.semconv.resource import ResourceAttributes +from opentelemetry.semconv._incubating.attributes.deployment_attributes import ( # type: ignore[import-untyped] + DEPLOYMENT_ENVIRONMENT, +) +from opentelemetry.semconv._incubating.attributes.host_attributes import ( # type: ignore[import-untyped] + HOST_NAME, +) +from opentelemetry.semconv.attributes import service_attributes from opentelemetry.trace import SpanKind from opentelemetry.util.types import AttributeValue @@ -73,13 +79,13 @@ class TencentTraceClient: self.resource = Resource( attributes={ - ResourceAttributes.SERVICE_NAME: service_name, - ResourceAttributes.SERVICE_VERSION: f"dify-{dify_config.project.version}-{dify_config.COMMIT_SHA}", - ResourceAttributes.DEPLOYMENT_ENVIRONMENT: f"{dify_config.DEPLOY_ENV}-{dify_config.EDITION}", - ResourceAttributes.HOST_NAME: socket.gethostname(), - ResourceAttributes.TELEMETRY_SDK_LANGUAGE: "python", - ResourceAttributes.TELEMETRY_SDK_NAME: "opentelemetry", - ResourceAttributes.TELEMETRY_SDK_VERSION: _get_opentelemetry_sdk_version(), + service_attributes.SERVICE_NAME: service_name, + service_attributes.SERVICE_VERSION: f"dify-{dify_config.project.version}-{dify_config.COMMIT_SHA}", + DEPLOYMENT_ENVIRONMENT: f"{dify_config.DEPLOY_ENV}-{dify_config.EDITION}", + HOST_NAME: socket.gethostname(), + "telemetry.sdk.language": "python", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": _get_opentelemetry_sdk_version(), } ) # Prepare gRPC endpoint/metadata diff --git a/api/enterprise/telemetry/exporter.py b/api/enterprise/telemetry/exporter.py index b2f860764f..80959514f2 100644 --- a/api/enterprise/telemetry/exporter.py +++ b/api/enterprise/telemetry/exporter.py @@ -27,7 +27,10 @@ from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.sdk.trace.sampling import ParentBasedTraceIdRatio -from opentelemetry.semconv.resource import ResourceAttributes +from opentelemetry.semconv._incubating.attributes.host_attributes import ( # type: ignore[import-untyped] + HOST_NAME, +) +from opentelemetry.semconv.attributes import service_attributes from opentelemetry.trace import SpanContext, TraceFlags from opentelemetry.util.types import Attributes, AttributeValue @@ -114,8 +117,8 @@ class EnterpriseExporter: resource = Resource( attributes={ - ResourceAttributes.SERVICE_NAME: service_name, - ResourceAttributes.HOST_NAME: socket.gethostname(), + service_attributes.SERVICE_NAME: service_name, + HOST_NAME: socket.gethostname(), } ) sampler = ParentBasedTraceIdRatio(sampling_rate) diff --git a/api/extensions/ext_sentry.py b/api/extensions/ext_sentry.py index 651f8ed898..5cc58f27c4 100644 --- a/api/extensions/ext_sentry.py +++ b/api/extensions/ext_sentry.py @@ -6,15 +6,24 @@ def init_app(app: DifyApp): if dify_config.SENTRY_DSN: import sentry_sdk from graphon.model_runtime.errors.invoke import InvokeRateLimitError - from langfuse import parse_error from sentry_sdk.integrations.celery import CeleryIntegration from sentry_sdk.integrations.flask import FlaskIntegration from werkzeug.exceptions import HTTPException + try: + from langfuse._utils import parse_error + + _langfuse_error_response = parse_error.defaultErrorResponse + except (ImportError, AttributeError): + _langfuse_error_response = ( + "Unexpected error occurred. Please check your request" + " and contact support: https://langfuse.com/support." + ) + def before_send(event, hint): if "exc_info" in hint: _, exc_value, _ = hint["exc_info"] - if parse_error.defaultErrorResponse in str(exc_value): + if _langfuse_error_response in str(exc_value): return None return event @@ -27,7 +36,7 @@ def init_app(app: DifyApp): ValueError, FileNotFoundError, InvokeRateLimitError, - parse_error.defaultErrorResponse, + _langfuse_error_response, ], traces_sample_rate=dify_config.SENTRY_TRACES_SAMPLE_RATE, profiles_sample_rate=dify_config.SENTRY_PROFILES_SAMPLE_RATE, diff --git a/api/pyproject.toml b/api/pyproject.toml index f737d0699f..a09b474bf5 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ "httpx[socks]~=0.28.0", "jieba==0.42.1", "json-repair>=0.55.1", - "langfuse~=2.51.3", + "langfuse>=3.0.0,<5.0.0", "langsmith~=0.7.16", "markdown~=3.10.2", "mlflow-skinny>=3.0.0", @@ -41,23 +41,23 @@ dependencies = [ "openpyxl~=3.1.5", "opik~=1.10.37", "litellm==1.82.6", # Pinned to avoid madoka dependency issue - "opentelemetry-api==1.28.0", - "opentelemetry-distro==0.49b0", - "opentelemetry-exporter-otlp==1.28.0", - "opentelemetry-exporter-otlp-proto-common==1.28.0", - "opentelemetry-exporter-otlp-proto-grpc==1.28.0", - "opentelemetry-exporter-otlp-proto-http==1.28.0", - "opentelemetry-instrumentation==0.49b0", - "opentelemetry-instrumentation-celery==0.49b0", - "opentelemetry-instrumentation-flask==0.49b0", - "opentelemetry-instrumentation-httpx==0.49b0", - "opentelemetry-instrumentation-redis==0.49b0", - "opentelemetry-instrumentation-sqlalchemy==0.49b0", + "opentelemetry-api==1.40.0", + "opentelemetry-distro==0.61b0", + "opentelemetry-exporter-otlp==1.40.0", + "opentelemetry-exporter-otlp-proto-common==1.40.0", + "opentelemetry-exporter-otlp-proto-grpc==1.40.0", + "opentelemetry-exporter-otlp-proto-http==1.40.0", + "opentelemetry-instrumentation==0.61b0", + "opentelemetry-instrumentation-celery==0.61b0", + "opentelemetry-instrumentation-flask==0.61b0", + "opentelemetry-instrumentation-httpx==0.61b0", + "opentelemetry-instrumentation-redis==0.61b0", + "opentelemetry-instrumentation-sqlalchemy==0.61b0", "opentelemetry-propagator-b3==1.40.0", - "opentelemetry-proto==1.28.0", - "opentelemetry-sdk==1.28.0", - "opentelemetry-semantic-conventions==0.49b0", - "opentelemetry-util-http==0.49b0", + "opentelemetry-proto==1.40.0", + "opentelemetry-sdk==1.40.0", + "opentelemetry-semantic-conventions==0.61b0", + "opentelemetry-util-http==0.61b0", "pandas[excel,output-formatting,performance]~=3.0.1", "psycogreen~=1.0.2", "psycopg2-binary~=2.9.6", diff --git a/api/tests/unit_tests/core/ops/langfuse_trace/test_langfuse_trace.py b/api/tests/unit_tests/core/ops/langfuse_trace/test_langfuse_trace.py index 97f7a16327..374371fb42 100644 --- a/api/tests/unit_tests/core/ops/langfuse_trace/test_langfuse_trace.py +++ b/api/tests/unit_tests/core/ops/langfuse_trace/test_langfuse_trace.py @@ -521,11 +521,11 @@ def test_generate_name_trace(trace_instance): def test_add_trace_success(trace_instance): data = LangfuseTrace(id="t1", name="trace") trace_instance.add_trace(data) - trace_instance.langfuse_client.trace.assert_called_once() + trace_instance.langfuse_client.api.ingestion.batch.assert_called_once() def test_add_trace_error(trace_instance): - trace_instance.langfuse_client.trace.side_effect = Exception("error") + trace_instance.langfuse_client.api.ingestion.batch.side_effect = Exception("error") data = LangfuseTrace(id="t1", name="trace") with pytest.raises(ValueError, match="LangFuse Failed to create trace: error"): trace_instance.add_trace(data) @@ -534,11 +534,11 @@ def test_add_trace_error(trace_instance): def test_add_span_success(trace_instance): data = LangfuseSpan(id="s1", name="span", trace_id="t1") trace_instance.add_span(data) - trace_instance.langfuse_client.span.assert_called_once() + trace_instance.langfuse_client.api.ingestion.batch.assert_called_once() def test_add_span_error(trace_instance): - trace_instance.langfuse_client.span.side_effect = Exception("error") + trace_instance.langfuse_client.api.ingestion.batch.side_effect = Exception("error") data = LangfuseSpan(id="s1", name="span", trace_id="t1") with pytest.raises(ValueError, match="LangFuse Failed to create span: error"): trace_instance.add_span(data) @@ -554,11 +554,11 @@ def test_update_span(trace_instance): def test_add_generation_success(trace_instance): data = LangfuseGeneration(id="g1", name="gen", trace_id="t1") trace_instance.add_generation(data) - trace_instance.langfuse_client.generation.assert_called_once() + trace_instance.langfuse_client.api.ingestion.batch.assert_called_once() def test_add_generation_error(trace_instance): - trace_instance.langfuse_client.generation.side_effect = Exception("error") + trace_instance.langfuse_client.api.ingestion.batch.side_effect = Exception("error") data = LangfuseGeneration(id="g1", name="gen", trace_id="t1") with pytest.raises(ValueError, match="LangFuse Failed to create generation: error"): trace_instance.add_generation(data) @@ -585,12 +585,12 @@ def test_api_check_error(trace_instance): def test_get_project_key_success(trace_instance): mock_data = MagicMock() mock_data.id = "proj-1" - trace_instance.langfuse_client.client.projects.get.return_value = MagicMock(data=[mock_data]) + trace_instance.langfuse_client.api.projects.get.return_value = MagicMock(data=[mock_data]) assert trace_instance.get_project_key() == "proj-1" def test_get_project_key_error(trace_instance): - trace_instance.langfuse_client.client.projects.get.side_effect = Exception("fail") + trace_instance.langfuse_client.api.projects.get.side_effect = Exception("fail") with pytest.raises(ValueError, match="LangFuse get project key failed: fail"): trace_instance.get_project_key() diff --git a/api/uv.lock b/api/uv.lock index c4cf31e3f5..3e8d794866 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1700,30 +1700,30 @@ requires-dist = [ { name = "httpx-sse", specifier = "~=0.4.0" }, { name = "jieba", specifier = "==0.42.1" }, { name = "json-repair", specifier = ">=0.55.1" }, - { name = "langfuse", specifier = "~=2.51.3" }, + { name = "langfuse", specifier = ">=3.0.0,<5.0.0" }, { name = "langsmith", specifier = "~=0.7.16" }, { name = "litellm", specifier = "==1.82.6" }, { name = "markdown", specifier = "~=3.10.2" }, { name = "mlflow-skinny", specifier = ">=3.0.0" }, { name = "numpy", specifier = "~=1.26.4" }, { name = "openpyxl", specifier = "~=3.1.5" }, - { name = "opentelemetry-api", specifier = "==1.28.0" }, - { name = "opentelemetry-distro", specifier = "==0.49b0" }, - { name = "opentelemetry-exporter-otlp", specifier = "==1.28.0" }, - { name = "opentelemetry-exporter-otlp-proto-common", specifier = "==1.28.0" }, - { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = "==1.28.0" }, - { name = "opentelemetry-exporter-otlp-proto-http", specifier = "==1.28.0" }, - { name = "opentelemetry-instrumentation", specifier = "==0.49b0" }, - { name = "opentelemetry-instrumentation-celery", specifier = "==0.49b0" }, - { name = "opentelemetry-instrumentation-flask", specifier = "==0.49b0" }, - { name = "opentelemetry-instrumentation-httpx", specifier = "==0.49b0" }, - { name = "opentelemetry-instrumentation-redis", specifier = "==0.49b0" }, - { name = "opentelemetry-instrumentation-sqlalchemy", specifier = "==0.49b0" }, + { name = "opentelemetry-api", specifier = "==1.40.0" }, + { name = "opentelemetry-distro", specifier = "==0.61b0" }, + { name = "opentelemetry-exporter-otlp", specifier = "==1.40.0" }, + { name = "opentelemetry-exporter-otlp-proto-common", specifier = "==1.40.0" }, + { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = "==1.40.0" }, + { name = "opentelemetry-exporter-otlp-proto-http", specifier = "==1.40.0" }, + { name = "opentelemetry-instrumentation", specifier = "==0.61b0" }, + { name = "opentelemetry-instrumentation-celery", specifier = "==0.61b0" }, + { name = "opentelemetry-instrumentation-flask", specifier = "==0.61b0" }, + { name = "opentelemetry-instrumentation-httpx", specifier = "==0.61b0" }, + { name = "opentelemetry-instrumentation-redis", specifier = "==0.61b0" }, + { name = "opentelemetry-instrumentation-sqlalchemy", specifier = "==0.61b0" }, { name = "opentelemetry-propagator-b3", specifier = "==1.40.0" }, - { name = "opentelemetry-proto", specifier = "==1.28.0" }, - { name = "opentelemetry-sdk", specifier = "==1.28.0" }, - { name = "opentelemetry-semantic-conventions", specifier = "==0.49b0" }, - { name = "opentelemetry-util-http", specifier = "==0.49b0" }, + { name = "opentelemetry-proto", specifier = "==1.40.0" }, + { name = "opentelemetry-sdk", specifier = "==1.40.0" }, + { name = "opentelemetry-semantic-conventions", specifier = "==0.61b0" }, + { name = "opentelemetry-util-http", specifier = "==0.61b0" }, { name = "opik", specifier = "~=1.10.37" }, { name = "packaging", specifier = "~=23.2" }, { name = "pandas", extras = ["excel", "output-formatting", "performance"], specifier = "~=3.0.1" }, @@ -3393,20 +3393,22 @@ sdist = { url = "https://files.pythonhosted.org/packages/0e/72/a3add0e4eec4eb9e2 [[package]] name = "langfuse" -version = "2.51.5" +version = "4.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, { name = "backoff" }, { name = "httpx" }, - { name = "idna" }, + { name = "openai" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, { name = "packaging" }, { name = "pydantic" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/e9/22c9c05d877ab85da6d9008aaa7360f2a9ad58787a8e36e00b1b5be9a990/langfuse-2.51.5.tar.gz", hash = "sha256:55bc37b5c5d3ae133c1a95db09117cfb3117add110ba02ebbf2ce45ac4395c5b", size = 117574, upload-time = "2024-10-09T00:59:15.016Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/94/ab00e21fa5977d6b9c68fb3a95de2aa1a1e586964ff2af3e37405bf65d9f/langfuse-4.0.1.tar.gz", hash = "sha256:40a6daf3ab505945c314246d5b577d48fcfde0a47e8c05267ea6bd494ae9608e", size = 272749, upload-time = "2026-03-19T14:03:34.508Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/f7/242a13ca094c78464b7d4df77dfe7d4c44ed77b15fed3d2e3486afa5d2e1/langfuse-2.51.5-py3-none-any.whl", hash = "sha256:b95401ca710ef94b521afa6541933b6f93d7cfd4a97523c8fc75bca4d6d219fb", size = 214281, upload-time = "2024-10-09T00:59:12.596Z" }, + { url = "https://files.pythonhosted.org/packages/27/8f/3145ef00940f9c29d7e0200fd040f35616eac21c6ab4610a1ba14f3a04c1/langfuse-4.0.1-py3-none-any.whl", hash = "sha256:e22f49ea31304f97fc31a97c014ba63baa8802d9568295d54f06b00b43c30524", size = 465049, upload-time = "2026-03-19T14:03:32.527Z" }, ] [[package]] @@ -4200,95 +4202,95 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.28.0" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "deprecated" }, { name = "importlib-metadata" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/36/260eaea0f74fdd0c0d8f22ed3a3031109ea1c85531f94f4fde266c29e29a/opentelemetry_api-1.28.0.tar.gz", hash = "sha256:578610bcb8aa5cdcb11169d136cc752958548fb6ccffb0969c1036b0ee9e5353", size = 62803, upload-time = "2024-11-05T19:14:45.497Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/e4/3b25d8b856791c04d8a62b1257b5fc09dc41a057800db06885af8ddcdce1/opentelemetry_api-1.28.0-py3-none-any.whl", hash = "sha256:8457cd2c59ea1bd0988560f021656cecd254ad7ef6be4ba09dbefeca2409ce52", size = 64314, upload-time = "2024-11-05T19:14:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" }, ] [[package]] name = "opentelemetry-distro" -version = "0.49b0" +version = "0.61b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-sdk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/75/7cb7c33899e66bb366d40a889111a78c22df0951038b6699f1663e715a9f/opentelemetry_distro-0.49b0.tar.gz", hash = "sha256:1bafa274f9e83baa0d2a5d47ed02caffcf9bcca60107b389b145400d82b07513", size = 2560, upload-time = "2024-11-05T19:21:39.379Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/00/1f8acc51326956a596fefaf67751380001af36029132a7a07d4debce3c06/opentelemetry_distro-0.61b0.tar.gz", hash = "sha256:975b845f50181ad53753becf4fd4b123b54fa04df5a9d78812264436d6518981", size = 2590, upload-time = "2026-03-04T14:20:12.453Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/db/806172b6a4933966eee518db814b375e620602f7fe776b74ef795690f135/opentelemetry_distro-0.49b0-py3-none-any.whl", hash = "sha256:1af4074702f605ea210753dd41947dc2fd61b39724f23cdcf15d5654867cd3c2", size = 3318, upload-time = "2024-11-05T19:20:34.065Z" }, + { url = "https://files.pythonhosted.org/packages/56/2c/efcc995cd7484e6e55b1d26bd7fa6c55ca96bd415ff94310b52c19f330b0/opentelemetry_distro-0.61b0-py3-none-any.whl", hash = "sha256:f21d1ac0627549795d75e332006dd068877f00e461b1b2e8fe4568d6eb7b9590", size = 3349, upload-time = "2026-03-04T14:18:57.788Z" }, ] [[package]] name = "opentelemetry-exporter-otlp" -version = "1.28.0" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-exporter-otlp-proto-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/16/14e3fc163930ea68f0980a4cdd4ae5796e60aeb898965990e13263d64baf/opentelemetry_exporter_otlp-1.28.0.tar.gz", hash = "sha256:31ae7495831681dd3da34ac457f6970f147465ae4b9aae3a888d7a581c7cd868", size = 6170, upload-time = "2024-11-05T19:14:47.349Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/37/b6708e0eff5c5fb9aba2e0ea09f7f3bcbfd12a592d2a780241b5f6014df7/opentelemetry_exporter_otlp-1.40.0.tar.gz", hash = "sha256:7caa0870b95e2fcb59d64e16e2b639ecffb07771b6cd0000b5d12e5e4fef765a", size = 6152, upload-time = "2026-03-04T14:17:23.235Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/82/3f521b3c1f2a411ed60a24a8c9f486c1beeaf8c6c55337c87d3ae1642151/opentelemetry_exporter_otlp-1.28.0-py3-none-any.whl", hash = "sha256:1fd02d70f2c1b7ac5579c81e78de4594b188d3317c8ceb69e8b53900fb7b40fd", size = 7024, upload-time = "2024-11-05T19:14:24.534Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fc/aea77c28d9f3ffef2fdafdc3f4a235aee4091d262ddabd25882f47ce5c5f/opentelemetry_exporter_otlp-1.40.0-py3-none-any.whl", hash = "sha256:48c87e539ec9afb30dc443775a1334cc5487de2f72a770a4c00b1610bf6c697d", size = 7023, upload-time = "2026-03-04T14:17:03.612Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.28.0" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/8d/5d411084ac441052f4c9bae03a1aec65ae5d16b439fea7b9c5ac3842c013/opentelemetry_exporter_otlp_proto_common-1.28.0.tar.gz", hash = "sha256:5fa0419b0c8e291180b0fc8430a20dd44a3f3236f8e0827992145914f273ec4f", size = 18505, upload-time = "2024-11-05T19:14:48.204Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/bc/1559d46557fe6eca0b46c88d4c2676285f1f3be2e8d06bb5d15fbffc814a/opentelemetry_exporter_otlp_proto_common-1.40.0.tar.gz", hash = "sha256:1cbee86a4064790b362a86601ee7934f368b81cd4cc2f2e163902a6e7818a0fa", size = 20416, upload-time = "2026-03-04T14:17:23.801Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/72/3c44aabc74db325aaba09361b6a0d80f6d601f0ff86ecea8ee655c9538fc/opentelemetry_exporter_otlp_proto_common-1.28.0-py3-none-any.whl", hash = "sha256:467e6437d24e020156dffecece8c0a4471a8a60f6a34afeda7386df31a092410", size = 18403, upload-time = "2024-11-05T19:14:25.798Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ca/8f122055c97a932311a3f640273f084e738008933503d0c2563cd5d591fc/opentelemetry_exporter_otlp_proto_common-1.40.0-py3-none-any.whl", hash = "sha256:7081ff453835a82417bf38dccf122c827c3cbc94f2079b03bba02a3165f25149", size = 18369, upload-time = "2026-03-04T14:17:04.796Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.28.0" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "deprecated" }, { name = "googleapis-common-protos" }, { name = "grpcio" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-common" }, { name = "opentelemetry-proto" }, { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/4d/f215162e58041afb4bdf5dbd0d8faf0b7fc9bf7b3d3fc0e44e06f9e7e869/opentelemetry_exporter_otlp_proto_grpc-1.28.0.tar.gz", hash = "sha256:47a11c19dc7f4289e220108e113b7de90d59791cb4c37fc29f69a6a56f2c3735", size = 26237, upload-time = "2024-11-05T19:14:49.026Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/7f/b9e60435cfcc7590fa87436edad6822240dddbc184643a2a005301cc31f4/opentelemetry_exporter_otlp_proto_grpc-1.40.0.tar.gz", hash = "sha256:bd4015183e40b635b3dab8da528b27161ba83bf4ef545776b196f0fb4ec47740", size = 25759, upload-time = "2026-03-04T14:17:24.4Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/b5/afabc8106abc0f9cfeecf5b3e682622b3e04bba1d9b967dbfcd91b9c4ebe/opentelemetry_exporter_otlp_proto_grpc-1.28.0-py3-none-any.whl", hash = "sha256:edbdc53e7783f88d4535db5807cb91bd7b1ec9e9b9cdbfee14cd378f29a3b328", size = 18532, upload-time = "2024-11-05T19:14:26.853Z" }, + { url = "https://files.pythonhosted.org/packages/96/6f/7ee0980afcbdcd2d40362da16f7f9796bd083bf7f0b8e038abfbc0300f5d/opentelemetry_exporter_otlp_proto_grpc-1.40.0-py3-none-any.whl", hash = "sha256:2aa0ca53483fe0cf6405087a7491472b70335bc5c7944378a0a8e72e86995c52", size = 20304, upload-time = "2026-03-04T14:17:05.942Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-http" -version = "1.28.0" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "deprecated" }, { name = "googleapis-common-protos" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-common" }, { name = "opentelemetry-proto" }, { name = "opentelemetry-sdk" }, { name = "requests" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/2a/555f2845928086cd51aa6941c7a546470805b68ed631ec139ce7d841763d/opentelemetry_exporter_otlp_proto_http-1.28.0.tar.gz", hash = "sha256:d83a9a03a8367ead577f02a64127d827c79567de91560029688dd5cfd0152a8e", size = 15051, upload-time = "2024-11-05T19:14:49.813Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/fa/73d50e2c15c56be4d000c98e24221d494674b0cc95524e2a8cb3856d95a4/opentelemetry_exporter_otlp_proto_http-1.40.0.tar.gz", hash = "sha256:db48f5e0f33217588bbc00274a31517ba830da576e59503507c839b38fa0869c", size = 17772, upload-time = "2026-03-04T14:17:25.324Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/ce/80d5adabbf7ab4a0ca7b5e0f4039b24d273be370c3ba85fc05b13794411c/opentelemetry_exporter_otlp_proto_http-1.28.0-py3-none-any.whl", hash = "sha256:e8f3f7961b747edb6b44d51de4901a61e9c01d50debd747b120a08c4996c7e7b", size = 17228, upload-time = "2024-11-05T19:14:28.613Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3a/8865d6754e61c9fb170cdd530a124a53769ee5f740236064816eb0ca7301/opentelemetry_exporter_otlp_proto_http-1.40.0-py3-none-any.whl", hash = "sha256:a8d1dab28f504c5d96577d6509f80a8150e44e8f45f82cdbe0e34c99ab040069", size = 19960, upload-time = "2026-03-04T14:17:07.153Z" }, ] [[package]] name = "opentelemetry-instrumentation" -version = "0.49b0" +version = "0.61b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4296,14 +4298,14 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/6b/6c25b15063c92a011cf3f68375971e2c58a9c764690847edc97df2d94eeb/opentelemetry_instrumentation-0.49b0.tar.gz", hash = "sha256:398a93e0b9dc2d11cc8627e1761665c506fe08c6b2df252a2ab3ade53d751c46", size = 26478, upload-time = "2024-11-05T19:21:41.402Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/37/6bf8e66bfcee5d3c6515b79cb2ee9ad05fe573c20f7ceb288d0e7eeec28c/opentelemetry_instrumentation-0.61b0.tar.gz", hash = "sha256:cb21b48db738c9de196eba6b805b4ff9de3b7f187e4bbf9a466fa170514f1fc7", size = 32606, upload-time = "2026-03-04T14:20:16.825Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/61/e0d21e958d6072ce25c4f5e26a1d22835fc86f80836660adf6badb6038ce/opentelemetry_instrumentation-0.49b0-py3-none-any.whl", hash = "sha256:68364d73a1ff40894574cbc6138c5f98674790cae1f3b0865e21cf702f24dcb3", size = 30694, upload-time = "2024-11-05T19:20:38.584Z" }, + { url = "https://files.pythonhosted.org/packages/d8/3e/f6f10f178b6316de67f0dfdbbb699a24fbe8917cf1743c1595fb9dcdd461/opentelemetry_instrumentation-0.61b0-py3-none-any.whl", hash = "sha256:92a93a280e69788e8f88391247cc530fd81f16f2b011979d4d6398f805cfbc63", size = 33448, upload-time = "2026-03-04T14:19:02.447Z" }, ] [[package]] name = "opentelemetry-instrumentation-asgi" -version = "0.49b0" +version = "0.61b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, @@ -4312,28 +4314,28 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e8/55/693c3d0938ba5fead5c3aa4ac7022a992b4ff99a8e9979800d0feb843ff4/opentelemetry_instrumentation_asgi-0.49b0.tar.gz", hash = "sha256:959fd9b1345c92f20c6ef1d42f92ef6a76b3c3083fbc4104d59da6859b15b083", size = 24117, upload-time = "2024-11-05T19:21:46.769Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/3e/143cf5c034e58037307e6a24f06e0dd64b2c49ae60a965fc580027581931/opentelemetry_instrumentation_asgi-0.61b0.tar.gz", hash = "sha256:9d08e127244361dc33976d39dd4ca8f128b5aa5a7ae425208400a80a095019b5", size = 26691, upload-time = "2026-03-04T14:20:21.038Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/0b/7900c782a1dfaa584588d724bc3bbdf8405a32497537dd96b3fcbf8461b9/opentelemetry_instrumentation_asgi-0.49b0-py3-none-any.whl", hash = "sha256:722a90856457c81956c88f35a6db606cc7db3231046b708aae2ddde065723dbe", size = 16326, upload-time = "2024-11-05T19:20:46.176Z" }, + { url = "https://files.pythonhosted.org/packages/19/78/154470cf9d741a7487fbb5067357b87386475bbb77948a6707cae982e158/opentelemetry_instrumentation_asgi-0.61b0-py3-none-any.whl", hash = "sha256:e4b3ce6b66074e525e717efff20745434e5efd5d9df6557710856fba356da7a4", size = 16980, upload-time = "2026-03-04T14:19:10.894Z" }, ] [[package]] name = "opentelemetry-instrumentation-celery" -version = "0.49b0" +version = "0.61b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/8b/9b8a9dda3ed53354c6f707a45cdb7a4730e1c109b50fc1b413525493f811/opentelemetry_instrumentation_celery-0.49b0.tar.gz", hash = "sha256:afbaee97cc9c75f29bcc9784f16f8e37c415d4fe9b334748c5b90a3d30d12473", size = 14702, upload-time = "2024-11-05T19:21:53.672Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/43/e79108a804d16b1dc8ff28edd0e94ac393cf6359a5adcd7cdd2ec4be85f4/opentelemetry_instrumentation_celery-0.61b0.tar.gz", hash = "sha256:0e352a567dc89ed8bc083fc635035ce3c5b96bbbd92831ffd676e93b87f8e94f", size = 14780, upload-time = "2026-03-04T14:20:27.776Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/21/8c/d7d4adb36abbc0e517a69f7a069f32742122ae22d6017202f64570d9f4c5/opentelemetry_instrumentation_celery-0.49b0-py3-none-any.whl", hash = "sha256:38d4a78c78f33020032ef77ef0ead756bdf7838bcfb603de10f5925d39f14929", size = 13749, upload-time = "2024-11-05T19:20:54.98Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ed/c05f3c84b455654eb6c047474ffde61ed92efc24030f64213c98bca9d44b/opentelemetry_instrumentation_celery-0.61b0-py3-none-any.whl", hash = "sha256:01235733ff0cdf571cb03b270645abb14b9c8d830313dc5842097ec90146320b", size = 13856, upload-time = "2026-03-04T14:19:20.98Z" }, ] [[package]] name = "opentelemetry-instrumentation-fastapi" -version = "0.49b0" +version = "0.61b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4342,14 +4344,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/bf/8e6d2a4807360f2203192017eb4845f5628dbeaf0597adf3d141cc5c24e1/opentelemetry_instrumentation_fastapi-0.49b0.tar.gz", hash = "sha256:6d14935c41fd3e49328188b6a59dd4c37bd17a66b01c15b0c64afa9714a1f905", size = 19230, upload-time = "2024-11-05T19:21:59.361Z" } +sdist = { url = "https://files.pythonhosted.org/packages/37/35/aa727bb6e6ef930dcdc96a617b83748fece57b43c47d83ba8d83fbeca657/opentelemetry_instrumentation_fastapi-0.61b0.tar.gz", hash = "sha256:3a24f35b07c557ae1bbc483bf8412221f25d79a405f8b047de8b670722e2fa9f", size = 24800, upload-time = "2026-03-04T14:20:32.759Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/f4/0895b9410c10abf987c90dee1b7688a8f2214a284fe15e575648f6a1473a/opentelemetry_instrumentation_fastapi-0.49b0-py3-none-any.whl", hash = "sha256:646e1b18523cbe6860ae9711eb2c7b9c85466c3c7697cd6b8fb5180d85d3fe6e", size = 12101, upload-time = "2024-11-05T19:21:01.805Z" }, + { url = "https://files.pythonhosted.org/packages/91/05/acfeb2cccd434242a0a7d0ea29afaf077e04b42b35b485d89aee4e0d9340/opentelemetry_instrumentation_fastapi-0.61b0-py3-none-any.whl", hash = "sha256:a1a844d846540d687d377516b2ff698b51d87c781b59f47c214359c4a241047c", size = 13485, upload-time = "2026-03-04T14:19:30.351Z" }, ] [[package]] name = "opentelemetry-instrumentation-flask" -version = "0.49b0" +version = "0.61b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4359,14 +4361,14 @@ dependencies = [ { name = "opentelemetry-util-http" }, { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/12/dc72873fb1e35699941d8eb6a53ef25e8c5843dea37665dad33bd720f047/opentelemetry_instrumentation_flask-0.49b0.tar.gz", hash = "sha256:f7c5ab67753c4781a2e21c8f43dc5fc02ece74fdd819466c75d025db80aa7576", size = 19176, upload-time = "2024-11-05T19:22:00.816Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/33/d6852d8f2c3eef86f2f8c858d6f5315983c7063e07e595519e96d4c31c06/opentelemetry_instrumentation_flask-0.61b0.tar.gz", hash = "sha256:e9faf58dfd9860a1868442d180142645abdafc1a652dd73d469a5efd106a7d49", size = 24071, upload-time = "2026-03-04T14:20:33.437Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/fc/354da8f33ef0daebfc8e4eac995d342ae13a35097bbad512cfe0d2f3c61a/opentelemetry_instrumentation_flask-0.49b0-py3-none-any.whl", hash = "sha256:f3ef330c3cee3e2c161f27f1e7017c8800b9bfb6f9204f2f7bfb0b274874be0e", size = 14582, upload-time = "2024-11-05T19:21:02.793Z" }, + { url = "https://files.pythonhosted.org/packages/3e/41/619f3530324a58491f2d20f216a10dd7393629b29db4610dda642a27f4ed/opentelemetry_instrumentation_flask-0.61b0-py3-none-any.whl", hash = "sha256:e8ce474d7ce543bfbbb3e93f8a6f8263348af9d7b45502f387420cf3afa71253", size = 15996, upload-time = "2026-03-04T14:19:31.304Z" }, ] [[package]] name = "opentelemetry-instrumentation-httpx" -version = "0.49b0" +version = "0.61b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4375,14 +4377,14 @@ dependencies = [ { name = "opentelemetry-util-http" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a0/53/8b5e05e55a513d846ead5afb0509bec37a34a1c3e82f30b13d14156334b1/opentelemetry_instrumentation_httpx-0.49b0.tar.gz", hash = "sha256:07165b624f3e58638cee47ecf1c81939a8c2beb7e42ce9f69e25a9f21dc3f4cf", size = 17750, upload-time = "2024-11-05T19:22:02.911Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/2a/e2becd55e33c29d1d9ef76e2579040ed1951cb33bacba259f6aff2fdd2a6/opentelemetry_instrumentation_httpx-0.61b0.tar.gz", hash = "sha256:6569ec097946c5551c2a4252f74c98666addd1bf047c1dde6b4ef426719ff8dd", size = 24104, upload-time = "2026-03-04T14:20:34.752Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/9f/843391c6d645cd4f6914b27bc807fc1ff52b97f84cbe3ca675641976b23f/opentelemetry_instrumentation_httpx-0.49b0-py3-none-any.whl", hash = "sha256:e59e0d2fda5ef841630c68da1d78ff9192f63590a9099f12f0eab614abdf239a", size = 14110, upload-time = "2024-11-05T19:21:04.698Z" }, + { url = "https://files.pythonhosted.org/packages/af/88/dde310dce56e2d85cf1a09507f5888544955309edc4b8d22971d6d3d1417/opentelemetry_instrumentation_httpx-0.61b0-py3-none-any.whl", hash = "sha256:dee05c93a6593a5dc3ae5d9d5c01df8b4e2c5d02e49275e5558534ee46343d5e", size = 17198, upload-time = "2026-03-04T14:19:33.585Z" }, ] [[package]] name = "opentelemetry-instrumentation-redis" -version = "0.49b0" +version = "0.61b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4390,14 +4392,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/5b/1398eb2f92fd76787ccec28d24dc4c7dfaaf97a7557e7729e2f7c2c05d84/opentelemetry_instrumentation_redis-0.49b0.tar.gz", hash = "sha256:922542c3bd192ad4ba74e2c7e0a253c7c58a5cefbd6f89da2aba4d193a974703", size = 11353, upload-time = "2024-11-05T19:22:12.822Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/21/26205f89358a5f2be3ee5512d3d3bce16b622977f64aeaa9d3fa8887dd39/opentelemetry_instrumentation_redis-0.61b0.tar.gz", hash = "sha256:ae0fbb56be9a641e621d55b02a7d62977a2c77c5ee760addd79b9b266e46e523", size = 14781, upload-time = "2026-03-04T14:20:45.694Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/e4/4f258fef0759629f2e8a0210d5533cfef3ecad69ff35be044637a3e2783e/opentelemetry_instrumentation_redis-0.49b0-py3-none-any.whl", hash = "sha256:b7d8f758bac53e77b7e7ca98ce80f91230577502dacb619ebe8e8b6058042067", size = 12453, upload-time = "2024-11-05T19:21:18.534Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e1/8f4c8e4194291dbe828aeabe779050a8497b379ad90040a5a0a7074b1d08/opentelemetry_instrumentation_redis-0.61b0-py3-none-any.whl", hash = "sha256:8d4e850bbb5f8eeafa44c0eac3a007990c7125de187bc9c3659e29ff7e091172", size = 15506, upload-time = "2026-03-04T14:19:48.588Z" }, ] [[package]] name = "opentelemetry-instrumentation-sqlalchemy" -version = "0.49b0" +version = "0.61b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4406,14 +4408,14 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a0/a7/24f6cce3808ae1802dd1b60d752fbab877db5655198929cf4ee8ea416923/opentelemetry_instrumentation_sqlalchemy-0.49b0.tar.gz", hash = "sha256:32658e520fc8b35823c722f5d8831d3a410b76dd2724adb2887befc041ddef04", size = 13194, upload-time = "2024-11-05T19:22:14.92Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/4f/3a325b180944610697a0a926d49d782b41a86120050d44fefb2715b630ac/opentelemetry_instrumentation_sqlalchemy-0.61b0.tar.gz", hash = "sha256:13a3a159a2043a52f0180b3757fbaa26741b0e08abb50deddce4394c118956e6", size = 15343, upload-time = "2026-03-04T14:20:47.648Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/6b/a1a3685fed593282999cdc374ece15efbd56f8d774bd368bf7ff2cf5923c/opentelemetry_instrumentation_sqlalchemy-0.49b0-py3-none-any.whl", hash = "sha256:d854052d2b02cd0562e5628a514c8153fceada7f585137e173165dfd0a46ef6a", size = 13358, upload-time = "2024-11-05T19:21:23.654Z" }, + { url = "https://files.pythonhosted.org/packages/1f/97/b906a930c6a1a20c53ecc8b58cabc2cdd0ce560a2b5d44259084ffe4333e/opentelemetry_instrumentation_sqlalchemy-0.61b0-py3-none-any.whl", hash = "sha256:f115e0be54116ba4c327b8d7b68db4045ee18d44439d888ab8130a549c50d1c1", size = 14547, upload-time = "2026-03-04T14:19:53.088Z" }, ] [[package]] name = "opentelemetry-instrumentation-wsgi" -version = "0.49b0" +version = "0.61b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4421,9 +4423,9 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/2b/91b022b004ac9e9ab0eefd10bc4257975291f88adc81b4ef2c601ddb1adf/opentelemetry_instrumentation_wsgi-0.49b0.tar.gz", hash = "sha256:0812a02e132f8fc3d5c897bba84e530c37b85c315b199bb97ca6508279e7eb23", size = 17733, upload-time = "2024-11-05T19:22:24.3Z" } +sdist = { url = "https://files.pythonhosted.org/packages/89/e5/189f2845362cfe78e356ba127eab21456309def411c6874aa4800c3de816/opentelemetry_instrumentation_wsgi-0.61b0.tar.gz", hash = "sha256:380f2ae61714e5303275a80b2e14c58571573cd1fddf496d8c39fb9551c5e532", size = 19898, upload-time = "2026-03-04T14:20:54.068Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/1d/59979665778ed8c85bc31c92b75571cd7afb8e3322fb513c87fe1bad6d78/opentelemetry_instrumentation_wsgi-0.49b0-py3-none-any.whl", hash = "sha256:8869ccf96611827e4448417718920e9eec6d25bffb5bf72c7952c7346ec33fbc", size = 13699, upload-time = "2024-11-05T19:21:35.039Z" }, + { url = "https://files.pythonhosted.org/packages/96/75/d6b42ba26f3c921be6d01b16561b7bb863f843bad7ac3a5011f62617bcab/opentelemetry_instrumentation_wsgi-0.61b0-py3-none-any.whl", hash = "sha256:bd33b0824166f24134a3400648805e8d2e6a7951f070241294e8b8866611d7fa", size = 14628, upload-time = "2026-03-04T14:20:03.934Z" }, ] [[package]] @@ -4441,50 +4443,50 @@ wheels = [ [[package]] name = "opentelemetry-proto" -version = "1.28.0" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/63/ac4cef4d30ea0ca1d2153ad2fc62d91d1cf3b89b0e4e5cbd61a8c567885f/opentelemetry_proto-1.28.0.tar.gz", hash = "sha256:4a45728dfefa33f7908b828b9b7c9f2c6de42a05d5ec7b285662ddae71c4c870", size = 34331, upload-time = "2024-11-05T19:14:59.503Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/77/dd38991db037fdfce45849491cb61de5ab000f49824a00230afb112a4392/opentelemetry_proto-1.40.0.tar.gz", hash = "sha256:03f639ca129ba513f5819810f5b1f42bcb371391405d99c168fe6937c62febcd", size = 45667, upload-time = "2026-03-04T14:17:31.194Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/94/c0b43d16e1d96ee1e699373aa59f14a3aa2e7126af3f11d6adc5dcc531cd/opentelemetry_proto-1.28.0-py3-none-any.whl", hash = "sha256:d5ad31b997846543b8e15504657d9a8cf1ad3c71dcbbb6c4799b1ab29e38f7f9", size = 55832, upload-time = "2024-11-05T19:14:40.446Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b2/189b2577dde745b15625b3214302605b1353436219d42b7912e77fa8dc24/opentelemetry_proto-1.40.0-py3-none-any.whl", hash = "sha256:266c4385d88923a23d63e353e9761af0f47a6ed0d486979777fe4de59dc9b25f", size = 72073, upload-time = "2026-03-04T14:17:16.673Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.28.0" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0c/5b/a509ccab93eacc6044591d5ec437d8266e76f893d0389bbf7e5592c7da32/opentelemetry_sdk-1.28.0.tar.gz", hash = "sha256:41d5420b2e3fb7716ff4981b510d551eff1fc60eb5a95cf7335b31166812a893", size = 156155, upload-time = "2024-11-05T19:15:00.451Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/fd/3c3125b20ba18ce2155ba9ea74acb0ae5d25f8cd39cfd37455601b7955cc/opentelemetry_sdk-1.40.0.tar.gz", hash = "sha256:18e9f5ec20d859d268c7cb3c5198c8d105d073714db3de50b593b8c1345a48f2", size = 184252, upload-time = "2026-03-04T14:17:31.87Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/fe/c8decbebb5660529f1d6ba65e50a45b1294022dfcba2968fc9c8697c42b2/opentelemetry_sdk-1.28.0-py3-none-any.whl", hash = "sha256:4b37da81d7fad67f6683c4420288c97f4ed0d988845d5886435f428ec4b8429a", size = 118692, upload-time = "2024-11-05T19:14:41.669Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c5/6a852903d8bfac758c6dc6e9a68b015d3c33f2f1be5e9591e0f4b69c7e0a/opentelemetry_sdk-1.40.0-py3-none-any.whl", hash = "sha256:787d2154a71f4b3d81f20524a8ce061b7db667d24e46753f32a7bc48f1c1f3f1", size = 141951, upload-time = "2026-03-04T14:17:17.961Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.49b0" +version = "0.61b0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "deprecated" }, { name = "opentelemetry-api" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ee/c8/433b0e54143f8c9369f5c4a7a83e73eec7eb2ee7d0b7e81a9243e78c8e80/opentelemetry_semantic_conventions-0.49b0.tar.gz", hash = "sha256:dbc7b28339e5390b6b28e022835f9bac4e134a80ebf640848306d3c5192557e8", size = 95227, upload-time = "2024-11-05T19:15:01.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/c0/4ae7973f3c2cfd2b6e321f1675626f0dab0a97027cc7a297474c9c8f3d04/opentelemetry_semantic_conventions-0.61b0.tar.gz", hash = "sha256:072f65473c5d7c6dc0355b27d6c9d1a679d63b6d4b4b16a9773062cb7e31192a", size = 145755, upload-time = "2026-03-04T14:17:32.664Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/05/20104df4ef07d3bf5c3fd6bcc796ef70ab4ea4309378a9ba57bc4b4d01fa/opentelemetry_semantic_conventions-0.49b0-py3-none-any.whl", hash = "sha256:0458117f6ead0b12e3221813e3e511d85698c31901cac84682052adb9c17c7cd", size = 159214, upload-time = "2024-11-05T19:14:43.047Z" }, + { url = "https://files.pythonhosted.org/packages/b2/37/cc6a55e448deaa9b27377d087da8615a3416d8ad523d5960b78dbeadd02a/opentelemetry_semantic_conventions-0.61b0-py3-none-any.whl", hash = "sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2", size = 231621, upload-time = "2026-03-04T14:17:19.33Z" }, ] [[package]] name = "opentelemetry-util-http" -version = "0.49b0" +version = "0.61b0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/99/377ef446928808211b127b9ab31c348bc465c8da4514ebeec6e4a3de3d21/opentelemetry_util_http-0.49b0.tar.gz", hash = "sha256:02928496afcffd58a7c15baf99d2cedae9b8325a8ac52b0d0877b2e8f936dd1b", size = 7863, upload-time = "2024-11-05T19:22:26.973Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/3c/f0196223efc5c4ca19f8fad3d5462b171ac6333013335ce540c01af419e9/opentelemetry_util_http-0.61b0.tar.gz", hash = "sha256:1039cb891334ad2731affdf034d8fb8b48c239af9b6dd295e5fabd07f1c95572", size = 11361, upload-time = "2026-03-04T14:20:57.01Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/0e/ab0a89b315d0bacdd355a345bb69b20c50fc1f0804b52b56fe1c35a60e68/opentelemetry_util_http-0.49b0-py3-none-any.whl", hash = "sha256:8661bbd6aea1839badc44de067ec9c15c05eab05f729f496c856c50a1203caf1", size = 6945, upload-time = "2024-11-05T19:21:37.81Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e5/c08aaaf2f64288d2b6ef65741d2de5454e64af3e050f34285fb1907492fe/opentelemetry_util_http-0.61b0-py3-none-any.whl", hash = "sha256:8e715e848233e9527ea47e275659ea60a57a75edf5206a3b937e236a6da5fc33", size = 9281, upload-time = "2026-03-04T14:20:08.364Z" }, ] [[package]] From 51f6ca2bed96cbc736371fbce22786ddf773ad65 Mon Sep 17 00:00:00 2001 From: Wu Tianwei <30284043+WTW0313@users.noreply.github.com> Date: Mon, 30 Mar 2026 21:07:20 +0800 Subject: [PATCH 007/199] fix(workflow): improve node organization (#34276) --- .../use-workflow-interactions.spec.tsx | 6 +- .../workflow/hooks/use-workflow-organize.ts | 4 +- .../utils/__tests__/elk-layout.spec.ts | 68 ++--- .../components/workflow/utils/elk-layout.ts | 233 +++++++----------- 4 files changed, 123 insertions(+), 188 deletions(-) diff --git a/web/app/components/workflow/hooks/__tests__/use-workflow-interactions.spec.tsx b/web/app/components/workflow/hooks/__tests__/use-workflow-interactions.spec.tsx index 457b54e763..95dc3dff00 100644 --- a/web/app/components/workflow/hooks/__tests__/use-workflow-interactions.spec.tsx +++ b/web/app/components/workflow/hooks/__tests__/use-workflow-interactions.spec.tsx @@ -28,7 +28,7 @@ const mockHandleEdgeCancelRunningStatus = vi.hoisted(() => vi.fn()) const mockHandleSyncWorkflowDraft = vi.hoisted(() => vi.fn()) const mockSaveStateToHistory = vi.hoisted(() => vi.fn()) const mockGetLayoutForChildNodes = vi.hoisted(() => vi.fn()) -const mockGetLayoutByDagre = vi.hoisted(() => vi.fn()) +const mockGetLayoutByELK = vi.hoisted(() => vi.fn()) const mockInitialNodes = vi.hoisted(() => vi.fn((nodes: unknown[], _edges: unknown[]) => nodes)) const mockInitialEdges = vi.hoisted(() => vi.fn((edges: unknown[], _nodes: unknown[]) => edges)) @@ -112,7 +112,7 @@ vi.mock('../use-workflow-history', () => ({ vi.mock('../../utils', async importOriginal => ({ ...(await importOriginal()), getLayoutForChildNodes: (...args: unknown[]) => mockGetLayoutForChildNodes(...args), - getLayoutByDagre: (...args: unknown[]) => mockGetLayoutByDagre(...args), + getLayoutByELK: (...args: unknown[]) => mockGetLayoutByELK(...args), initialNodes: (nodes: unknown[], edges: unknown[]) => mockInitialNodes(nodes, edges), initialEdges: (edges: unknown[], nodes: unknown[]) => mockInitialEdges(edges, nodes), })) @@ -203,7 +203,7 @@ describe('use-workflow-interactions exports', () => { ['loop-child', { x: 40, y: 60, width: 100, height: 60 }], ]), }) - mockGetLayoutByDagre.mockResolvedValue({ + mockGetLayoutByELK.mockResolvedValue({ nodes: new Map([ ['loop-node', { x: 10, y: 20, width: 360, height: 260, layer: 0 }], ['top-node', { x: 500, y: 30, width: 240, height: 100, layer: 0 }], diff --git a/web/app/components/workflow/hooks/use-workflow-organize.ts b/web/app/components/workflow/hooks/use-workflow-organize.ts index 284fb2261c..da158a4214 100644 --- a/web/app/components/workflow/hooks/use-workflow-organize.ts +++ b/web/app/components/workflow/hooks/use-workflow-organize.ts @@ -2,7 +2,7 @@ import { useCallback } from 'react' import { useReactFlow, useStoreApi } from 'reactflow' import { useWorkflowStore } from '../store' import { - getLayoutByDagre, + getLayoutByELK, getLayoutForChildNodes, } from '../utils' import { useNodesSyncDraft } from './use-nodes-sync-draft' @@ -49,7 +49,7 @@ export const useWorkflowOrganize = () => { nodes, getContainerSizeChanges(parentNodes, childLayoutsMap), ) - const layout = await getLayoutByDagre(nodesWithUpdatedSizes, edges) + const layout = await getLayoutByELK(nodesWithUpdatedSizes, edges) const nextNodes = applyLayoutToNodes({ nodes: nodesWithUpdatedSizes, layout, diff --git a/web/app/components/workflow/utils/__tests__/elk-layout.spec.ts b/web/app/components/workflow/utils/__tests__/elk-layout.spec.ts index 662b380f5d..1a3c52ec2d 100644 --- a/web/app/components/workflow/utils/__tests__/elk-layout.spec.ts +++ b/web/app/components/workflow/utils/__tests__/elk-layout.spec.ts @@ -5,7 +5,7 @@ import { CUSTOM_ITERATION_START_NODE } from '../../nodes/iteration-start/constan import { CUSTOM_LOOP_START_NODE } from '../../nodes/loop-start/constants' import { BlockEnum } from '../../types' -type ElkChild = Record & { id: string, width?: number, height?: number, x?: number, y?: number, children?: ElkChild[], ports?: Array<{ id: string }>, layoutOptions?: Record } +type ElkChild = Record & { id: string, width?: number, height?: number, x?: number, y?: number, children?: ElkChild[], ports?: Array<{ id: string, layoutOptions?: Record }>, layoutOptions?: Record } type ElkGraph = Record & { id: string, children?: ElkChild[], edges?: Array> } let layoutCallArgs: ElkGraph | null = null @@ -32,7 +32,7 @@ vi.mock('elkjs/lib/elk.bundled.js', () => { } }) -const { getLayoutByDagre, getLayoutForChildNodes } = await import('../elk-layout') +const { getLayoutByELK, getLayoutForChildNodes } = await import('../elk-layout') function makeWorkflowNode(overrides: Omit, 'data'> & { data?: Partial & Record } = {}): Node { return createNode({ @@ -51,7 +51,7 @@ beforeEach(() => { mockReturnOverride = null }) -describe('getLayoutByDagre', () => { +describe('getLayoutByELK', () => { it('should return layout for simple linear graph', async () => { const nodes = [ makeWorkflowNode({ id: 'a', data: { type: BlockEnum.Start, title: '', desc: '' } }), @@ -59,7 +59,7 @@ describe('getLayoutByDagre', () => { ] const edges = [makeWorkflowEdge({ source: 'a', target: 'b' })] - const result = await getLayoutByDagre(nodes, edges) + const result = await getLayoutByELK(nodes, edges) expect(result.nodes.size).toBe(2) expect(result.nodes.has('a')).toBe(true) @@ -74,7 +74,7 @@ describe('getLayoutByDagre', () => { makeWorkflowNode({ id: 'child', data: { type: BlockEnum.Code, title: '', desc: '' }, parentId: 'a' }), ] - const result = await getLayoutByDagre(nodes, []) + const result = await getLayoutByELK(nodes, []) expect(result.nodes.size).toBe(1) expect(result.nodes.has('child')).toBe(false) }) @@ -85,7 +85,7 @@ describe('getLayoutByDagre', () => { makeWorkflowNode({ id: 'iter-start', type: CUSTOM_ITERATION_START_NODE, data: { type: BlockEnum.IterationStart, title: '', desc: '' } }), ] - const result = await getLayoutByDagre(nodes, []) + const result = await getLayoutByELK(nodes, []) expect(result.nodes.size).toBe(1) }) @@ -98,7 +98,7 @@ describe('getLayoutByDagre', () => { makeWorkflowEdge({ source: 'a', target: 'b', data: { isInIteration: true, iteration_id: 'iter-1' } }), ] - await getLayoutByDagre(nodes, edges) + await getLayoutByELK(nodes, edges) expect(layoutCallArgs!.edges).toHaveLength(0) }) @@ -107,7 +107,7 @@ describe('getLayoutByDagre', () => { Reflect.deleteProperty(node, 'width') Reflect.deleteProperty(node, 'height') - const result = await getLayoutByDagre([node], []) + const result = await getLayoutByELK([node], []) expect(result.nodes.size).toBe(1) const info = result.nodes.get('a')! expect(info.width).toBe(244) @@ -133,13 +133,13 @@ describe('getLayoutByDagre', () => { makeWorkflowEdge({ id: 'e2', source: 'if-1', target: 'c', sourceHandle: 'false' }), ] - await getLayoutByDagre(nodes, edges) + await getLayoutByELK(nodes, edges) const ifElkNode = layoutCallArgs!.children!.find((c: ElkChild) => c.id === 'if-1')! expect(ifElkNode.ports).toHaveLength(2) expect(ifElkNode.layoutOptions!['elk.portConstraints']).toBe('FIXED_ORDER') }) - it('should use normal node for IfElse with single branch', async () => { + it('should build ports for IfElse even with single branch', async () => { const nodes = [ makeWorkflowNode({ id: 'if-1', @@ -149,9 +149,10 @@ describe('getLayoutByDagre', () => { ] const edges = [makeWorkflowEdge({ source: 'if-1', target: 'b', sourceHandle: 'case-1' })] - await getLayoutByDagre(nodes, edges) + await getLayoutByELK(nodes, edges) const ifElkNode = layoutCallArgs!.children!.find((c: ElkChild) => c.id === 'if-1')! - expect(ifElkNode.ports).toBeUndefined() + expect(ifElkNode.ports).toHaveLength(1) + expect(ifElkNode.ports![0].layoutOptions!['elk.port.side']).toBe('EAST') }) it('should build ports for HumanInput nodes with multiple branches', async () => { @@ -168,12 +169,12 @@ describe('getLayoutByDagre', () => { makeWorkflowEdge({ id: 'e2', source: 'hi-1', target: 'c', sourceHandle: '__timeout' }), ] - await getLayoutByDagre(nodes, edges) + await getLayoutByELK(nodes, edges) const hiElkNode = layoutCallArgs!.children!.find((c: ElkChild) => c.id === 'hi-1')! expect(hiElkNode.ports).toHaveLength(2) }) - it('should use normal node for HumanInput with single branch', async () => { + it('should build ports for HumanInput even with single branch', async () => { const nodes = [ makeWorkflowNode({ id: 'hi-1', @@ -183,20 +184,21 @@ describe('getLayoutByDagre', () => { ] const edges = [makeWorkflowEdge({ source: 'hi-1', target: 'b', sourceHandle: 'action-1' })] - await getLayoutByDagre(nodes, edges) + await getLayoutByELK(nodes, edges) const hiElkNode = layoutCallArgs!.children!.find((c: ElkChild) => c.id === 'hi-1')! - expect(hiElkNode.ports).toBeUndefined() + expect(hiElkNode.ports).toHaveLength(1) + expect(hiElkNode.ports![0].layoutOptions!['elk.port.side']).toBe('EAST') }) it('should normalise bounds so minX and minY start at 0', async () => { const nodes = [makeWorkflowNode({ id: 'a', data: { type: BlockEnum.Start, title: '', desc: '' } })] - const result = await getLayoutByDagre(nodes, []) + const result = await getLayoutByELK(nodes, []) expect(result.bounds.minX).toBe(0) expect(result.bounds.minY).toBe(0) }) it('should return empty layout when no nodes match filter', async () => { - const result = await getLayoutByDagre([], []) + const result = await getLayoutByELK([], []) expect(result.nodes.size).toBe(0) expect(result.bounds).toEqual({ minX: 0, minY: 0, maxX: 0, maxY: 0 }) }) @@ -225,7 +227,7 @@ describe('getLayoutByDagre', () => { makeWorkflowEdge({ id: 'e-b', source: 'if-1', target: 'y', sourceHandle: 'case-b' }), ] - await getLayoutByDagre(nodes, edges) + await getLayoutByELK(nodes, edges) const ifNode = layoutCallArgs!.children!.find((c: ElkChild) => c.id === 'if-1')! const portIds = ifNode.ports!.map((p: { id: string }) => p.id) expect(portIds[portIds.length - 1]).toContain('false') @@ -247,7 +249,7 @@ describe('getLayoutByDagre', () => { makeWorkflowEdge({ id: 'e-a2', source: 'hi-1', target: 'y', sourceHandle: 'a2' }), ] - await getLayoutByDagre(nodes, edges) + await getLayoutByELK(nodes, edges) const hiNode = layoutCallArgs!.children!.find((c: ElkChild) => c.id === 'hi-1')! const portIds = hiNode.ports!.map((p: { id: string }) => p.id) expect(portIds[portIds.length - 1]).toContain('__timeout') @@ -267,7 +269,7 @@ describe('getLayoutByDagre', () => { makeWorkflowEdge({ id: 'e2', source: 'if-1', target: 'c', sourceHandle: 'false' }), ] - await getLayoutByDagre(nodes, edges) + await getLayoutByELK(nodes, edges) const portEdges = layoutCallArgs!.edges!.filter((e: Record) => e.sourcePort) expect(portEdges.length).toBeGreaterThan(0) }) @@ -286,7 +288,7 @@ describe('getLayoutByDagre', () => { Reflect.deleteProperty(e1, 'sourceHandle') Reflect.deleteProperty(e2, 'sourceHandle') - const result = await getLayoutByDagre(nodes, [e1, e2]) + const result = await getLayoutByELK(nodes, [e1, e2]) expect(result.nodes.size).toBeGreaterThan(0) }) @@ -299,7 +301,7 @@ describe('getLayoutByDagre', () => { }) const nodes = [makeWorkflowNode({ id: 'a', data: { type: BlockEnum.Start, title: '', desc: '' } })] - const result = await getLayoutByDagre(nodes, []) + const result = await getLayoutByELK(nodes, []) const info = result.nodes.get('a')! expect(info.x).toBe(0) expect(info.y).toBe(0) @@ -326,7 +328,7 @@ describe('getLayoutByDagre', () => { makeWorkflowNode({ id: 'a', data: { type: BlockEnum.Start, title: '', desc: '' } }), makeWorkflowNode({ id: 'b', data: { type: BlockEnum.Code, title: '', desc: '' } }), ] - const result = await getLayoutByDagre(nodes, []) + const result = await getLayoutByELK(nodes, []) expect(result.nodes.get('a')!.layer).toBe(0) expect(result.nodes.get('b')!.layer).toBe(1) }) @@ -354,7 +356,7 @@ describe('getLayoutByDagre', () => { makeWorkflowNode({ id: 'nested-1', data: { type: BlockEnum.Code, title: '', desc: '' } }), makeWorkflowNode({ id: 'nested-2', data: { type: BlockEnum.Code, title: '', desc: '' } }), ] - const result = await getLayoutByDagre(nodes, []) + const result = await getLayoutByELK(nodes, []) expect(result.nodes.has('nested-1')).toBe(true) expect(result.nodes.has('nested-2')).toBe(true) }) @@ -372,7 +374,7 @@ describe('getLayoutByDagre', () => { makeWorkflowNode({ id: 'visible', data: { type: BlockEnum.Start, title: '', desc: '' } }), makeWorkflowNode({ id: 'also-visible', data: { type: BlockEnum.Code, title: '', desc: '' } }), ] - const result = await getLayoutByDagre(nodes, []) + const result = await getLayoutByELK(nodes, []) expect(result.nodes.size).toBe(2) }) @@ -390,7 +392,7 @@ describe('getLayoutByDagre', () => { makeWorkflowEdge({ id: 'e2', source: 'if-1', target: 'c', sourceHandle: 'other-unknown' }), ] - await getLayoutByDagre(nodes, edges) + await getLayoutByELK(nodes, edges) const ifNode = layoutCallArgs!.children!.find((c: ElkChild) => c.id === 'if-1')! expect(ifNode.ports).toHaveLength(2) }) @@ -409,7 +411,7 @@ describe('getLayoutByDagre', () => { makeWorkflowEdge({ id: 'e2', source: 'hi-1', target: 'c', sourceHandle: 'another-unknown' }), ] - await getLayoutByDagre(nodes, edges) + await getLayoutByELK(nodes, edges) const hiNode = layoutCallArgs!.children!.find((c: ElkChild) => c.id === 'hi-1')! expect(hiNode.ports).toHaveLength(2) }) @@ -428,7 +430,7 @@ describe('getLayoutByDagre', () => { Reflect.deleteProperty(e1, 'sourceHandle') Reflect.deleteProperty(e2, 'sourceHandle') - await getLayoutByDagre(nodes, [e1, e2]) + await getLayoutByELK(nodes, [e1, e2]) const ifNode = layoutCallArgs!.children!.find((c: ElkChild) => c.id === 'if-1')! expect(ifNode.ports).toHaveLength(2) }) @@ -447,7 +449,7 @@ describe('getLayoutByDagre', () => { Reflect.deleteProperty(e1, 'sourceHandle') Reflect.deleteProperty(e2, 'sourceHandle') - await getLayoutByDagre(nodes, [e1, e2]) + await getLayoutByELK(nodes, [e1, e2]) const hiNode = layoutCallArgs!.children!.find((c: ElkChild) => c.id === 'hi-1')! expect(hiNode.ports).toHaveLength(2) }) @@ -463,7 +465,7 @@ describe('getLayoutByDagre', () => { makeWorkflowEdge({ id: 'e2', source: 'if-1', target: 'c', sourceHandle: 'false' }), ] - await getLayoutByDagre(nodes, edges) + await getLayoutByELK(nodes, edges) const ifNode = layoutCallArgs!.children!.find((c: ElkChild) => c.id === 'if-1')! expect(ifNode.ports).toHaveLength(2) }) @@ -479,7 +481,7 @@ describe('getLayoutByDagre', () => { makeWorkflowEdge({ id: 'e2', source: 'hi-1', target: 'c', sourceHandle: '__timeout' }), ] - await getLayoutByDagre(nodes, edges) + await getLayoutByELK(nodes, edges) const hiNode = layoutCallArgs!.children!.find((c: ElkChild) => c.id === 'hi-1')! expect(hiNode.ports).toHaveLength(2) }) @@ -492,7 +494,7 @@ describe('getLayoutByDagre', () => { makeWorkflowEdge({ source: 'x', target: 'y', data: { isInLoop: true, loop_id: 'loop-1' } }), ] - await getLayoutByDagre(nodes, edges) + await getLayoutByELK(nodes, edges) expect(layoutCallArgs!.edges).toHaveLength(0) }) }) diff --git a/web/app/components/workflow/utils/elk-layout.ts b/web/app/components/workflow/utils/elk-layout.ts index 280d0f7b1d..9860bbc770 100644 --- a/web/app/components/workflow/utils/elk-layout.ts +++ b/web/app/components/workflow/utils/elk-layout.ts @@ -18,9 +18,6 @@ import { BlockEnum, } from '@/app/components/workflow/types' -// Although the file name refers to Dagre, the implementation now relies on ELK's layered algorithm. -// Keep the export signatures unchanged to minimise the blast radius while we migrate the layout stack. - const elk = new ELK() const DEFAULT_NODE_WIDTH = 244 @@ -41,7 +38,6 @@ const ROOT_LAYOUT_OPTIONS = { // === Port Configuration === 'elk.portConstraints': 'FIXED_ORDER', 'elk.layered.considerModelOrder.strategy': 'PREFER_EDGES', - 'elk.port.side': 'SOUTH', // === Node Placement - Best quality === 'elk.layered.nodePlacement.strategy': 'NETWORK_SIMPLEX', @@ -278,32 +274,16 @@ const collectLayout = (graph: ElkNode, predicate: (id: string) => boolean): Layo } } -/** - * Build If/Else node with ELK native Ports instead of dummy nodes - * This is the recommended approach for handling multiple branches - */ -const buildIfElseWithPorts = ( - ifElseNode: Node, - edges: Edge[], -): { node: ElkNodeShape, portMap: Map } | null => { - const childEdges = edges.filter(edge => edge.source === ifElseNode.id) - - if (childEdges.length <= 1) - return null - - // Sort child edges according to case order - const sortedChildEdges = [...childEdges].sort((edgeA, edgeB) => { +const sortIfElseOutEdges = (ifElseNode: Node, outEdges: Edge[]): Edge[] => { + return [...outEdges].sort((edgeA, edgeB) => { const handleA = edgeA.sourceHandle const handleB = edgeB.sourceHandle if (handleA && handleB) { const cases = (ifElseNode.data as IfElseNodeType).cases || [] - const isAElse = handleA === 'false' - const isBElse = handleB === 'false' - - if (isAElse) + if (handleA === 'false') return 1 - if (isBElse) + if (handleB === 'false') return -1 const indexA = cases.findIndex((c: CaseItem) => c.case_id === handleA) @@ -315,67 +295,20 @@ const buildIfElseWithPorts = ( return 0 }) - - // Create ELK ports for each branch - const ports: ElkPortShape[] = sortedChildEdges.map((edge, index) => ({ - id: `${ifElseNode.id}-port-${edge.sourceHandle || index}`, - layoutOptions: { - 'port.side': 'EAST', // Ports on the right side (matching 'RIGHT' direction) - 'port.index': String(index), - }, - })) - - // Build port mapping: sourceHandle -> portId - const portMap = new Map() - sortedChildEdges.forEach((edge, index) => { - const portId = `${ifElseNode.id}-port-${edge.sourceHandle || index}` - portMap.set(edge.id, portId) - }) - - return { - node: { - id: ifElseNode.id, - width: ifElseNode.width ?? DEFAULT_NODE_WIDTH, - height: ifElseNode.height ?? DEFAULT_NODE_HEIGHT, - ports, - layoutOptions: { - 'elk.portConstraints': 'FIXED_ORDER', - }, - }, - portMap, - } } -/** - * Build Human Input node with ELK native Ports for multiple branches - * Handles user actions as branches with __timeout as the last fixed branch - */ -const buildHumanInputWithPorts = ( - humanInputNode: Node, - edges: Edge[], -): { node: ElkNodeShape, portMap: Map } | null => { - const childEdges = edges.filter(edge => edge.source === humanInputNode.id) - - if (childEdges.length <= 1) - return null - - // Sort child edges: user actions first (by order), then __timeout last - const sortedChildEdges = [...childEdges].sort((edgeA, edgeB) => { +const sortHumanInputOutEdges = (humanInputNode: Node, outEdges: Edge[]): Edge[] => { + return [...outEdges].sort((edgeA, edgeB) => { const handleA = edgeA.sourceHandle const handleB = edgeB.sourceHandle if (handleA && handleB) { const userActions = (humanInputNode.data as HumanInputNodeType).user_actions || [] - const isATimeout = handleA === '__timeout' - const isBTimeout = handleB === '__timeout' - - // __timeout should always be last - if (isATimeout) + if (handleA === '__timeout') return 1 - if (isBTimeout) + if (handleB === '__timeout') return -1 - // Sort by user_actions order const indexA = userActions.findIndex(action => action.id === handleA) const indexB = userActions.findIndex(action => action.id === handleB) @@ -385,35 +318,6 @@ const buildHumanInputWithPorts = ( return 0 }) - - // Create ELK ports for each branch - const ports: ElkPortShape[] = sortedChildEdges.map((edge, index) => ({ - id: `${humanInputNode.id}-port-${edge.sourceHandle || index}`, - layoutOptions: { - 'port.side': 'EAST', - 'port.index': String(index), - }, - })) - - // Build port mapping: edge.id -> portId - const portMap = new Map() - sortedChildEdges.forEach((edge, index) => { - const portId = `${humanInputNode.id}-port-${edge.sourceHandle || index}` - portMap.set(edge.id, portId) - }) - - return { - node: { - id: humanInputNode.id, - width: humanInputNode.width ?? DEFAULT_NODE_WIDTH, - height: humanInputNode.height ?? DEFAULT_NODE_HEIGHT, - ports, - layoutOptions: { - 'elk.portConstraints': 'FIXED_ORDER', - }, - }, - portMap, - } } const normaliseBounds = (layout: LayoutResult): LayoutResult => { @@ -448,58 +352,87 @@ const normaliseBounds = (layout: LayoutResult): LayoutResult => { } } -export const getLayoutByDagre = async (originNodes: Node[], originEdges: Edge[]): Promise => { +export const getLayoutByELK = async (originNodes: Node[], originEdges: Edge[]): Promise => { edgeCounter = 0 const nodes = cloneDeep(originNodes).filter(node => !node.parentId && node.type === CUSTOM_NODE) const edges = cloneDeep(originEdges).filter(edge => (!edge.data?.isInIteration && !edge.data?.isInLoop)) - const elkNodes: ElkNodeShape[] = [] - const elkEdges: ElkEdgeShape[] = [] - - // Track which edges have been processed for If/Else nodes with ports - const edgeToPortMap = new Map() - - // Build nodes with ports for If/Else and Human Input nodes - nodes.forEach((node) => { - if (node.data.type === BlockEnum.IfElse) { - const portsResult = buildIfElseWithPorts(node, edges) - if (portsResult) { - // Use node with ports - elkNodes.push(portsResult.node) - // Store port mappings for edges - portsResult.portMap.forEach((portId, edgeId) => { - edgeToPortMap.set(edgeId, portId) - }) - } - else { - // No multiple branches, use normal node - elkNodes.push(toElkNode(node)) - } - } - else if (node.data.type === BlockEnum.HumanInput) { - const portsResult = buildHumanInputWithPorts(node, edges) - if (portsResult) { - // Use node with ports - elkNodes.push(portsResult.node) - // Store port mappings for edges - portsResult.portMap.forEach((portId, edgeId) => { - edgeToPortMap.set(edgeId, portId) - }) - } - else { - // No multiple branches, use normal node - elkNodes.push(toElkNode(node)) - } - } - else { - elkNodes.push(toElkNode(node)) - } + const outEdgesByNode = new Map() + const inEdgesByNode = new Map() + edges.forEach((edge) => { + if (!outEdgesByNode.has(edge.source)) + outEdgesByNode.set(edge.source, []) + outEdgesByNode.get(edge.source)!.push(edge) + if (!inEdgesByNode.has(edge.target)) + inEdgesByNode.set(edge.target, []) + inEdgesByNode.get(edge.target)!.push(edge) }) - // Build edges with port connections - edges.forEach((edge) => { - const sourcePort = edgeToPortMap.get(edge.id) - elkEdges.push(createEdge(edge.source, edge.target, sourcePort)) + const elkNodes: ElkNodeShape[] = [] + const elkEdges: ElkEdgeShape[] = [] + const sourcePortMap = new Map() + const targetPortMap = new Map() + const sortedOutEdgesByNode = new Map() + + nodes.forEach((node) => { + const inEdges = inEdgesByNode.get(node.id) || [] + let outEdges = outEdgesByNode.get(node.id) || [] + + if (node.data.type === BlockEnum.IfElse) + outEdges = sortIfElseOutEdges(node, outEdges) + else if (node.data.type === BlockEnum.HumanInput) + outEdges = sortHumanInputOutEdges(node, outEdges) + + sortedOutEdgesByNode.set(node.id, outEdges) + + const ports: ElkPortShape[] = [] + + inEdges.forEach((edge, index) => { + const portId = `${node.id}-in-${index}` + ports.push({ + id: portId, + layoutOptions: { + 'elk.port.side': 'WEST', + 'elk.port.index': String(index), + }, + }) + targetPortMap.set(edge.id, portId) + }) + + outEdges.forEach((edge, index) => { + const portId = `${node.id}-out-${edge.sourceHandle || index}` + ports.push({ + id: portId, + layoutOptions: { + 'elk.port.side': 'EAST', + 'elk.port.index': String(index), + }, + }) + sourcePortMap.set(edge.id, portId) + }) + + elkNodes.push({ + id: node.id, + width: node.width ?? DEFAULT_NODE_WIDTH, + height: node.height ?? DEFAULT_NODE_HEIGHT, + ...(ports.length > 0 && { + ports, + layoutOptions: { 'elk.portConstraints': 'FIXED_ORDER' }, + }), + }) + }) + + // Build edges in sorted per-node order so PREFER_EDGES aligns with port order + nodes.forEach((node) => { + const outEdges = sortedOutEdgesByNode.get(node.id) || [] + outEdges.forEach((edge) => { + elkEdges.push(createEdge( + edge.source, + edge.target, + sourcePortMap.get(edge.id), + targetPortMap.get(edge.id), + )) + }) }) const graph = { From 3c7180bfd5e4c17deb19728234db29a2a3e6e6a5 Mon Sep 17 00:00:00 2001 From: YBoy Date: Mon, 30 Mar 2026 17:56:30 +0300 Subject: [PATCH 008/199] test: migrate trigger providers controller tests to testcontainers (#34295) --- .../workspace/test_trigger_providers.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) rename api/tests/{unit_tests => test_containers_integration_tests}/controllers/console/workspace/test_trigger_providers.py (95%) diff --git a/api/tests/unit_tests/controllers/console/workspace/test_trigger_providers.py b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py similarity index 95% rename from api/tests/unit_tests/controllers/console/workspace/test_trigger_providers.py rename to api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py index 4776bc7af0..b4d12bff62 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_trigger_providers.py +++ b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py @@ -1,3 +1,7 @@ +"""Testcontainers integration tests for controllers.console.workspace.trigger_providers endpoints.""" + +from __future__ import annotations + from unittest.mock import MagicMock, patch import pytest @@ -40,6 +44,10 @@ def mock_user(): class TestTriggerProviderApis: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_icon_success(self, app): api = TriggerProviderIconApi() method = unwrap(api.get) @@ -84,6 +92,10 @@ class TestTriggerProviderApis: class TestTriggerSubscriptionListApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_list_success(self, app): api = TriggerSubscriptionListApi() method = unwrap(api.get) @@ -115,6 +127,10 @@ class TestTriggerSubscriptionListApi: class TestTriggerSubscriptionBuilderApis: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_create_builder(self, app): api = TriggerSubscriptionBuilderCreateApi() method = unwrap(api.post) @@ -219,6 +235,10 @@ class TestTriggerSubscriptionBuilderApis: class TestTriggerSubscriptionCrud: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_update_rename_only(self, app): api = TriggerSubscriptionUpdateApi() method = unwrap(api.post) @@ -321,6 +341,10 @@ class TestTriggerSubscriptionCrud: class TestTriggerOAuthApis: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_oauth_authorize_success(self, app): api = TriggerOAuthAuthorizeApi() method = unwrap(api.get) @@ -455,6 +479,10 @@ class TestTriggerOAuthApis: class TestTriggerOAuthClientManageApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_get_client(self, app): api = TriggerOAuthClientManageApi() method = unwrap(api.get) @@ -527,6 +555,10 @@ class TestTriggerOAuthClientManageApi: class TestTriggerSubscriptionVerifyApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_verify_success(self, app): api = TriggerSubscriptionVerifyApi() method = unwrap(api.post) From a1513f06c31dc09c2a2c6d63162c6ae88fbc0796 Mon Sep 17 00:00:00 2001 From: doskoi <50610194+t-daisuke@users.noreply.github.com> Date: Mon, 30 Mar 2026 23:56:58 +0900 Subject: [PATCH 009/199] =?UTF-8?q?fix(i18n):=20translate=20"nodes.note.ad?= =?UTF-8?q?dNote"=20as=20"=E3=83=A1=E3=83=A2=E3=82=92=E8=BF=BD=E5=8A=A0"?= =?UTF-8?q?=20in=20ja-JP=20(#34294)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/i18n/ja-JP/workflow.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/i18n/ja-JP/workflow.json b/web/i18n/ja-JP/workflow.json index b9b60d6e73..acf43b8ce8 100644 --- a/web/i18n/ja-JP/workflow.json +++ b/web/i18n/ja-JP/workflow.json @@ -818,7 +818,7 @@ "nodes.loop.setLoopVariables": "ループスコープ内で変数を設定", "nodes.loop.totalLoopCount": "総ループ回数:{{count}}", "nodes.loop.variableName": "変数名", - "nodes.note.addNote": "コメントを追加", + "nodes.note.addNote": "メモを追加", "nodes.note.editor.bold": "太字", "nodes.note.editor.bulletList": "リスト", "nodes.note.editor.enterUrl": "リンク入力中...", From dede190be2721370404b9e81f2f5519f5c8ce126 Mon Sep 17 00:00:00 2001 From: YBoy Date: Mon, 30 Mar 2026 17:57:28 +0300 Subject: [PATCH 010/199] test: migrate data source controller tests to testcontainers (#34292) --- .../console/datasets/test_data_source.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) rename api/tests/{unit_tests => test_containers_integration_tests}/controllers/console/datasets/test_data_source.py (95%) diff --git a/api/tests/unit_tests/controllers/console/datasets/test_data_source.py b/api/tests/test_containers_integration_tests/controllers/console/datasets/test_data_source.py similarity index 95% rename from api/tests/unit_tests/controllers/console/datasets/test_data_source.py rename to api/tests/test_containers_integration_tests/controllers/console/datasets/test_data_source.py index d841f67f9b..1c07d4ca1c 100644 --- a/api/tests/unit_tests/controllers/console/datasets/test_data_source.py +++ b/api/tests/test_containers_integration_tests/controllers/console/datasets/test_data_source.py @@ -1,3 +1,7 @@ +"""Testcontainers integration tests for controllers.console.datasets.data_source endpoints.""" + +from __future__ import annotations + from unittest.mock import MagicMock, PropertyMock, patch import pytest @@ -46,6 +50,10 @@ def mock_engine(): class TestDataSourceApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_get_success(self, app, patch_tenant): api = DataSourceApi() method = unwrap(api.get) @@ -179,6 +187,10 @@ class TestDataSourceApi: class TestDataSourceNotionListApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_get_credential_not_found(self, app, patch_tenant): api = DataSourceNotionListApi() method = unwrap(api.get) @@ -310,6 +322,10 @@ class TestDataSourceNotionListApi: class TestDataSourceNotionApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_get_preview_success(self, app, patch_tenant): api = DataSourceNotionApi() method = unwrap(api.get) @@ -364,6 +380,10 @@ class TestDataSourceNotionApi: class TestDataSourceNotionDatasetSyncApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_get_success(self, app, patch_tenant): api = DataSourceNotionDatasetSyncApi() method = unwrap(api.get) @@ -403,6 +423,10 @@ class TestDataSourceNotionDatasetSyncApi: class TestDataSourceNotionDocumentSyncApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_get_success(self, app, patch_tenant): api = DataSourceNotionDocumentSyncApi() method = unwrap(api.get) From 623c8ae803c155283adfb4b65a43e710ccd36f9f Mon Sep 17 00:00:00 2001 From: YBoy Date: Mon, 30 Mar 2026 17:58:04 +0300 Subject: [PATCH 011/199] test: migrate app apis controller tests to testcontainers (#34291) --- .../controllers/console/app/test_app_apis.py | 87 ++++++------------- 1 file changed, 25 insertions(+), 62 deletions(-) rename api/tests/{unit_tests => test_containers_integration_tests}/controllers/console/app/test_app_apis.py (90%) diff --git a/api/tests/unit_tests/controllers/console/app/test_app_apis.py b/api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py similarity index 90% rename from api/tests/unit_tests/controllers/console/app/test_app_apis.py rename to api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py index 1d1e119fd6..fbaec069bb 100644 --- a/api/tests/unit_tests/controllers/console/app/test_app_apis.py +++ b/api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py @@ -1,7 +1,4 @@ -""" -Additional tests to improve coverage for low-coverage modules in controllers/console/app. -Target: increase coverage for files with <75% coverage. -""" +"""Testcontainers integration tests for controllers/console/app endpoints.""" from __future__ import annotations @@ -70,26 +67,12 @@ def _unwrap(func): return func -class _ConnContext: - def __init__(self, rows): - self._rows = rows - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc, tb): - return False - - def execute(self, _query, _args): - return self._rows - - -# ========== Completion Tests ========== class TestCompletionEndpoints: - """Tests for completion API endpoints.""" + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers def test_completion_create_payload(self): - """Test completion creation payload.""" payload = CompletionMessagePayload(inputs={"prompt": "test"}, model_config={}) assert payload.inputs == {"prompt": "test"} @@ -209,7 +192,9 @@ class TestCompletionEndpoints: class TestAppEndpoints: - """Tests for app endpoints.""" + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers def test_app_put_should_preserve_icon_type_when_payload_omits_it(self, app, monkeypatch): api = app_module.AppApi() @@ -250,12 +235,12 @@ class TestAppEndpoints: ) -# ========== OpsTrace Tests ========== class TestOpsTraceEndpoints: - """Tests for ops_trace endpoint.""" + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers def test_ops_trace_query_basic(self): - """Test ops_trace query.""" query = TraceProviderQuery(tracing_provider="langfuse") assert query.tracing_provider == "langfuse" @@ -310,12 +295,12 @@ class TestOpsTraceEndpoints: method(app_id="app-1") -# ========== Site Tests ========== class TestSiteEndpoints: - """Tests for site endpoint.""" + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers def test_site_response_structure(self): - """Test site response structure.""" payload = AppSiteUpdatePayload(title="My Site", description="Test site") assert payload.title == "My Site" @@ -369,27 +354,22 @@ class TestSiteEndpoints: assert result is site -# ========== Workflow Tests ========== class TestWorkflowEndpoints: - """Tests for workflow endpoints.""" - def test_workflow_copy_payload(self): - """Test workflow copy payload.""" payload = SyncDraftWorkflowPayload(graph={}, features={}) assert payload.graph == {} def test_workflow_mode_query(self): - """Test workflow mode query.""" payload = AdvancedChatWorkflowRunPayload(inputs={}, query="hi") assert payload.query == "hi" -# ========== Workflow App Log Tests ========== class TestWorkflowAppLogEndpoints: - """Tests for workflow app log endpoints.""" + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers def test_workflow_app_log_query(self): - """Test workflow app log query.""" query = WorkflowAppLogQuery(keyword="test", page=1, limit=20) assert query.keyword == "test" @@ -427,12 +407,12 @@ class TestWorkflowAppLogEndpoints: assert result == {"items": [], "total": 0} -# ========== Workflow Draft Variable Tests ========== class TestWorkflowDraftVariableEndpoints: - """Tests for workflow draft variable endpoints.""" + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers def test_workflow_variable_creation(self): - """Test workflow variable creation.""" payload = WorkflowDraftVariableUpdatePayload(name="var1", value="test") assert payload.name == "var1" @@ -472,12 +452,12 @@ class TestWorkflowDraftVariableEndpoints: assert result == {"items": [], "total": 0} -# ========== Workflow Statistic Tests ========== class TestWorkflowStatisticEndpoints: - """Tests for workflow statistic endpoints.""" + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers def test_workflow_statistic_time_range(self): - """Test workflow statistic time range query.""" query = WorkflowStatisticQuery(start="2024-01-01", end="2024-12-31") assert query.start == "2024-01-01" @@ -541,12 +521,12 @@ class TestWorkflowStatisticEndpoints: assert response.get_json() == {"data": [{"date": "2024-01-02"}]} -# ========== Workflow Trigger Tests ========== class TestWorkflowTriggerEndpoints: - """Tests for workflow trigger endpoints.""" + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers def test_webhook_trigger_payload(self): - """Test webhook trigger payload.""" payload = Parser(node_id="node-1") assert payload.node_id == "node-1" @@ -578,22 +558,13 @@ class TestWorkflowTriggerEndpoints: assert result is trigger -# ========== Wraps Tests ========== class TestWrapsEndpoints: - """Tests for wraps utility functions.""" - def test_get_app_model_context(self): - """Test get_app_model wrapper context.""" - # These are decorator functions, so we test their availability assert hasattr(wraps_module, "get_app_model") -# ========== MCP Server Tests ========== class TestMCPServerEndpoints: - """Tests for MCP server endpoints.""" - def test_mcp_server_connection(self): - """Test MCP server connection.""" payload = MCPServerCreatePayload(parameters={"url": "http://localhost:3000"}) assert payload.parameters["url"] == "http://localhost:3000" @@ -602,22 +573,14 @@ class TestMCPServerEndpoints: assert payload.status == "active" -# ========== Error Handling Tests ========== class TestErrorHandling: - """Tests for error handling in various endpoints.""" - def test_annotation_list_query_validation(self): - """Test annotation list query validation.""" with pytest.raises(ValueError): annotation_module.AnnotationListQuery(page=0) -# ========== Integration-like Tests ========== class TestPayloadIntegration: - """Integration tests for payload handling.""" - def test_multiple_payload_types(self): - """Test handling of multiple payload types.""" payloads = [ annotation_module.AnnotationReplyPayload( score_threshold=0.5, embedding_provider_name="openai", embedding_model_name="text-embedding-3-small" From cc89b57c1f53361778af356be77334ccdbb38eb6 Mon Sep 17 00:00:00 2001 From: YBoy Date: Mon, 30 Mar 2026 18:01:50 +0300 Subject: [PATCH 012/199] test: migrate web forgot password controller tests to testcontainers (#34288) --- .../web/test_web_forgot_password.py | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) rename api/tests/{unit_tests => test_containers_integration_tests}/controllers/web/test_web_forgot_password.py (95%) diff --git a/api/tests/unit_tests/controllers/web/test_web_forgot_password.py b/api/tests/test_containers_integration_tests/controllers/web/test_web_forgot_password.py similarity index 95% rename from api/tests/unit_tests/controllers/web/test_web_forgot_password.py rename to api/tests/test_containers_integration_tests/controllers/web/test_web_forgot_password.py index 3d7c319947..19057726c3 100644 --- a/api/tests/unit_tests/controllers/web/test_web_forgot_password.py +++ b/api/tests/test_containers_integration_tests/controllers/web/test_web_forgot_password.py @@ -1,9 +1,12 @@ +"""Testcontainers integration tests for controllers.web.forgot_password endpoints.""" + +from __future__ import annotations + import base64 from types import SimpleNamespace from unittest.mock import MagicMock, patch import pytest -from flask import Flask from controllers.web.forgot_password import ( ForgotPasswordCheckApi, @@ -12,13 +15,6 @@ from controllers.web.forgot_password import ( ) -@pytest.fixture -def app(): - flask_app = Flask(__name__) - flask_app.config["TESTING"] = True - return flask_app - - @pytest.fixture(autouse=True) def _patch_wraps(): wraps_features = SimpleNamespace(enable_email_password_login=True) @@ -33,6 +29,10 @@ def _patch_wraps(): class TestForgotPasswordSendEmailApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + @patch("controllers.web.forgot_password.AccountService.send_reset_password_email") @patch("controllers.web.forgot_password.AccountService.get_account_by_email_with_case_fallback") @patch("controllers.web.forgot_password.AccountService.is_email_send_ip_limit", return_value=False) @@ -69,6 +69,10 @@ class TestForgotPasswordSendEmailApi: class TestForgotPasswordCheckApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + @patch("controllers.web.forgot_password.AccountService.reset_forgot_password_error_rate_limit") @patch("controllers.web.forgot_password.AccountService.generate_reset_password_token") @patch("controllers.web.forgot_password.AccountService.revoke_reset_password_token") @@ -143,6 +147,10 @@ class TestForgotPasswordCheckApi: class TestForgotPasswordResetApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + @patch("controllers.web.forgot_password.ForgotPasswordResetApi._update_existing_account") @patch("controllers.web.forgot_password.AccountService.get_account_by_email_with_case_fallback") @patch("controllers.web.forgot_password.Session") From bc14ad6a8f6476f205dabd5f382051b7bd410152 Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Mon, 30 Mar 2026 23:05:57 +0800 Subject: [PATCH 013/199] fix: map checkbox and json_object types in MCP schema publishing (#34226) Signed-off-by: majiayu000 <1835304752@qq.com> Co-authored-by: Asuka Minato --- api/controllers/mcp/mcp.py | 1 + api/core/mcp/server/streamable_http.py | 8 ++ .../core/mcp/server/test_streamable_http.py | 80 +++++++++++++++++-- 3 files changed, 82 insertions(+), 7 deletions(-) diff --git a/api/controllers/mcp/mcp.py b/api/controllers/mcp/mcp.py index 3d00f77e79..58ec76243b 100644 --- a/api/controllers/mcp/mcp.py +++ b/api/controllers/mcp/mcp.py @@ -174,6 +174,7 @@ class MCPAppApi(Resource): required=variable.get("required", False), max_length=variable.get("max_length"), options=variable.get("options") or [], + json_schema=variable.get("json_schema"), ) def _parse_mcp_request(self, args: dict) -> mcp_types.ClientRequest | mcp_types.ClientNotification: diff --git a/api/core/mcp/server/streamable_http.py b/api/core/mcp/server/streamable_http.py index 27000c947c..278add8cc9 100644 --- a/api/core/mcp/server/streamable_http.py +++ b/api/core/mcp/server/streamable_http.py @@ -260,4 +260,12 @@ def convert_input_form_to_parameters( parameters[item.variable]["enum"] = item.options elif item.type == VariableEntityType.NUMBER: parameters[item.variable]["type"] = "number" + elif item.type == VariableEntityType.CHECKBOX: + parameters[item.variable]["type"] = "boolean" + elif item.type == VariableEntityType.JSON_OBJECT: + parameters[item.variable]["type"] = "object" + if item.json_schema: + for key in ("properties", "required", "additionalProperties"): + if key in item.json_schema: + parameters[item.variable][key] = item.json_schema[key] return parameters, required diff --git a/api/tests/unit_tests/core/mcp/server/test_streamable_http.py b/api/tests/unit_tests/core/mcp/server/test_streamable_http.py index 313d18c695..9a815fb94d 100644 --- a/api/tests/unit_tests/core/mcp/server/test_streamable_http.py +++ b/api/tests/unit_tests/core/mcp/server/test_streamable_http.py @@ -415,12 +415,44 @@ class TestUtilityFunctions: label="Upload", required=False, ), + VariableEntity( + type=VariableEntityType.CHECKBOX, + variable="enabled", + description="Enable flag", + label="Enabled", + required=False, + ), + VariableEntity( + type=VariableEntityType.JSON_OBJECT, + variable="config", + description="Config object", + label="Config", + required=True, + ), + VariableEntity( + type=VariableEntityType.JSON_OBJECT, + variable="schema_config", + description="Config with schema", + label="Schema Config", + required=False, + json_schema={ + "properties": { + "host": {"type": "string"}, + "port": {"type": "number"}, + }, + "required": ["host"], + "additionalProperties": False, + }, + ), ] parameters_dict: dict[str, str] = { "name": "Enter your name", "category": "Select category", "count": "Enter count", + "enabled": "Enable flag", + "config": "Config object", + "schema_config": "Config with schema", } parameters, required = convert_input_form_to_parameters(user_input_form, parameters_dict) @@ -437,20 +469,35 @@ class TestUtilityFunctions: assert "count" in parameters assert parameters["count"]["type"] == "number" - # FILE type should be skipped - it creates empty dict but gets filtered later - # Check that it doesn't have any meaningful content - if "upload" in parameters: - assert parameters["upload"] == {} + # FILE type is skipped entirely via `continue` — key should not exist + assert "upload" not in parameters + + # CHECKBOX maps to boolean + assert parameters["enabled"]["type"] == "boolean" + + # JSON_OBJECT without json_schema maps to object + assert parameters["config"]["type"] == "object" + assert "properties" not in parameters["config"] + + # JSON_OBJECT with json_schema forwards schema keys + assert parameters["schema_config"]["type"] == "object" + assert parameters["schema_config"]["properties"] == { + "host": {"type": "string"}, + "port": {"type": "number"}, + } + assert parameters["schema_config"]["required"] == ["host"] + assert parameters["schema_config"]["additionalProperties"] is False # Check required fields assert "name" in required assert "count" in required + assert "config" in required assert "category" not in required # Note: _get_request_id function has been removed as request_id is now passed as parameter def test_convert_input_form_to_parameters_jsonschema_validation_ok(self): - """Current schema uses 'number' for numeric fields; it should be a valid JSON Schema.""" + """Generated schema with all supported types should be valid JSON Schema.""" user_input_form = [ VariableEntity( type=VariableEntityType.NUMBER, @@ -466,11 +513,27 @@ class TestUtilityFunctions: label="Name", required=False, ), + VariableEntity( + type=VariableEntityType.CHECKBOX, + variable="enabled", + description="Toggle", + label="Enabled", + required=False, + ), + VariableEntity( + type=VariableEntityType.JSON_OBJECT, + variable="metadata", + description="Metadata", + label="Metadata", + required=False, + ), ] parameters_dict = { "count": "Enter count", "name": "Enter your name", + "enabled": "Toggle flag", + "metadata": "Metadata object", } parameters, required = convert_input_form_to_parameters(user_input_form, parameters_dict) @@ -485,9 +548,12 @@ class TestUtilityFunctions: # 1) The schema itself must be valid jsonschema.Draft202012Validator.check_schema(schema) - # 2) Both float and integer instances should pass validation + # 2) Validate instances with all types jsonschema.validate(instance={"count": 3.14, "name": "alice"}, schema=schema) - jsonschema.validate(instance={"count": 2, "name": "bob"}, schema=schema) + jsonschema.validate( + instance={"count": 2, "enabled": True, "metadata": {"key": "val"}}, + schema=schema, + ) def test_legacy_float_type_schema_is_invalid(self): """Legacy/buggy behavior: using 'float' should produce an invalid JSON Schema.""" From 953bcc33b1f4691833932614399123e06d363310 Mon Sep 17 00:00:00 2001 From: YBoy Date: Mon, 30 Mar 2026 19:18:21 +0300 Subject: [PATCH 014/199] test: migrate workspace wraps controller tests to testcontainers (#34296) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../console/workspace/test_workspace_wraps.py | 185 ++++++++++++++++++ .../console/workspace/test_workspace_wraps.py | 142 -------------- 2 files changed, 185 insertions(+), 142 deletions(-) create mode 100644 api/tests/test_containers_integration_tests/controllers/console/workspace/test_workspace_wraps.py delete mode 100644 api/tests/unit_tests/controllers/console/workspace/test_workspace_wraps.py diff --git a/api/tests/test_containers_integration_tests/controllers/console/workspace/test_workspace_wraps.py b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_workspace_wraps.py new file mode 100644 index 0000000000..99cabb6cea --- /dev/null +++ b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_workspace_wraps.py @@ -0,0 +1,185 @@ +"""Testcontainers integration tests for plugin_permission_required decorator.""" + +from __future__ import annotations + +from types import SimpleNamespace +from unittest.mock import patch + +import pytest +from sqlalchemy.orm import Session +from werkzeug.exceptions import Forbidden + +from controllers.console.workspace import plugin_permission_required +from models.account import Tenant, TenantPluginPermission, TenantStatus + + +def _create_tenant(db_session: Session) -> Tenant: + tenant = Tenant(name="test-tenant", status=TenantStatus.NORMAL, plan="basic") + db_session.add(tenant) + db_session.commit() + db_session.expire_all() + return tenant + + +def _create_permission( + db_session: Session, + tenant_id: str, + install: TenantPluginPermission.InstallPermission = TenantPluginPermission.InstallPermission.EVERYONE, + debug: TenantPluginPermission.DebugPermission = TenantPluginPermission.DebugPermission.EVERYONE, +) -> TenantPluginPermission: + perm = TenantPluginPermission( + tenant_id=tenant_id, + install_permission=install, + debug_permission=debug, + ) + db_session.add(perm) + db_session.commit() + db_session.expire_all() + return perm + + +class TestPluginPermissionRequired: + def test_allows_without_permission(self, db_session_with_containers: Session): + tenant = _create_tenant(db_session_with_containers) + user = SimpleNamespace(is_admin_or_owner=False) + + with patch( + "controllers.console.workspace.current_account_with_tenant", + return_value=(user, tenant.id), + ): + + @plugin_permission_required() + def handler(): + return "ok" + + assert handler() == "ok" + + def test_install_nobody_forbidden(self, db_session_with_containers: Session): + tenant = _create_tenant(db_session_with_containers) + _create_permission( + db_session_with_containers, + tenant.id, + install=TenantPluginPermission.InstallPermission.NOBODY, + debug=TenantPluginPermission.DebugPermission.EVERYONE, + ) + user = SimpleNamespace(is_admin_or_owner=True) + + with patch( + "controllers.console.workspace.current_account_with_tenant", + return_value=(user, tenant.id), + ): + + @plugin_permission_required(install_required=True) + def handler(): + return "ok" + + with pytest.raises(Forbidden): + handler() + + def test_install_admin_requires_admin(self, db_session_with_containers: Session): + tenant = _create_tenant(db_session_with_containers) + _create_permission( + db_session_with_containers, + tenant.id, + install=TenantPluginPermission.InstallPermission.ADMINS, + debug=TenantPluginPermission.DebugPermission.EVERYONE, + ) + user = SimpleNamespace(is_admin_or_owner=False) + + with patch( + "controllers.console.workspace.current_account_with_tenant", + return_value=(user, tenant.id), + ): + + @plugin_permission_required(install_required=True) + def handler(): + return "ok" + + with pytest.raises(Forbidden): + handler() + + def test_install_admin_allows_admin(self, db_session_with_containers: Session): + tenant = _create_tenant(db_session_with_containers) + _create_permission( + db_session_with_containers, + tenant.id, + install=TenantPluginPermission.InstallPermission.ADMINS, + debug=TenantPluginPermission.DebugPermission.EVERYONE, + ) + user = SimpleNamespace(is_admin_or_owner=True) + + with patch( + "controllers.console.workspace.current_account_with_tenant", + return_value=(user, tenant.id), + ): + + @plugin_permission_required(install_required=True) + def handler(): + return "ok" + + assert handler() == "ok" + + def test_debug_nobody_forbidden(self, db_session_with_containers: Session): + tenant = _create_tenant(db_session_with_containers) + _create_permission( + db_session_with_containers, + tenant.id, + install=TenantPluginPermission.InstallPermission.EVERYONE, + debug=TenantPluginPermission.DebugPermission.NOBODY, + ) + user = SimpleNamespace(is_admin_or_owner=True) + + with patch( + "controllers.console.workspace.current_account_with_tenant", + return_value=(user, tenant.id), + ): + + @plugin_permission_required(debug_required=True) + def handler(): + return "ok" + + with pytest.raises(Forbidden): + handler() + + def test_debug_admin_requires_admin(self, db_session_with_containers: Session): + tenant = _create_tenant(db_session_with_containers) + _create_permission( + db_session_with_containers, + tenant.id, + install=TenantPluginPermission.InstallPermission.EVERYONE, + debug=TenantPluginPermission.DebugPermission.ADMINS, + ) + user = SimpleNamespace(is_admin_or_owner=False) + + with patch( + "controllers.console.workspace.current_account_with_tenant", + return_value=(user, tenant.id), + ): + + @plugin_permission_required(debug_required=True) + def handler(): + return "ok" + + with pytest.raises(Forbidden): + handler() + + def test_debug_admin_allows_admin(self, db_session_with_containers: Session): + tenant = _create_tenant(db_session_with_containers) + _create_permission( + db_session_with_containers, + tenant.id, + install=TenantPluginPermission.InstallPermission.EVERYONE, + debug=TenantPluginPermission.DebugPermission.ADMINS, + ) + user = SimpleNamespace(is_admin_or_owner=True) + + with patch( + "controllers.console.workspace.current_account_with_tenant", + return_value=(user, tenant.id), + ): + + @plugin_permission_required(debug_required=True) + def handler(): + return "ok" + + assert handler() == "ok" diff --git a/api/tests/unit_tests/controllers/console/workspace/test_workspace_wraps.py b/api/tests/unit_tests/controllers/console/workspace/test_workspace_wraps.py deleted file mode 100644 index b290748155..0000000000 --- a/api/tests/unit_tests/controllers/console/workspace/test_workspace_wraps.py +++ /dev/null @@ -1,142 +0,0 @@ -from __future__ import annotations - -import importlib -from types import SimpleNamespace - -import pytest -from werkzeug.exceptions import Forbidden - -from controllers.console.workspace import plugin_permission_required -from models.account import TenantPluginPermission - - -class _SessionStub: - def __init__(self, permission): - self._permission = permission - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc, tb): - return False - - def query(self, *_args, **_kwargs): - return self - - def where(self, *_args, **_kwargs): - return self - - def first(self): - return self._permission - - -def _workspace_module(): - return importlib.import_module(plugin_permission_required.__module__) - - -def _patch_session(monkeypatch: pytest.MonkeyPatch, permission): - module = _workspace_module() - monkeypatch.setattr(module, "Session", lambda *_args, **_kwargs: _SessionStub(permission)) - monkeypatch.setattr(module, "db", SimpleNamespace(engine=object())) - - -def test_plugin_permission_allows_without_permission(monkeypatch: pytest.MonkeyPatch) -> None: - user = SimpleNamespace(is_admin_or_owner=False) - module = _workspace_module() - monkeypatch.setattr(module, "current_account_with_tenant", lambda: (user, "t1")) - _patch_session(monkeypatch, None) - - @plugin_permission_required() - def handler(): - return "ok" - - assert handler() == "ok" - - -def test_plugin_permission_install_nobody_forbidden(monkeypatch: pytest.MonkeyPatch) -> None: - user = SimpleNamespace(is_admin_or_owner=True) - permission = SimpleNamespace( - install_permission=TenantPluginPermission.InstallPermission.NOBODY, - debug_permission=TenantPluginPermission.DebugPermission.EVERYONE, - ) - module = _workspace_module() - monkeypatch.setattr(module, "current_account_with_tenant", lambda: (user, "t1")) - _patch_session(monkeypatch, permission) - - @plugin_permission_required(install_required=True) - def handler(): - return "ok" - - with pytest.raises(Forbidden): - handler() - - -def test_plugin_permission_install_admin_requires_admin(monkeypatch: pytest.MonkeyPatch) -> None: - user = SimpleNamespace(is_admin_or_owner=False) - permission = SimpleNamespace( - install_permission=TenantPluginPermission.InstallPermission.ADMINS, - debug_permission=TenantPluginPermission.DebugPermission.EVERYONE, - ) - module = _workspace_module() - monkeypatch.setattr(module, "current_account_with_tenant", lambda: (user, "t1")) - _patch_session(monkeypatch, permission) - - @plugin_permission_required(install_required=True) - def handler(): - return "ok" - - with pytest.raises(Forbidden): - handler() - - -def test_plugin_permission_install_admin_allows_admin(monkeypatch: pytest.MonkeyPatch) -> None: - user = SimpleNamespace(is_admin_or_owner=True) - permission = SimpleNamespace( - install_permission=TenantPluginPermission.InstallPermission.ADMINS, - debug_permission=TenantPluginPermission.DebugPermission.EVERYONE, - ) - module = _workspace_module() - monkeypatch.setattr(module, "current_account_with_tenant", lambda: (user, "t1")) - _patch_session(monkeypatch, permission) - - @plugin_permission_required(install_required=True) - def handler(): - return "ok" - - assert handler() == "ok" - - -def test_plugin_permission_debug_nobody_forbidden(monkeypatch: pytest.MonkeyPatch) -> None: - user = SimpleNamespace(is_admin_or_owner=True) - permission = SimpleNamespace( - install_permission=TenantPluginPermission.InstallPermission.EVERYONE, - debug_permission=TenantPluginPermission.DebugPermission.NOBODY, - ) - module = _workspace_module() - monkeypatch.setattr(module, "current_account_with_tenant", lambda: (user, "t1")) - _patch_session(monkeypatch, permission) - - @plugin_permission_required(debug_required=True) - def handler(): - return "ok" - - with pytest.raises(Forbidden): - handler() - - -def test_plugin_permission_debug_admin_requires_admin(monkeypatch: pytest.MonkeyPatch) -> None: - user = SimpleNamespace(is_admin_or_owner=False) - permission = SimpleNamespace( - install_permission=TenantPluginPermission.InstallPermission.EVERYONE, - debug_permission=TenantPluginPermission.DebugPermission.ADMINS, - ) - module = _workspace_module() - monkeypatch.setattr(module, "current_account_with_tenant", lambda: (user, "t1")) - _patch_session(monkeypatch, permission) - - @plugin_permission_required(debug_required=True) - def handler(): - return "ok" - - with pytest.raises(Forbidden): - handler() From 5fc4dfaf7ba182149079c1fa91a87056687ee33e Mon Sep 17 00:00:00 2001 From: YBoy Date: Mon, 30 Mar 2026 19:19:15 +0300 Subject: [PATCH 015/199] test: migrate web wraps controller tests to testcontainers (#34289) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../controllers/web/test_wraps.py | 215 ++++++++---------- 1 file changed, 95 insertions(+), 120 deletions(-) rename api/tests/{unit_tests => test_containers_integration_tests}/controllers/web/test_wraps.py (67%) diff --git a/api/tests/unit_tests/controllers/web/test_wraps.py b/api/tests/test_containers_integration_tests/controllers/web/test_wraps.py similarity index 67% rename from api/tests/unit_tests/controllers/web/test_wraps.py rename to api/tests/test_containers_integration_tests/controllers/web/test_wraps.py index 85049ae975..19833cc772 100644 --- a/api/tests/unit_tests/controllers/web/test_wraps.py +++ b/api/tests/test_containers_integration_tests/controllers/web/test_wraps.py @@ -1,13 +1,14 @@ -"""Unit tests for controllers.web.wraps — JWT auth decorator and validation helpers.""" +"""Testcontainers integration tests for controllers.web.wraps — JWT auth decorator and validation helpers.""" from __future__ import annotations from datetime import UTC, datetime, timedelta from types import SimpleNamespace from unittest.mock import MagicMock, patch +from uuid import uuid4 import pytest -from flask import Flask +from sqlalchemy.orm import Session from werkzeug.exceptions import BadRequest, NotFound, Unauthorized from controllers.web.error import WebAppAuthAccessDeniedError, WebAppAuthRequiredError @@ -18,12 +19,8 @@ from controllers.web.wraps import ( ) -# --------------------------------------------------------------------------- -# _validate_webapp_token -# --------------------------------------------------------------------------- class TestValidateWebappToken: def test_enterprise_enabled_and_app_auth_requires_webapp_source(self) -> None: - """When both flags are true, a non-webapp source must raise.""" decoded = {"token_source": "other"} with pytest.raises(WebAppAuthRequiredError): _validate_webapp_token(decoded, app_web_auth_enabled=True, system_webapp_auth_enabled=True) @@ -38,7 +35,6 @@ class TestValidateWebappToken: _validate_webapp_token(decoded, app_web_auth_enabled=True, system_webapp_auth_enabled=True) def test_public_app_rejects_webapp_source(self) -> None: - """When auth is not required, a webapp-sourced token must be rejected.""" decoded = {"token_source": "webapp"} with pytest.raises(Unauthorized): _validate_webapp_token(decoded, app_web_auth_enabled=False, system_webapp_auth_enabled=False) @@ -52,18 +48,13 @@ class TestValidateWebappToken: _validate_webapp_token(decoded, app_web_auth_enabled=False, system_webapp_auth_enabled=False) def test_system_enabled_but_app_public(self) -> None: - """system_webapp_auth_enabled=True but app is public — webapp source rejected.""" decoded = {"token_source": "webapp"} with pytest.raises(Unauthorized): _validate_webapp_token(decoded, app_web_auth_enabled=False, system_webapp_auth_enabled=True) -# --------------------------------------------------------------------------- -# _validate_user_accessibility -# --------------------------------------------------------------------------- class TestValidateUserAccessibility: def test_skips_when_auth_disabled(self) -> None: - """No checks when system or app auth is disabled.""" _validate_user_accessibility( decoded={}, app_code="code", @@ -123,7 +114,6 @@ class TestValidateUserAccessibility: def test_external_auth_type_checks_sso_update_time( self, mock_perm_check: MagicMock, mock_sso_time: MagicMock ) -> None: - # granted_at is before SSO update time → denied mock_sso_time.return_value = datetime.now(UTC) old_granted = int((datetime.now(UTC) - timedelta(hours=1)).timestamp()) decoded = {"user_id": "u1", "auth_type": "external", "granted_at": old_granted} @@ -164,7 +154,6 @@ class TestValidateUserAccessibility: recent_granted = int(datetime.now(UTC).timestamp()) decoded = {"user_id": "u1", "auth_type": "external", "granted_at": recent_granted} settings = SimpleNamespace(access_mode="public") - # Should not raise _validate_user_accessibility( decoded=decoded, app_code="code", @@ -191,10 +180,49 @@ class TestValidateUserAccessibility: ) -# --------------------------------------------------------------------------- -# decode_jwt_token -# --------------------------------------------------------------------------- class TestDecodeJwtToken: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + + def _create_app_site_enduser(self, db_session: Session, *, enable_site: bool = True): + from models.model import App, AppMode, CustomizeTokenStrategy, EndUser, Site + + tenant_id = str(uuid4()) + app_model = App( + tenant_id=tenant_id, + mode=AppMode.CHAT.value, + name="test-app", + enable_site=enable_site, + enable_api=True, + ) + db_session.add(app_model) + db_session.commit() + db_session.expire_all() + + site = Site( + app_id=app_model.id, + title="test-site", + default_language="en-US", + customize_token_strategy=CustomizeTokenStrategy.NOT_ALLOW, + code="code1", + ) + db_session.add(site) + db_session.commit() + db_session.expire_all() + + end_user = EndUser( + tenant_id=tenant_id, + app_id=app_model.id, + type="browser", + session_id="sess-1", + ) + db_session.add(end_user) + db_session.commit() + db_session.expire_all() + + return app_model, site, end_user + @patch("controllers.web.wraps._validate_user_accessibility") @patch("controllers.web.wraps._validate_webapp_token") @patch("controllers.web.wraps.EnterpriseService.WebAppAuth.get_app_access_mode_by_id") @@ -202,10 +230,8 @@ class TestDecodeJwtToken: @patch("controllers.web.wraps.FeatureService.get_system_features") @patch("controllers.web.wraps.PassportService") @patch("controllers.web.wraps.extract_webapp_passport") - @patch("controllers.web.wraps.db") def test_happy_path( self, - mock_db: MagicMock, mock_extract: MagicMock, mock_passport_cls: MagicMock, mock_features: MagicMock, @@ -213,40 +239,28 @@ class TestDecodeJwtToken: mock_access_mode: MagicMock, mock_validate_token: MagicMock, mock_validate_user: MagicMock, - app: Flask, + app, + db_session_with_containers: Session, ) -> None: + app_model, site, end_user = self._create_app_site_enduser(db_session_with_containers) + mock_extract.return_value = "jwt-token" mock_passport_cls.return_value.verify.return_value = { - "app_code": "code1", - "app_id": "app-1", - "end_user_id": "eu-1", + "app_code": site.code, + "app_id": app_model.id, + "end_user_id": end_user.id, } mock_features.return_value = SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)) - app_model = SimpleNamespace(id="app-1", enable_site=True) - site = SimpleNamespace(code="code1") - end_user = SimpleNamespace(id="eu-1", session_id="sess-1") + with app.test_request_context("/", headers={"X-App-Code": site.code}): + result_app, result_user = decode_jwt_token() - # Configure session mock to return correct objects via scalar() - session_mock = MagicMock() - session_mock.scalar.side_effect = [app_model, site, end_user] - session_ctx = MagicMock() - session_ctx.__enter__ = MagicMock(return_value=session_mock) - session_ctx.__exit__ = MagicMock(return_value=False) - mock_db.engine = "engine" - - with patch("controllers.web.wraps.Session", return_value=session_ctx): - with app.test_request_context("/", headers={"X-App-Code": "code1"}): - result_app, result_user = decode_jwt_token() - - assert result_app.id == "app-1" - assert result_user.id == "eu-1" + assert result_app.id == app_model.id + assert result_user.id == end_user.id @patch("controllers.web.wraps.FeatureService.get_system_features") @patch("controllers.web.wraps.extract_webapp_passport") - def test_missing_token_raises_unauthorized( - self, mock_extract: MagicMock, mock_features: MagicMock, app: Flask - ) -> None: + def test_missing_token_raises_unauthorized(self, mock_extract: MagicMock, mock_features: MagicMock, app) -> None: mock_features.return_value = SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)) mock_extract.return_value = None @@ -257,137 +271,98 @@ class TestDecodeJwtToken: @patch("controllers.web.wraps.FeatureService.get_system_features") @patch("controllers.web.wraps.PassportService") @patch("controllers.web.wraps.extract_webapp_passport") - @patch("controllers.web.wraps.db") def test_missing_app_raises_not_found( self, - mock_db: MagicMock, mock_extract: MagicMock, mock_passport_cls: MagicMock, mock_features: MagicMock, - app: Flask, + app, ) -> None: + non_existent_id = str(uuid4()) mock_extract.return_value = "jwt-token" mock_passport_cls.return_value.verify.return_value = { "app_code": "code1", - "app_id": "app-1", - "end_user_id": "eu-1", + "app_id": non_existent_id, + "end_user_id": str(uuid4()), } mock_features.return_value = SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)) - session_mock = MagicMock() - session_mock.scalar.return_value = None # No app found - session_ctx = MagicMock() - session_ctx.__enter__ = MagicMock(return_value=session_mock) - session_ctx.__exit__ = MagicMock(return_value=False) - mock_db.engine = "engine" - - with patch("controllers.web.wraps.Session", return_value=session_ctx): - with app.test_request_context("/", headers={"X-App-Code": "code1"}): - with pytest.raises(NotFound): - decode_jwt_token() + with app.test_request_context("/", headers={"X-App-Code": "code1"}): + with pytest.raises(NotFound): + decode_jwt_token() @patch("controllers.web.wraps.FeatureService.get_system_features") @patch("controllers.web.wraps.PassportService") @patch("controllers.web.wraps.extract_webapp_passport") - @patch("controllers.web.wraps.db") def test_disabled_site_raises_bad_request( self, - mock_db: MagicMock, mock_extract: MagicMock, mock_passport_cls: MagicMock, mock_features: MagicMock, - app: Flask, + app, + db_session_with_containers: Session, ) -> None: + app_model, site, end_user = self._create_app_site_enduser(db_session_with_containers, enable_site=False) + mock_extract.return_value = "jwt-token" mock_passport_cls.return_value.verify.return_value = { - "app_code": "code1", - "app_id": "app-1", - "end_user_id": "eu-1", + "app_code": site.code, + "app_id": app_model.id, + "end_user_id": end_user.id, } mock_features.return_value = SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)) - app_model = SimpleNamespace(id="app-1", enable_site=False) - - session_mock = MagicMock() - # scalar calls: app_model, site (code found), then end_user - session_mock.scalar.side_effect = [app_model, SimpleNamespace(code="code1"), None] - session_ctx = MagicMock() - session_ctx.__enter__ = MagicMock(return_value=session_mock) - session_ctx.__exit__ = MagicMock(return_value=False) - mock_db.engine = "engine" - - with patch("controllers.web.wraps.Session", return_value=session_ctx): - with app.test_request_context("/", headers={"X-App-Code": "code1"}): - with pytest.raises(BadRequest, match="Site is disabled"): - decode_jwt_token() + with app.test_request_context("/", headers={"X-App-Code": site.code}): + with pytest.raises(BadRequest, match="Site is disabled"): + decode_jwt_token() @patch("controllers.web.wraps.FeatureService.get_system_features") @patch("controllers.web.wraps.PassportService") @patch("controllers.web.wraps.extract_webapp_passport") - @patch("controllers.web.wraps.db") def test_missing_end_user_raises_not_found( self, - mock_db: MagicMock, mock_extract: MagicMock, mock_passport_cls: MagicMock, mock_features: MagicMock, - app: Flask, + app, + db_session_with_containers: Session, ) -> None: + app_model, site, _ = self._create_app_site_enduser(db_session_with_containers) + non_existent_eu = str(uuid4()) + mock_extract.return_value = "jwt-token" mock_passport_cls.return_value.verify.return_value = { - "app_code": "code1", - "app_id": "app-1", - "end_user_id": "eu-1", + "app_code": site.code, + "app_id": app_model.id, + "end_user_id": non_existent_eu, } mock_features.return_value = SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)) - app_model = SimpleNamespace(id="app-1", enable_site=True) - site = SimpleNamespace(code="code1") - - session_mock = MagicMock() - session_mock.scalar.side_effect = [app_model, site, None] # end_user is None - session_ctx = MagicMock() - session_ctx.__enter__ = MagicMock(return_value=session_mock) - session_ctx.__exit__ = MagicMock(return_value=False) - mock_db.engine = "engine" - - with patch("controllers.web.wraps.Session", return_value=session_ctx): - with app.test_request_context("/", headers={"X-App-Code": "code1"}): - with pytest.raises(NotFound): - decode_jwt_token() + with app.test_request_context("/", headers={"X-App-Code": site.code}): + with pytest.raises(NotFound): + decode_jwt_token() @patch("controllers.web.wraps.FeatureService.get_system_features") @patch("controllers.web.wraps.PassportService") @patch("controllers.web.wraps.extract_webapp_passport") - @patch("controllers.web.wraps.db") def test_user_id_mismatch_raises_unauthorized( self, - mock_db: MagicMock, mock_extract: MagicMock, mock_passport_cls: MagicMock, mock_features: MagicMock, - app: Flask, + app, + db_session_with_containers: Session, ) -> None: + app_model, site, end_user = self._create_app_site_enduser(db_session_with_containers) + mock_extract.return_value = "jwt-token" mock_passport_cls.return_value.verify.return_value = { - "app_code": "code1", - "app_id": "app-1", - "end_user_id": "eu-1", + "app_code": site.code, + "app_id": app_model.id, + "end_user_id": end_user.id, } mock_features.return_value = SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)) - app_model = SimpleNamespace(id="app-1", enable_site=True) - site = SimpleNamespace(code="code1") - end_user = SimpleNamespace(id="eu-1", session_id="sess-1") - - session_mock = MagicMock() - session_mock.scalar.side_effect = [app_model, site, end_user] - session_ctx = MagicMock() - session_ctx.__enter__ = MagicMock(return_value=session_mock) - session_ctx.__exit__ = MagicMock(return_value=False) - mock_db.engine = "engine" - - with patch("controllers.web.wraps.Session", return_value=session_ctx): - with app.test_request_context("/", headers={"X-App-Code": "code1"}): - with pytest.raises(Unauthorized, match="expired"): - decode_jwt_token(user_id="different-user") + with app.test_request_context("/", headers={"X-App-Code": site.code}): + with pytest.raises(Unauthorized, match="expired"): + decode_jwt_token(user_id="different-user") From 3a7885819d7831380a862ef695f87ca999d18828 Mon Sep 17 00:00:00 2001 From: YBoy Date: Tue, 31 Mar 2026 03:25:46 +0300 Subject: [PATCH 016/199] test: migrate web conversation controller tests to testcontainers (#34287) --- .../controllers/web/test_conversation.py | 73 +++++++++---------- 1 file changed, 35 insertions(+), 38 deletions(-) rename api/tests/{unit_tests => test_containers_integration_tests}/controllers/web/test_conversation.py (72%) diff --git a/api/tests/unit_tests/controllers/web/test_conversation.py b/api/tests/test_containers_integration_tests/controllers/web/test_conversation.py similarity index 72% rename from api/tests/unit_tests/controllers/web/test_conversation.py rename to api/tests/test_containers_integration_tests/controllers/web/test_conversation.py index e5adbbbf66..e1e6741014 100644 --- a/api/tests/unit_tests/controllers/web/test_conversation.py +++ b/api/tests/test_containers_integration_tests/controllers/web/test_conversation.py @@ -1,4 +1,4 @@ -"""Unit tests for controllers.web.conversation endpoints.""" +"""Testcontainers integration tests for controllers.web.conversation endpoints.""" from __future__ import annotations @@ -7,7 +7,6 @@ from unittest.mock import MagicMock, patch from uuid import uuid4 import pytest -from flask import Flask from werkzeug.exceptions import NotFound from controllers.web.conversation import ( @@ -33,18 +32,18 @@ def _end_user() -> SimpleNamespace: return SimpleNamespace(id="eu-1") -# --------------------------------------------------------------------------- -# ConversationListApi -# --------------------------------------------------------------------------- class TestConversationListApi: - def test_non_chat_mode_raises(self, app: Flask) -> None: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + + def test_non_chat_mode_raises(self, app) -> None: with app.test_request_context("/conversations"): with pytest.raises(NotChatAppError): ConversationListApi().get(_completion_app(), _end_user()) @patch("controllers.web.conversation.WebConversationService.pagination_by_last_id") - @patch("controllers.web.conversation.db") - def test_happy_path(self, mock_db: MagicMock, mock_paginate: MagicMock, app: Flask) -> None: + def test_happy_path(self, mock_paginate: MagicMock, app) -> None: conv_id = str(uuid4()) conv = SimpleNamespace( id=conv_id, @@ -56,34 +55,26 @@ class TestConversationListApi: updated_at=1700000000, ) mock_paginate.return_value = SimpleNamespace(limit=20, has_more=False, data=[conv]) - mock_db.engine = "engine" - session_mock = MagicMock() - session_ctx = MagicMock() - session_ctx.__enter__ = MagicMock(return_value=session_mock) - session_ctx.__exit__ = MagicMock(return_value=False) - - with ( - app.test_request_context("/conversations?limit=20"), - patch("controllers.web.conversation.Session", return_value=session_ctx), - ): + with app.test_request_context("/conversations?limit=20"): result = ConversationListApi().get(_chat_app(), _end_user()) assert result["limit"] == 20 assert result["has_more"] is False -# --------------------------------------------------------------------------- -# ConversationApi (delete) -# --------------------------------------------------------------------------- class TestConversationApi: - def test_non_chat_mode_raises(self, app: Flask) -> None: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + + def test_non_chat_mode_raises(self, app) -> None: with app.test_request_context(f"/conversations/{uuid4()}"): with pytest.raises(NotChatAppError): ConversationApi().delete(_completion_app(), _end_user(), uuid4()) @patch("controllers.web.conversation.ConversationService.delete") - def test_delete_success(self, mock_delete: MagicMock, app: Flask) -> None: + def test_delete_success(self, mock_delete: MagicMock, app) -> None: c_id = uuid4() with app.test_request_context(f"/conversations/{c_id}"): result, status = ConversationApi().delete(_chat_app(), _end_user(), c_id) @@ -92,25 +83,26 @@ class TestConversationApi: assert result["result"] == "success" @patch("controllers.web.conversation.ConversationService.delete", side_effect=ConversationNotExistsError()) - def test_delete_not_found(self, mock_delete: MagicMock, app: Flask) -> None: + def test_delete_not_found(self, mock_delete: MagicMock, app) -> None: c_id = uuid4() with app.test_request_context(f"/conversations/{c_id}"): with pytest.raises(NotFound, match="Conversation Not Exists"): ConversationApi().delete(_chat_app(), _end_user(), c_id) -# --------------------------------------------------------------------------- -# ConversationRenameApi -# --------------------------------------------------------------------------- class TestConversationRenameApi: - def test_non_chat_mode_raises(self, app: Flask) -> None: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + + def test_non_chat_mode_raises(self, app) -> None: with app.test_request_context(f"/conversations/{uuid4()}/name", method="POST", json={"name": "x"}): with pytest.raises(NotChatAppError): ConversationRenameApi().post(_completion_app(), _end_user(), uuid4()) @patch("controllers.web.conversation.ConversationService.rename") @patch("controllers.web.conversation.web_ns") - def test_rename_success(self, mock_ns: MagicMock, mock_rename: MagicMock, app: Flask) -> None: + def test_rename_success(self, mock_ns: MagicMock, mock_rename: MagicMock, app) -> None: c_id = uuid4() mock_ns.payload = {"name": "New Name", "auto_generate": False} conv = SimpleNamespace( @@ -134,7 +126,7 @@ class TestConversationRenameApi: side_effect=ConversationNotExistsError(), ) @patch("controllers.web.conversation.web_ns") - def test_rename_not_found(self, mock_ns: MagicMock, mock_rename: MagicMock, app: Flask) -> None: + def test_rename_not_found(self, mock_ns: MagicMock, mock_rename: MagicMock, app) -> None: c_id = uuid4() mock_ns.payload = {"name": "X", "auto_generate": False} @@ -143,17 +135,18 @@ class TestConversationRenameApi: ConversationRenameApi().post(_chat_app(), _end_user(), c_id) -# --------------------------------------------------------------------------- -# ConversationPinApi / ConversationUnPinApi -# --------------------------------------------------------------------------- class TestConversationPinApi: - def test_non_chat_mode_raises(self, app: Flask) -> None: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + + def test_non_chat_mode_raises(self, app) -> None: with app.test_request_context(f"/conversations/{uuid4()}/pin", method="PATCH"): with pytest.raises(NotChatAppError): ConversationPinApi().patch(_completion_app(), _end_user(), uuid4()) @patch("controllers.web.conversation.WebConversationService.pin") - def test_pin_success(self, mock_pin: MagicMock, app: Flask) -> None: + def test_pin_success(self, mock_pin: MagicMock, app) -> None: c_id = uuid4() with app.test_request_context(f"/conversations/{c_id}/pin", method="PATCH"): result = ConversationPinApi().patch(_chat_app(), _end_user(), c_id) @@ -161,7 +154,7 @@ class TestConversationPinApi: assert result["result"] == "success" @patch("controllers.web.conversation.WebConversationService.pin", side_effect=ConversationNotExistsError()) - def test_pin_not_found(self, mock_pin: MagicMock, app: Flask) -> None: + def test_pin_not_found(self, mock_pin: MagicMock, app) -> None: c_id = uuid4() with app.test_request_context(f"/conversations/{c_id}/pin", method="PATCH"): with pytest.raises(NotFound): @@ -169,13 +162,17 @@ class TestConversationPinApi: class TestConversationUnPinApi: - def test_non_chat_mode_raises(self, app: Flask) -> None: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + + def test_non_chat_mode_raises(self, app) -> None: with app.test_request_context(f"/conversations/{uuid4()}/unpin", method="PATCH"): with pytest.raises(NotChatAppError): ConversationUnPinApi().patch(_completion_app(), _end_user(), uuid4()) @patch("controllers.web.conversation.WebConversationService.unpin") - def test_unpin_success(self, mock_unpin: MagicMock, app: Flask) -> None: + def test_unpin_success(self, mock_unpin: MagicMock, app) -> None: c_id = uuid4() with app.test_request_context(f"/conversations/{c_id}/unpin", method="PATCH"): result = ConversationUnPinApi().patch(_chat_app(), _end_user(), c_id) From c58170f5b83f98a3d766d73ddb946a8d38365e3e Mon Sep 17 00:00:00 2001 From: YBoy Date: Tue, 31 Mar 2026 03:26:50 +0300 Subject: [PATCH 017/199] test: migrate app import api controller tests to testcontainers (#34290) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../console/app/test_app_import_api.py | 142 ++++++++++++++++ .../console/app/test_app_import_api.py | 157 ------------------ 2 files changed, 142 insertions(+), 157 deletions(-) create mode 100644 api/tests/test_containers_integration_tests/controllers/console/app/test_app_import_api.py delete mode 100644 api/tests/unit_tests/controllers/console/app/test_app_import_api.py diff --git a/api/tests/test_containers_integration_tests/controllers/console/app/test_app_import_api.py b/api/tests/test_containers_integration_tests/controllers/console/app/test_app_import_api.py new file mode 100644 index 0000000000..d8c6821f8d --- /dev/null +++ b/api/tests/test_containers_integration_tests/controllers/console/app/test_app_import_api.py @@ -0,0 +1,142 @@ +"""Testcontainers integration tests for controllers.console.app.app_import endpoints.""" + +from __future__ import annotations + +from types import SimpleNamespace +from unittest.mock import MagicMock + +import pytest + +from controllers.console.app import app_import as app_import_module +from services.app_dsl_service import ImportStatus + + +def _unwrap(func): + bound_self = getattr(func, "__self__", None) + while hasattr(func, "__wrapped__"): + func = func.__wrapped__ + if bound_self is not None: + return func.__get__(bound_self, bound_self.__class__) + return func + + +class _Result: + def __init__(self, status: ImportStatus, app_id: str | None = "app-1"): + self.status = status + self.app_id = app_id + + def model_dump(self, mode: str = "json"): + return {"status": self.status, "app_id": self.app_id} + + +def _install_features(monkeypatch: pytest.MonkeyPatch, enabled: bool) -> None: + features = SimpleNamespace(webapp_auth=SimpleNamespace(enabled=enabled)) + monkeypatch.setattr(app_import_module.FeatureService, "get_system_features", lambda: features) + + +class TestAppImportApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + + def test_import_post_returns_failed_status(self, app, monkeypatch: pytest.MonkeyPatch) -> None: + api = app_import_module.AppImportApi() + method = _unwrap(api.post) + + _install_features(monkeypatch, enabled=False) + monkeypatch.setattr( + app_import_module.AppDslService, + "import_app", + lambda *_args, **_kwargs: _Result(ImportStatus.FAILED, app_id=None), + ) + monkeypatch.setattr(app_import_module, "current_account_with_tenant", lambda: (SimpleNamespace(id="u1"), "t1")) + + with app.test_request_context("/console/api/apps/imports", method="POST", json={"mode": "yaml-content"}): + response, status = method() + + assert status == 400 + assert response["status"] == ImportStatus.FAILED + + def test_import_post_returns_pending_status(self, app, monkeypatch: pytest.MonkeyPatch) -> None: + api = app_import_module.AppImportApi() + method = _unwrap(api.post) + + _install_features(monkeypatch, enabled=False) + monkeypatch.setattr( + app_import_module.AppDslService, + "import_app", + lambda *_args, **_kwargs: _Result(ImportStatus.PENDING), + ) + monkeypatch.setattr(app_import_module, "current_account_with_tenant", lambda: (SimpleNamespace(id="u1"), "t1")) + + with app.test_request_context("/console/api/apps/imports", method="POST", json={"mode": "yaml-content"}): + response, status = method() + + assert status == 202 + assert response["status"] == ImportStatus.PENDING + + def test_import_post_updates_webapp_auth_when_enabled(self, app, monkeypatch: pytest.MonkeyPatch) -> None: + api = app_import_module.AppImportApi() + method = _unwrap(api.post) + + _install_features(monkeypatch, enabled=True) + monkeypatch.setattr( + app_import_module.AppDslService, + "import_app", + lambda *_args, **_kwargs: _Result(ImportStatus.COMPLETED, app_id="app-123"), + ) + update_access = MagicMock() + monkeypatch.setattr(app_import_module.EnterpriseService.WebAppAuth, "update_app_access_mode", update_access) + monkeypatch.setattr(app_import_module, "current_account_with_tenant", lambda: (SimpleNamespace(id="u1"), "t1")) + + with app.test_request_context("/console/api/apps/imports", method="POST", json={"mode": "yaml-content"}): + response, status = method() + + update_access.assert_called_once_with("app-123", "private") + assert status == 200 + assert response["status"] == ImportStatus.COMPLETED + + +class TestAppImportConfirmApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + + def test_import_confirm_returns_failed_status(self, app, monkeypatch: pytest.MonkeyPatch) -> None: + api = app_import_module.AppImportConfirmApi() + method = _unwrap(api.post) + + monkeypatch.setattr( + app_import_module.AppDslService, + "confirm_import", + lambda *_args, **_kwargs: _Result(ImportStatus.FAILED), + ) + monkeypatch.setattr(app_import_module, "current_account_with_tenant", lambda: (SimpleNamespace(id="u1"), "t1")) + + with app.test_request_context("/console/api/apps/imports/import-1/confirm", method="POST"): + response, status = method(import_id="import-1") + + assert status == 400 + assert response["status"] == ImportStatus.FAILED + + +class TestAppImportCheckDependenciesApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + + def test_import_check_dependencies_returns_result(self, app, monkeypatch: pytest.MonkeyPatch) -> None: + api = app_import_module.AppImportCheckDependenciesApi() + method = _unwrap(api.get) + + monkeypatch.setattr( + app_import_module.AppDslService, + "check_dependencies", + lambda *_args, **_kwargs: SimpleNamespace(model_dump=lambda mode="json": {"leaked_dependencies": []}), + ) + + with app.test_request_context("/console/api/apps/imports/app-1/check-dependencies", method="GET"): + response, status = method(app_model=SimpleNamespace(id="app-1")) + + assert status == 200 + assert response["leaked_dependencies"] == [] diff --git a/api/tests/unit_tests/controllers/console/app/test_app_import_api.py b/api/tests/unit_tests/controllers/console/app/test_app_import_api.py deleted file mode 100644 index 91f58460ac..0000000000 --- a/api/tests/unit_tests/controllers/console/app/test_app_import_api.py +++ /dev/null @@ -1,157 +0,0 @@ -from __future__ import annotations - -from types import SimpleNamespace -from unittest.mock import MagicMock - -import pytest - -from controllers.console.app import app_import as app_import_module -from services.app_dsl_service import ImportStatus - - -def _unwrap(func): - bound_self = getattr(func, "__self__", None) - while hasattr(func, "__wrapped__"): - func = func.__wrapped__ - if bound_self is not None: - return func.__get__(bound_self, bound_self.__class__) - return func - - -class _Result: - def __init__(self, status: ImportStatus, app_id: str | None = "app-1"): - self.status = status - self.app_id = app_id - - def model_dump(self, mode: str = "json"): - return {"status": self.status, "app_id": self.app_id} - - -class _SessionContext: - def __init__(self, session): - self._session = session - - def __enter__(self): - return self._session - - def __exit__(self, exc_type, exc, tb): - return False - - -def _install_session(monkeypatch: pytest.MonkeyPatch, session: MagicMock) -> None: - monkeypatch.setattr(app_import_module, "Session", lambda *_: _SessionContext(session)) - monkeypatch.setattr(app_import_module, "db", SimpleNamespace(engine=object())) - - -def _install_features(monkeypatch: pytest.MonkeyPatch, enabled: bool) -> None: - features = SimpleNamespace(webapp_auth=SimpleNamespace(enabled=enabled)) - monkeypatch.setattr(app_import_module.FeatureService, "get_system_features", lambda: features) - - -def test_import_post_returns_failed_status(app, monkeypatch: pytest.MonkeyPatch) -> None: - api = app_import_module.AppImportApi() - method = _unwrap(api.post) - - session = MagicMock() - _install_session(monkeypatch, session) - _install_features(monkeypatch, enabled=False) - monkeypatch.setattr( - app_import_module.AppDslService, - "import_app", - lambda *_args, **_kwargs: _Result(ImportStatus.FAILED, app_id=None), - ) - monkeypatch.setattr(app_import_module, "current_account_with_tenant", lambda: (SimpleNamespace(id="u1"), "t1")) - - with app.test_request_context("/console/api/apps/imports", method="POST", json={"mode": "yaml-content"}): - response, status = method() - - session.commit.assert_called_once() - assert status == 400 - assert response["status"] == ImportStatus.FAILED - - -def test_import_post_returns_pending_status(app, monkeypatch: pytest.MonkeyPatch) -> None: - api = app_import_module.AppImportApi() - method = _unwrap(api.post) - - session = MagicMock() - _install_session(monkeypatch, session) - _install_features(monkeypatch, enabled=False) - monkeypatch.setattr( - app_import_module.AppDslService, - "import_app", - lambda *_args, **_kwargs: _Result(ImportStatus.PENDING), - ) - monkeypatch.setattr(app_import_module, "current_account_with_tenant", lambda: (SimpleNamespace(id="u1"), "t1")) - - with app.test_request_context("/console/api/apps/imports", method="POST", json={"mode": "yaml-content"}): - response, status = method() - - session.commit.assert_called_once() - assert status == 202 - assert response["status"] == ImportStatus.PENDING - - -def test_import_post_updates_webapp_auth_when_enabled(app, monkeypatch: pytest.MonkeyPatch) -> None: - api = app_import_module.AppImportApi() - method = _unwrap(api.post) - - session = MagicMock() - _install_session(monkeypatch, session) - _install_features(monkeypatch, enabled=True) - monkeypatch.setattr( - app_import_module.AppDslService, - "import_app", - lambda *_args, **_kwargs: _Result(ImportStatus.COMPLETED, app_id="app-123"), - ) - update_access = MagicMock() - monkeypatch.setattr(app_import_module.EnterpriseService.WebAppAuth, "update_app_access_mode", update_access) - monkeypatch.setattr(app_import_module, "current_account_with_tenant", lambda: (SimpleNamespace(id="u1"), "t1")) - - with app.test_request_context("/console/api/apps/imports", method="POST", json={"mode": "yaml-content"}): - response, status = method() - - session.commit.assert_called_once() - update_access.assert_called_once_with("app-123", "private") - assert status == 200 - assert response["status"] == ImportStatus.COMPLETED - - -def test_import_confirm_returns_failed_status(app, monkeypatch: pytest.MonkeyPatch) -> None: - api = app_import_module.AppImportConfirmApi() - method = _unwrap(api.post) - - session = MagicMock() - _install_session(monkeypatch, session) - monkeypatch.setattr( - app_import_module.AppDslService, - "confirm_import", - lambda *_args, **_kwargs: _Result(ImportStatus.FAILED), - ) - monkeypatch.setattr(app_import_module, "current_account_with_tenant", lambda: (SimpleNamespace(id="u1"), "t1")) - - with app.test_request_context("/console/api/apps/imports/import-1/confirm", method="POST"): - response, status = method(import_id="import-1") - - session.commit.assert_called_once() - assert status == 400 - assert response["status"] == ImportStatus.FAILED - - -def test_import_check_dependencies_returns_result(app, monkeypatch: pytest.MonkeyPatch) -> None: - api = app_import_module.AppImportCheckDependenciesApi() - method = _unwrap(api.get) - - session = MagicMock() - _install_session(monkeypatch, session) - monkeypatch.setattr( - app_import_module.AppDslService, - "check_dependencies", - lambda *_args, **_kwargs: SimpleNamespace(model_dump=lambda mode="json": {"leaked_dependencies": []}), - ) - - with app.test_request_context("/console/api/apps/imports/app-1/check-dependencies", method="GET"): - response, status = method(app_model=SimpleNamespace(id="app-1")) - - assert status == 200 - assert response["leaked_dependencies"] == [] From daebe26089a2b4dd4fd6dde86021a606246d4e9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 09:27:12 +0900 Subject: [PATCH 018/199] chore(deps): bump pygments from 2.19.2 to 2.20.0 in /api (#34301) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- api/uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/uv.lock b/api/uv.lock index 3e8d794866..39c362eda0 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -5255,11 +5255,11 @@ wheels = [ [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] From 097095a69bd85c6c4c98ed07467ec78634a1fe26 Mon Sep 17 00:00:00 2001 From: YBoy Date: Tue, 31 Mar 2026 03:28:04 +0300 Subject: [PATCH 019/199] test: migrate tool provider controller tests to testcontainers (#34293) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../console/workspace/test_tool_provider.py | 56 +++++++++++++------ 1 file changed, 39 insertions(+), 17 deletions(-) rename api/tests/{unit_tests => test_containers_integration_tests}/controllers/console/workspace/test_tool_provider.py (95%) diff --git a/api/tests/unit_tests/controllers/console/workspace/test_tool_provider.py b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_tool_provider.py similarity index 95% rename from api/tests/unit_tests/controllers/console/workspace/test_tool_provider.py rename to api/tests/test_containers_integration_tests/controllers/console/workspace/test_tool_provider.py index 16ea1bf509..e36bd213d9 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_tool_provider.py +++ b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_tool_provider.py @@ -1,9 +1,11 @@ +"""Testcontainers integration tests for controllers.console.workspace.tool_providers endpoints.""" + +from __future__ import annotations + import json from unittest.mock import MagicMock, patch import pytest -from flask import Flask -from flask_restx import Api from werkzeug.exceptions import Forbidden from controllers.console.workspace.tool_providers import ( @@ -31,7 +33,6 @@ from controllers.console.workspace.tool_providers import ( ToolOAuthCustomClient, ToolPluginOAuthApi, ToolProviderListApi, - ToolProviderMCPApi, ToolWorkflowListApi, ToolWorkflowProviderCreateApi, ToolWorkflowProviderDeleteApi, @@ -39,8 +40,6 @@ from controllers.console.workspace.tool_providers import ( ToolWorkflowProviderUpdateApi, is_valid_url, ) -from core.db.session_factory import configure_session_factory -from extensions.ext_database import db from services.tools.mcp_tools_manage_service import ReconnectResult @@ -61,17 +60,8 @@ def _mock_user_tenant(): @pytest.fixture -def client(): - app = Flask(__name__) - app.config["TESTING"] = True - app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" - api = Api(app) - api.add_resource(ToolProviderMCPApi, "/console/api/workspaces/current/tool-provider/mcp") - db.init_app(app) - # Configure session factory used by controller code - with app.app_context(): - configure_session_factory(db.engine) - return app.test_client() +def client(flask_app_with_containers): + return flask_app_with_containers.test_client() @patch( @@ -152,10 +142,14 @@ class TestUtils: assert not is_valid_url("") assert not is_valid_url("ftp://example.com") assert not is_valid_url("not-a-url") - assert not is_valid_url(None) + assert not is_valid_url(None) # type: ignore[arg-type] class TestToolProviderListApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_get_success(self, app): api = ToolProviderListApi() method = unwrap(api.get) @@ -175,6 +169,10 @@ class TestToolProviderListApi: class TestBuiltinProviderApis: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_list_tools(self, app): api = ToolBuiltinProviderListToolsApi() method = unwrap(api.get) @@ -379,6 +377,10 @@ class TestBuiltinProviderApis: class TestApiProviderApis: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_add(self, app): api = ToolApiProviderAddApi() method = unwrap(api.post) @@ -502,6 +504,10 @@ class TestApiProviderApis: class TestWorkflowApis: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_create(self, app): api = ToolWorkflowProviderCreateApi() method = unwrap(api.post) @@ -587,6 +593,10 @@ class TestWorkflowApis: class TestLists: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_builtin_list(self, app): api = ToolBuiltinListApi() method = unwrap(api.get) @@ -649,6 +659,10 @@ class TestLists: class TestLabels: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_labels(self, app): api = ToolLabelsApi() method = unwrap(api.get) @@ -664,6 +678,10 @@ class TestLabels: class TestOAuth: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_oauth_no_client(self, app): api = ToolPluginOAuthApi() method = unwrap(api.get) @@ -692,6 +710,10 @@ class TestOAuth: class TestOAuthCustomClient: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_save_custom_client(self, app): api = ToolOAuthCustomClient() method = unwrap(api.post) From 15aa8071f89a005dc3c68f4e3d9edd39fb5f66cb Mon Sep 17 00:00:00 2001 From: YBoy Date: Tue, 31 Mar 2026 03:28:44 +0300 Subject: [PATCH 020/199] test: migrate mcp controller tests to testcontainers (#34297) --- .../controllers/mcp/test_mcp.py | 35 ++++++++----------- 1 file changed, 14 insertions(+), 21 deletions(-) rename api/tests/{unit_tests => test_containers_integration_tests}/controllers/mcp/test_mcp.py (96%) diff --git a/api/tests/unit_tests/controllers/mcp/test_mcp.py b/api/tests/test_containers_integration_tests/controllers/mcp/test_mcp.py similarity index 96% rename from api/tests/unit_tests/controllers/mcp/test_mcp.py rename to api/tests/test_containers_integration_tests/controllers/mcp/test_mcp.py index b93770e9c2..90670a9db5 100644 --- a/api/tests/unit_tests/controllers/mcp/test_mcp.py +++ b/api/tests/test_containers_integration_tests/controllers/mcp/test_mcp.py @@ -1,5 +1,10 @@ +"""Testcontainers integration tests for controllers.mcp.mcp endpoints.""" + +from __future__ import annotations + import types from unittest.mock import MagicMock, patch +from uuid import uuid4 import pytest from flask import Response @@ -14,24 +19,6 @@ def unwrap(func): return func -@pytest.fixture(autouse=True) -def mock_db(): - module.db = types.SimpleNamespace(engine=object()) - - -@pytest.fixture -def fake_session(): - session = MagicMock() - session.__enter__.return_value = session - session.__exit__.return_value = False - return session - - -@pytest.fixture(autouse=True) -def mock_session(fake_session): - module.Session = MagicMock(return_value=fake_session) - - @pytest.fixture(autouse=True) def mock_mcp_ns(): fake_ns = types.SimpleNamespace() @@ -44,8 +31,13 @@ def fake_payload(data): module.mcp_ns.payload = data +_TENANT_ID = str(uuid4()) +_APP_ID = str(uuid4()) +_SERVER_ID = str(uuid4()) + + class DummyServer: - def __init__(self, status, app_id="app-1", tenant_id="tenant-1", server_id="srv-1"): + def __init__(self, status, app_id=_APP_ID, tenant_id=_TENANT_ID, server_id=_SERVER_ID): self.status = status self.app_id = app_id self.tenant_id = tenant_id @@ -54,8 +46,8 @@ class DummyServer: class DummyApp: def __init__(self, mode, workflow=None, app_model_config=None): - self.id = "app-1" - self.tenant_id = "tenant-1" + self.id = _APP_ID + self.tenant_id = _TENANT_ID self.mode = mode self.workflow = workflow self.app_model_config = app_model_config @@ -76,6 +68,7 @@ class DummyResult: return {"jsonrpc": "2.0", "result": "ok", "id": 1} +@pytest.mark.usefixtures("flask_req_ctx_with_containers") class TestMCPAppApi: @patch.object(module, "handle_mcp_request", return_value=DummyResult(), autospec=True) def test_success_request(self, mock_handle): From 5897b28355f11e15cc590024d8c7a60b453deae7 Mon Sep 17 00:00:00 2001 From: tmimmanuel <14046872+tmimmanuel@users.noreply.github.com> Date: Tue, 31 Mar 2026 02:29:57 +0200 Subject: [PATCH 021/199] refactor: use EnumText for Provider.quota_type and consolidate ProviderQuotaType (#34299) --- api/core/app/llm/quota.py | 2 +- api/core/provider_manager.py | 12 ++++------ .../update_provider_when_message_created.py | 2 +- api/models/provider.py | 24 ++++--------------- api/models/types.py | 4 ++-- .../unit_tests/models/test_provider_models.py | 2 +- 6 files changed, 14 insertions(+), 32 deletions(-) diff --git a/api/core/app/llm/quota.py b/api/core/app/llm/quota.py index 63d2235358..182f1b767d 100644 --- a/api/core/app/llm/quota.py +++ b/api/core/app/llm/quota.py @@ -81,7 +81,7 @@ def deduct_llm_quota(*, tenant_id: str, model_instance: ModelInstance, usage: LL # TODO: Use provider name with prefix after the data migration. Provider.provider_name == ModelProviderID(model_instance.provider).provider_name, Provider.provider_type == ProviderType.SYSTEM.value, - Provider.quota_type == system_configuration.current_quota_type.value, + Provider.quota_type == system_configuration.current_quota_type, Provider.quota_limit > Provider.quota_used, ) .values( diff --git a/api/core/provider_manager.py b/api/core/provider_manager.py index 30933239f6..b2a8e9c114 100644 --- a/api/core/provider_manager.py +++ b/api/core/provider_manager.py @@ -626,9 +626,8 @@ class ProviderManager: if provider_record.provider_type != ProviderType.SYSTEM: continue - provider_quota_to_provider_record_dict[ProviderQuotaType.value_of(provider_record.quota_type)] = ( - provider_record - ) + if provider_record.quota_type is not None: + provider_quota_to_provider_record_dict[provider_record.quota_type] = provider_record for quota in configuration.quotas: if quota.quota_type in (ProviderQuotaType.TRIAL, ProviderQuotaType.PAID): @@ -641,7 +640,7 @@ class ProviderManager: # TODO: Use provider name with prefix after the data migration. provider_name=ModelProviderID(provider_name).provider_name, provider_type=ProviderType.SYSTEM, - quota_type=quota.quota_type, + quota_type=quota.quota_type, # type: ignore[arg-type] quota_limit=0, # type: ignore quota_used=0, is_valid=True, @@ -921,9 +920,8 @@ class ProviderManager: if provider_record.provider_type != ProviderType.SYSTEM: continue - quota_type_to_provider_records_dict[ProviderQuotaType.value_of(provider_record.quota_type)] = ( - provider_record - ) + if provider_record.quota_type is not None: + quota_type_to_provider_records_dict[provider_record.quota_type] = provider_record # type: ignore[index] quota_configurations = [] if dify_config.EDITION == "CLOUD": diff --git a/api/events/event_handlers/update_provider_when_message_created.py b/api/events/event_handlers/update_provider_when_message_created.py index 1ddcc8f792..f68cdaadde 100644 --- a/api/events/event_handlers/update_provider_when_message_created.py +++ b/api/events/event_handlers/update_provider_when_message_created.py @@ -157,7 +157,7 @@ def handle(sender: Message, **kwargs): tenant_id=tenant_id, provider_name=ModelProviderID(model_config.provider).provider_name, provider_type=ProviderType.SYSTEM.value, - quota_type=provider_configuration.system_configuration.current_quota_type.value, + quota_type=provider_configuration.system_configuration.current_quota_type, ), values=_ProviderUpdateValues(quota_used=Provider.quota_used + used_quota, last_used=current_time), additional_filters=_ProviderUpdateAdditionalFilters( diff --git a/api/models/provider.py b/api/models/provider.py index afeee20b1e..bdcfb7aa0d 100644 --- a/api/models/provider.py +++ b/api/models/provider.py @@ -13,7 +13,7 @@ from libs.uuid_utils import uuidv7 from .base import TypeBase from .engine import db -from .enums import CredentialSourceType, PaymentStatus +from .enums import CredentialSourceType, PaymentStatus, ProviderQuotaType from .types import EnumText, LongText, StringUUID @@ -29,24 +29,6 @@ class ProviderType(StrEnum): raise ValueError(f"No matching enum found for value '{value}'") -class ProviderQuotaType(StrEnum): - PAID = auto() - """hosted paid quota""" - - FREE = auto() - """third-party free quota""" - - TRIAL = auto() - """hosted trial quota""" - - @staticmethod - def value_of(value: str) -> ProviderQuotaType: - for member in ProviderQuotaType: - if member.value == value: - return member - raise ValueError(f"No matching enum found for value '{value}'") - - class Provider(TypeBase): """ Provider model representing the API providers and their configurations. @@ -77,7 +59,9 @@ class Provider(TypeBase): last_used: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, init=False) credential_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True, default=None) - quota_type: Mapped[str | None] = mapped_column(String(40), nullable=True, server_default=text("''"), default="") + quota_type: Mapped[ProviderQuotaType | None] = mapped_column( + EnumText(ProviderQuotaType, length=40), nullable=True, server_default=text("''"), default=None + ) quota_limit: Mapped[int | None] = mapped_column(sa.BigInteger, nullable=True, default=None) quota_used: Mapped[int | None] = mapped_column(sa.BigInteger, nullable=True, default=0) diff --git a/api/models/types.py b/api/models/types.py index f8369dab9e..98084563be 100644 --- a/api/models/types.py +++ b/api/models/types.py @@ -144,8 +144,8 @@ class EnumText(TypeDecorator[_E | None], Generic[_E]): return dialect.type_descriptor(VARCHAR(self._length)) def process_result_value(self, value: str | None, dialect: Dialect) -> _E | None: - if value is None: - return value + if value is None or value == "": + return None # Type annotation guarantees value is str at this point return self._enum_class(value) diff --git a/api/tests/unit_tests/models/test_provider_models.py b/api/tests/unit_tests/models/test_provider_models.py index f628e54a4d..d7b597e5fb 100644 --- a/api/tests/unit_tests/models/test_provider_models.py +++ b/api/tests/unit_tests/models/test_provider_models.py @@ -202,7 +202,7 @@ class TestProviderModel: # Assert assert provider.provider_type == ProviderType.CUSTOM assert provider.is_valid is False - assert provider.quota_type == "" + assert provider.quota_type is None assert provider.quota_limit is None assert provider.quota_used == 0 assert provider.credential_id is None From 1344c3b280b582761e8a700c5914f219337826b6 Mon Sep 17 00:00:00 2001 From: tmimmanuel <14046872+tmimmanuel@users.noreply.github.com> Date: Tue, 31 Mar 2026 02:31:33 +0200 Subject: [PATCH 022/199] refactor: use EnumText for model_type in provider models (#34300) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/controllers/console/workspace/models.py | 4 +- api/core/entities/provider_configuration.py | 38 +++++++++---------- api/core/provider_manager.py | 12 +++--- api/models/provider.py | 11 +++--- api/services/model_load_balancing_service.py | 16 ++++---- .../test_model_load_balancing_service.py | 6 +-- .../unit_tests/core/test_provider_manager.py | 22 +++++------ .../test_model_load_balancing_service.py | 2 +- 8 files changed, 55 insertions(+), 56 deletions(-) diff --git a/api/controllers/console/workspace/models.py b/api/controllers/console/workspace/models.py index 2ec1a9435a..9182dbb510 100644 --- a/api/controllers/console/workspace/models.py +++ b/api/controllers/console/workspace/models.py @@ -287,12 +287,10 @@ class ModelProviderModelCredentialApi(Resource): provider=provider, ) else: - # Normalize model_type to the origin value stored in DB (e.g., "text-generation" for LLM) - normalized_model_type = args.model_type.to_origin_model_type() available_credentials = model_provider_service.get_provider_model_available_credentials( tenant_id=tenant_id, provider=provider, - model_type=normalized_model_type, + model_type=args.model_type, model=args.model, ) diff --git a/api/core/entities/provider_configuration.py b/api/core/entities/provider_configuration.py index 8b48aa2660..782897aea9 100644 --- a/api/core/entities/provider_configuration.py +++ b/api/core/entities/provider_configuration.py @@ -403,7 +403,7 @@ class ProviderConfiguration(BaseModel): ProviderModelCredential.tenant_id == self.tenant_id, ProviderModelCredential.provider_name.in_(self._get_provider_names()), ProviderModelCredential.model_name == model, - ProviderModelCredential.model_type == model_type.to_origin_model_type(), + ProviderModelCredential.model_type == model_type, ), ) @@ -753,7 +753,7 @@ class ProviderConfiguration(BaseModel): ProviderModel.tenant_id == self.tenant_id, ProviderModel.provider_name.in_(provider_names), ProviderModel.model_name == model, - ProviderModel.model_type == model_type.to_origin_model_type(), + ProviderModel.model_type == model_type, ) return session.execute(stmt).scalar_one_or_none() @@ -778,7 +778,7 @@ class ProviderConfiguration(BaseModel): ProviderModelCredential.tenant_id == self.tenant_id, ProviderModelCredential.provider_name.in_(self._get_provider_names()), ProviderModelCredential.model_name == model, - ProviderModelCredential.model_type == model_type.to_origin_model_type(), + ProviderModelCredential.model_type == model_type, ) credential_record = session.execute(stmt).scalar_one_or_none() @@ -825,7 +825,7 @@ class ProviderConfiguration(BaseModel): ProviderModelCredential.tenant_id == self.tenant_id, ProviderModelCredential.provider_name.in_(self._get_provider_names()), ProviderModelCredential.model_name == model, - ProviderModelCredential.model_type == model_type.to_origin_model_type(), + ProviderModelCredential.model_type == model_type, ProviderModelCredential.credential_name == credential_name, ) if exclude_id: @@ -901,7 +901,7 @@ class ProviderConfiguration(BaseModel): ProviderModelCredential.tenant_id == self.tenant_id, ProviderModelCredential.provider_name.in_(self._get_provider_names()), ProviderModelCredential.model_name == model, - ProviderModelCredential.model_type == model_type.to_origin_model_type(), + ProviderModelCredential.model_type == model_type, ) credential_record = s.execute(stmt).scalar_one_or_none() original_credentials = ( @@ -970,7 +970,7 @@ class ProviderConfiguration(BaseModel): tenant_id=self.tenant_id, provider_name=self.provider.provider, model_name=model, - model_type=model_type.to_origin_model_type(), + model_type=model_type, encrypted_config=json.dumps(credentials), credential_name=credential_name, ) @@ -983,7 +983,7 @@ class ProviderConfiguration(BaseModel): tenant_id=self.tenant_id, provider_name=self.provider.provider, model_name=model, - model_type=model_type.to_origin_model_type(), + model_type=model_type, credential_id=credential.id, is_valid=True, ) @@ -1038,7 +1038,7 @@ class ProviderConfiguration(BaseModel): ProviderModelCredential.tenant_id == self.tenant_id, ProviderModelCredential.provider_name.in_(self._get_provider_names()), ProviderModelCredential.model_name == model, - ProviderModelCredential.model_type == model_type.to_origin_model_type(), + ProviderModelCredential.model_type == model_type, ) credential_record = session.execute(stmt).scalar_one_or_none() if not credential_record: @@ -1083,7 +1083,7 @@ class ProviderConfiguration(BaseModel): ProviderModelCredential.tenant_id == self.tenant_id, ProviderModelCredential.provider_name.in_(self._get_provider_names()), ProviderModelCredential.model_name == model, - ProviderModelCredential.model_type == model_type.to_origin_model_type(), + ProviderModelCredential.model_type == model_type, ) credential_record = session.execute(stmt).scalar_one_or_none() if not credential_record: @@ -1116,7 +1116,7 @@ class ProviderConfiguration(BaseModel): ProviderModelCredential.tenant_id == self.tenant_id, ProviderModelCredential.provider_name.in_(self._get_provider_names()), ProviderModelCredential.model_name == model, - ProviderModelCredential.model_type == model_type.to_origin_model_type(), + ProviderModelCredential.model_type == model_type, ) available_credentials_count = session.execute(count_stmt).scalar() or 0 session.delete(credential_record) @@ -1156,7 +1156,7 @@ class ProviderConfiguration(BaseModel): ProviderModelCredential.tenant_id == self.tenant_id, ProviderModelCredential.provider_name.in_(self._get_provider_names()), ProviderModelCredential.model_name == model, - ProviderModelCredential.model_type == model_type.to_origin_model_type(), + ProviderModelCredential.model_type == model_type, ) credential_record = session.execute(stmt).scalar_one_or_none() if not credential_record: @@ -1171,7 +1171,7 @@ class ProviderConfiguration(BaseModel): tenant_id=self.tenant_id, provider_name=self.provider.provider, model_name=model, - model_type=model_type.to_origin_model_type(), + model_type=model_type, is_valid=True, credential_id=credential_id, ) @@ -1207,7 +1207,7 @@ class ProviderConfiguration(BaseModel): ProviderModelCredential.tenant_id == self.tenant_id, ProviderModelCredential.provider_name.in_(self._get_provider_names()), ProviderModelCredential.model_name == model, - ProviderModelCredential.model_type == model_type.to_origin_model_type(), + ProviderModelCredential.model_type == model_type, ) credential_record = session.execute(stmt).scalar_one_or_none() if not credential_record: @@ -1263,7 +1263,7 @@ class ProviderConfiguration(BaseModel): stmt = select(ProviderModelSetting).where( ProviderModelSetting.tenant_id == self.tenant_id, ProviderModelSetting.provider_name.in_(self._get_provider_names()), - ProviderModelSetting.model_type == model_type.to_origin_model_type(), + ProviderModelSetting.model_type == model_type, ProviderModelSetting.model_name == model, ) return session.execute(stmt).scalars().first() @@ -1286,7 +1286,7 @@ class ProviderConfiguration(BaseModel): model_setting = ProviderModelSetting( tenant_id=self.tenant_id, provider_name=self.provider.provider, - model_type=model_type.to_origin_model_type(), + model_type=model_type, model_name=model, enabled=True, ) @@ -1312,7 +1312,7 @@ class ProviderConfiguration(BaseModel): model_setting = ProviderModelSetting( tenant_id=self.tenant_id, provider_name=self.provider.provider, - model_type=model_type.to_origin_model_type(), + model_type=model_type, model_name=model, enabled=False, ) @@ -1348,7 +1348,7 @@ class ProviderConfiguration(BaseModel): stmt = select(func.count(LoadBalancingModelConfig.id)).where( LoadBalancingModelConfig.tenant_id == self.tenant_id, LoadBalancingModelConfig.provider_name.in_(provider_names), - LoadBalancingModelConfig.model_type == model_type.to_origin_model_type(), + LoadBalancingModelConfig.model_type == model_type, LoadBalancingModelConfig.model_name == model, ) load_balancing_config_count = session.execute(stmt).scalar() or 0 @@ -1364,7 +1364,7 @@ class ProviderConfiguration(BaseModel): model_setting = ProviderModelSetting( tenant_id=self.tenant_id, provider_name=self.provider.provider, - model_type=model_type.to_origin_model_type(), + model_type=model_type, model_name=model, load_balancing_enabled=True, ) @@ -1391,7 +1391,7 @@ class ProviderConfiguration(BaseModel): model_setting = ProviderModelSetting( tenant_id=self.tenant_id, provider_name=self.provider.provider, - model_type=model_type.to_origin_model_type(), + model_type=model_type, model_name=model, load_balancing_enabled=False, ) diff --git a/api/core/provider_manager.py b/api/core/provider_manager.py index b2a8e9c114..5d536e0e32 100644 --- a/api/core/provider_manager.py +++ b/api/core/provider_manager.py @@ -306,7 +306,7 @@ class ProviderManager: """ stmt = select(TenantDefaultModel).where( TenantDefaultModel.tenant_id == tenant_id, - TenantDefaultModel.model_type == model_type.to_origin_model_type(), + TenantDefaultModel.model_type == model_type, ) default_model = db.session.scalar(stmt) @@ -324,7 +324,7 @@ class ProviderManager: default_model = TenantDefaultModel( tenant_id=tenant_id, - model_type=model_type.to_origin_model_type(), + model_type=model_type, provider_name=available_model.provider.provider, model_name=available_model.model, ) @@ -391,7 +391,7 @@ class ProviderManager: raise ValueError(f"Model {model} does not exist.") stmt = select(TenantDefaultModel).where( TenantDefaultModel.tenant_id == tenant_id, - TenantDefaultModel.model_type == model_type.to_origin_model_type(), + TenantDefaultModel.model_type == model_type, ) default_model = db.session.scalar(stmt) @@ -405,7 +405,7 @@ class ProviderManager: # create default model default_model = TenantDefaultModel( tenant_id=tenant_id, - model_type=model_type.to_origin_model_type(), + model_type=model_type, provider_name=provider, model_name=model, ) @@ -822,7 +822,7 @@ class ProviderManager: custom_model_configurations.append( CustomModelConfiguration( model=provider_model_record.model_name, - model_type=ModelType.value_of(provider_model_record.model_type), + model_type=provider_model_record.model_type, credentials=provider_model_credentials, current_credential_id=provider_model_record.credential_id, current_credential_name=provider_model_record.credential_name, @@ -1201,7 +1201,7 @@ class ProviderManager: model_settings.append( ModelSettings( model=provider_model_setting.model_name, - model_type=ModelType.value_of(provider_model_setting.model_type), + model_type=provider_model_setting.model_type, enabled=provider_model_setting.enabled, load_balancing_enabled=provider_model_setting.load_balancing_enabled, load_balancing_configs=load_balancing_configs if len(load_balancing_configs) > 1 else [], diff --git a/api/models/provider.py b/api/models/provider.py index bdcfb7aa0d..8270961b31 100644 --- a/api/models/provider.py +++ b/api/models/provider.py @@ -6,6 +6,7 @@ from functools import cached_property from uuid import uuid4 import sqlalchemy as sa +from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy import DateTime, String, func, select, text from sqlalchemy.orm import Mapped, mapped_column @@ -131,7 +132,7 @@ class ProviderModel(TypeBase): tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) provider_name: Mapped[str] = mapped_column(String(255), nullable=False) model_name: Mapped[str] = mapped_column(String(255), nullable=False) - model_type: Mapped[str] = mapped_column(String(40), nullable=False) + model_type: Mapped[ModelType] = mapped_column(EnumText(ModelType, length=40), nullable=False) credential_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True, default=None) is_valid: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=text("false"), default=False) created_at: Mapped[datetime] = mapped_column( @@ -173,7 +174,7 @@ class TenantDefaultModel(TypeBase): tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) provider_name: Mapped[str] = mapped_column(String(255), nullable=False) model_name: Mapped[str] = mapped_column(String(255), nullable=False) - model_type: Mapped[str] = mapped_column(String(40), nullable=False) + model_type: Mapped[ModelType] = mapped_column(EnumText(ModelType, length=40), nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime, nullable=False, server_default=func.current_timestamp(), init=False ) @@ -253,7 +254,7 @@ class ProviderModelSetting(TypeBase): tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) provider_name: Mapped[str] = mapped_column(String(255), nullable=False) model_name: Mapped[str] = mapped_column(String(255), nullable=False) - model_type: Mapped[str] = mapped_column(String(40), nullable=False) + model_type: Mapped[ModelType] = mapped_column(EnumText(ModelType, length=40), nullable=False) enabled: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=text("true"), default=True) load_balancing_enabled: Mapped[bool] = mapped_column( sa.Boolean, nullable=False, server_default=text("false"), default=False @@ -283,7 +284,7 @@ class LoadBalancingModelConfig(TypeBase): tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) provider_name: Mapped[str] = mapped_column(String(255), nullable=False) model_name: Mapped[str] = mapped_column(String(255), nullable=False) - model_type: Mapped[str] = mapped_column(String(40), nullable=False) + model_type: Mapped[ModelType] = mapped_column(EnumText(ModelType, length=40), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) encrypted_config: Mapped[str | None] = mapped_column(LongText, nullable=True, default=None) credential_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True, default=None) @@ -348,7 +349,7 @@ class ProviderModelCredential(TypeBase): tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) provider_name: Mapped[str] = mapped_column(String(255), nullable=False) model_name: Mapped[str] = mapped_column(String(255), nullable=False) - model_type: Mapped[str] = mapped_column(String(40), nullable=False) + model_type: Mapped[ModelType] = mapped_column(EnumText(ModelType, length=40), nullable=False) credential_name: Mapped[str] = mapped_column(String(255), nullable=False) encrypted_config: Mapped[str] = mapped_column(LongText, nullable=False) created_at: Mapped[datetime] = mapped_column( diff --git a/api/services/model_load_balancing_service.py b/api/services/model_load_balancing_service.py index 25de411e43..752d3002d9 100644 --- a/api/services/model_load_balancing_service.py +++ b/api/services/model_load_balancing_service.py @@ -115,7 +115,7 @@ class ModelLoadBalancingService: .where( LoadBalancingModelConfig.tenant_id == tenant_id, LoadBalancingModelConfig.provider_name == provider_configuration.provider.provider, - LoadBalancingModelConfig.model_type == model_type_enum.to_origin_model_type(), + LoadBalancingModelConfig.model_type == model_type_enum, LoadBalancingModelConfig.model_name == model, or_( LoadBalancingModelConfig.credential_source_type == credential_source_type, @@ -240,7 +240,7 @@ class ModelLoadBalancingService: .where( LoadBalancingModelConfig.tenant_id == tenant_id, LoadBalancingModelConfig.provider_name == provider_configuration.provider.provider, - LoadBalancingModelConfig.model_type == model_type_enum.to_origin_model_type(), + LoadBalancingModelConfig.model_type == model_type_enum, LoadBalancingModelConfig.model_name == model, LoadBalancingModelConfig.id == config_id, ) @@ -288,7 +288,7 @@ class ModelLoadBalancingService: inherit_config = LoadBalancingModelConfig( tenant_id=tenant_id, provider_name=provider, - model_type=model_type.to_origin_model_type(), + model_type=model_type, model_name=model, name="__inherit__", ) @@ -328,7 +328,7 @@ class ModelLoadBalancingService: select(LoadBalancingModelConfig).where( LoadBalancingModelConfig.tenant_id == tenant_id, LoadBalancingModelConfig.provider_name == provider_configuration.provider.provider, - LoadBalancingModelConfig.model_type == model_type_enum.to_origin_model_type(), + LoadBalancingModelConfig.model_type == model_type_enum, LoadBalancingModelConfig.model_name == model, ) ).all() @@ -368,7 +368,7 @@ class ModelLoadBalancingService: tenant_id=tenant_id, provider_name=provider_configuration.provider.provider, model_name=model, - model_type=model_type_enum.to_origin_model_type(), + model_type=model_type_enum, ) .first() ) @@ -432,7 +432,7 @@ class ModelLoadBalancingService: load_balancing_model_config = LoadBalancingModelConfig( tenant_id=tenant_id, provider_name=provider_configuration.provider.provider, - model_type=model_type_enum.to_origin_model_type(), + model_type=model_type_enum, model_name=model, name=credential_record.credential_name, encrypted_config=credential_record.encrypted_config, @@ -460,7 +460,7 @@ class ModelLoadBalancingService: load_balancing_model_config = LoadBalancingModelConfig( tenant_id=tenant_id, provider_name=provider_configuration.provider.provider, - model_type=model_type_enum.to_origin_model_type(), + model_type=model_type_enum, model_name=model, name=name, encrypted_config=json.dumps(credentials), @@ -515,7 +515,7 @@ class ModelLoadBalancingService: .where( LoadBalancingModelConfig.tenant_id == tenant_id, LoadBalancingModelConfig.provider_name == provider, - LoadBalancingModelConfig.model_type == model_type_enum.to_origin_model_type(), + LoadBalancingModelConfig.model_type == model_type_enum, LoadBalancingModelConfig.model_name == model, LoadBalancingModelConfig.id == config_id, ) diff --git a/api/tests/test_containers_integration_tests/services/test_model_load_balancing_service.py b/api/tests/test_containers_integration_tests/services/test_model_load_balancing_service.py index ca6e7afeab..aca3839135 100644 --- a/api/tests/test_containers_integration_tests/services/test_model_load_balancing_service.py +++ b/api/tests/test_containers_integration_tests/services/test_model_load_balancing_service.py @@ -141,7 +141,7 @@ class TestModelLoadBalancingService: tenant_id=tenant_id, provider_name="openai", model_name="gpt-3.5-turbo", - model_type="text-generation", # Use the origin model type that matches the query + model_type="llm", enabled=True, load_balancing_enabled=False, ) @@ -298,7 +298,7 @@ class TestModelLoadBalancingService: tenant_id=tenant.id, provider_name="openai", model_name="gpt-3.5-turbo", - model_type="text-generation", # Use the origin model type that matches the query + model_type="llm", name="config1", encrypted_config='{"api_key": "test_key"}', enabled=True, @@ -417,7 +417,7 @@ class TestModelLoadBalancingService: tenant_id=tenant.id, provider_name="openai", model_name="gpt-3.5-turbo", - model_type="text-generation", # Use the origin model type that matches the query + model_type="llm", name="config1", encrypted_config='{"api_key": "test_key"}', enabled=True, diff --git a/api/tests/unit_tests/core/test_provider_manager.py b/api/tests/unit_tests/core/test_provider_manager.py index 259cb5fdd0..ee26172459 100644 --- a/api/tests/unit_tests/core/test_provider_manager.py +++ b/api/tests/unit_tests/core/test_provider_manager.py @@ -48,7 +48,7 @@ def test__to_model_settings(mocker: MockerFixture, mock_provider_entity): tenant_id="tenant_id", provider_name="openai", model_name="gpt-4", - model_type="text-generation", + model_type="llm", enabled=True, load_balancing_enabled=True, ) @@ -61,7 +61,7 @@ def test__to_model_settings(mocker: MockerFixture, mock_provider_entity): tenant_id="tenant_id", provider_name="openai", model_name="gpt-4", - model_type="text-generation", + model_type="llm", name="__inherit__", encrypted_config=None, enabled=True, @@ -70,7 +70,7 @@ def test__to_model_settings(mocker: MockerFixture, mock_provider_entity): tenant_id="tenant_id", provider_name="openai", model_name="gpt-4", - model_type="text-generation", + model_type="llm", name="first", encrypted_config='{"openai_api_key": "fake_key"}', enabled=True, @@ -110,7 +110,7 @@ def test__to_model_settings_only_one_lb(mocker: MockerFixture, mock_provider_ent tenant_id="tenant_id", provider_name="openai", model_name="gpt-4", - model_type="text-generation", + model_type="llm", enabled=True, load_balancing_enabled=True, ) @@ -121,7 +121,7 @@ def test__to_model_settings_only_one_lb(mocker: MockerFixture, mock_provider_ent tenant_id="tenant_id", provider_name="openai", model_name="gpt-4", - model_type="text-generation", + model_type="llm", name="__inherit__", encrypted_config=None, enabled=True, @@ -157,7 +157,7 @@ def test__to_model_settings_lb_disabled(mocker: MockerFixture, mock_provider_ent tenant_id="tenant_id", provider_name="openai", model_name="gpt-4", - model_type="text-generation", + model_type="llm", enabled=True, load_balancing_enabled=False, ) @@ -168,7 +168,7 @@ def test__to_model_settings_lb_disabled(mocker: MockerFixture, mock_provider_ent tenant_id="tenant_id", provider_name="openai", model_name="gpt-4", - model_type="text-generation", + model_type="llm", name="__inherit__", encrypted_config=None, enabled=True, @@ -177,7 +177,7 @@ def test__to_model_settings_lb_disabled(mocker: MockerFixture, mock_provider_ent tenant_id="tenant_id", provider_name="openai", model_name="gpt-4", - model_type="text-generation", + model_type="llm", name="first", encrypted_config='{"openai_api_key": "fake_key"}', enabled=True, @@ -270,7 +270,7 @@ def test_get_default_model_uses_injected_runtime_for_existing_default_record(moc tenant_id="tenant-id", provider_name="openai", model_name="gpt-4", - model_type=ModelType.LLM.to_origin_model_type(), + model_type=ModelType.LLM, ) mock_session = Mock() mock_session.scalar.return_value = existing_default_model @@ -449,7 +449,7 @@ def test_update_default_model_record_updates_existing_record(mocker: MockerFixtu tenant_id="tenant-id", provider_name="anthropic", model_name="claude-3-sonnet", - model_type=ModelType.LLM.to_origin_model_type(), + model_type=ModelType.LLM, ) mock_session = Mock() mock_session.scalar.return_value = existing_default_model @@ -487,7 +487,7 @@ def test_update_default_model_record_creates_record_with_origin_model_type(mocke assert created_default_model.tenant_id == "tenant-id" assert created_default_model.provider_name == "openai" assert created_default_model.model_name == "gpt-4" - assert created_default_model.model_type == ModelType.LLM.to_origin_model_type() + assert created_default_model.model_type == ModelType.LLM mock_session.commit.assert_called_once() diff --git a/api/tests/unit_tests/services/test_model_load_balancing_service.py b/api/tests/unit_tests/services/test_model_load_balancing_service.py index b43e79dff5..f85f1ace16 100644 --- a/api/tests/unit_tests/services/test_model_load_balancing_service.py +++ b/api/tests/unit_tests/services/test_model_load_balancing_service.py @@ -317,7 +317,7 @@ def test_init_inherit_config_should_create_and_persist_inherit_configuration( assert inherit_config.tenant_id == "tenant-1" assert inherit_config.provider_name == "openai" assert inherit_config.model_name == "gpt-4o-mini" - assert inherit_config.model_type == "text-generation" + assert inherit_config.model_type == "llm" assert inherit_config.name == "__inherit__" mock_db.session.add.assert_called_once_with(inherit_config) mock_db.session.commit.assert_called_once() From bbc3f90928bd167f9cfd5eb3a3001b228fd5d0e0 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 31 Mar 2026 09:51:38 +0800 Subject: [PATCH 023/199] chore(ci): move full VDB matrix off the PR path (#34216) Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/vdb-tests-full.yml | 95 ++++++++++++++++++++++++++++ .github/workflows/vdb-tests.yml | 25 ++++---- 2 files changed, 109 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/vdb-tests-full.yml diff --git a/.github/workflows/vdb-tests-full.yml b/.github/workflows/vdb-tests-full.yml new file mode 100644 index 0000000000..01d25902f6 --- /dev/null +++ b/.github/workflows/vdb-tests-full.yml @@ -0,0 +1,95 @@ +name: Run Full VDB Tests + +on: + schedule: + - cron: '0 3 * * 1' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: vdb-tests-full-${{ github.ref || github.run_id }} + cancel-in-progress: true + +jobs: + test: + name: Full VDB Tests + if: github.repository == 'langgenius/dify' + runs-on: ubuntu-latest + strategy: + matrix: + python-version: + - "3.12" + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Free Disk Space + uses: endersonmenezes/free-disk-space@7901478139cff6e9d44df5972fd8ab8fcade4db1 # v3.2.2 + with: + remove_dotnet: true + remove_haskell: true + remove_tool_cache: true + + - name: Setup UV and Python + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + with: + enable-cache: true + python-version: ${{ matrix.python-version }} + cache-dependency-glob: api/uv.lock + + - name: Check UV lockfile + run: uv lock --project api --check + + - name: Install dependencies + run: uv sync --project api --dev + + - name: Set up dotenvs + run: | + cp docker/.env.example docker/.env + cp docker/middleware.env.example docker/middleware.env + + - name: Expose Service Ports + run: sh .github/workflows/expose_service_ports.sh + +# - name: Set up Vector Store (TiDB) +# uses: hoverkraft-tech/compose-action@v2.0.2 +# with: +# compose-file: docker/tidb/docker-compose.yaml +# services: | +# tidb +# tiflash + + - name: Set up Full Vector Store Matrix + uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0 + with: + compose-file: | + docker/docker-compose.yaml + services: | + weaviate + qdrant + couchbase-server + etcd + minio + milvus-standalone + pgvecto-rs + pgvector + chroma + elasticsearch + oceanbase + + - name: setup test config + run: | + echo $(pwd) + ls -lah . + cp api/tests/integration_tests/.env.example api/tests/integration_tests/.env + +# - name: Check VDB Ready (TiDB) +# run: uv run --project api python api/tests/integration_tests/vdb/tidb_vector/check_tiflash_ready.py + + - name: Test Vector Stores + run: uv run --project api bash dev/pytest/pytest_vdb.sh diff --git a/.github/workflows/vdb-tests.yml b/.github/workflows/vdb-tests.yml index 026ff0fe57..47ec70f603 100644 --- a/.github/workflows/vdb-tests.yml +++ b/.github/workflows/vdb-tests.yml @@ -1,15 +1,18 @@ -name: Run VDB Tests +name: Run VDB Smoke Tests on: workflow_call: +permissions: + contents: read + concurrency: group: vdb-tests-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: test: - name: VDB Tests + name: VDB Smoke Tests runs-on: ubuntu-latest strategy: matrix: @@ -58,23 +61,18 @@ jobs: # tidb # tiflash - - name: Set up Vector Stores (Weaviate, Qdrant, PGVector, Milvus, PgVecto-RS, Chroma, MyScale, ElasticSearch, Couchbase, OceanBase) + - name: Set up Vector Stores for Smoke Coverage uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0 with: compose-file: | docker/docker-compose.yaml services: | + db_postgres + redis weaviate qdrant - couchbase-server - etcd - minio - milvus-standalone - pgvecto-rs pgvector chroma - elasticsearch - oceanbase - name: setup test config run: | @@ -86,4 +84,9 @@ jobs: # run: uv run --project api python api/tests/integration_tests/vdb/tidb_vector/check_tiflash_ready.py - name: Test Vector Stores - run: uv run --project api bash dev/pytest/pytest_vdb.sh + run: | + uv run --project api pytest --timeout "${PYTEST_TIMEOUT:-180}" \ + api/tests/integration_tests/vdb/chroma \ + api/tests/integration_tests/vdb/pgvector \ + api/tests/integration_tests/vdb/qdrant \ + api/tests/integration_tests/vdb/weaviate From 323c51e095a81ec0e12d4391aa3eb14b98ed92f4 Mon Sep 17 00:00:00 2001 From: Linchengyi Date: Tue, 31 Mar 2026 09:52:45 +0800 Subject: [PATCH 024/199] fix: bridge Dify design tokens for streamdown table fullscreen (#34224) --- web/app/styles/markdown.css | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/web/app/styles/markdown.css b/web/app/styles/markdown.css index 368c3718af..48aaf965ab 100644 --- a/web/app/styles/markdown.css +++ b/web/app/styles/markdown.css @@ -582,6 +582,44 @@ background-color: var(--color-components-menu-item-bg-hover); } +[data-streamdown="table-fullscreen"] { + background-color: var(--color-components-panel-bg); + color: var(--color-text-primary); +} + +[data-streamdown="table-fullscreen"] button { + color: var(--color-text-tertiary); +} + +[data-streamdown="table-fullscreen"] button:hover { + color: var(--color-text-primary); + background-color: var(--color-background-section-burn); +} + +[data-streamdown="table-fullscreen"] table[data-streamdown="table"] { + border-color: var(--color-divider-regular); +} + +[data-streamdown="table-fullscreen"] [data-streamdown="table-header"] { + background-color: var(--color-background-section-burn); +} + +[data-streamdown="table-fullscreen"] [data-streamdown="table-header"] th { + color: var(--color-text-tertiary); +} + +[data-streamdown="table-fullscreen"] [data-streamdown="table-body"] { + border-color: var(--color-divider-subtle); +} + +[data-streamdown="table-fullscreen"] [data-streamdown="table-body"] > tr { + border-color: var(--color-divider-subtle); +} + +[data-streamdown="table-fullscreen"] [data-streamdown="table-body"] td { + color: var(--color-text-secondary); +} + .markdown-body table img { background-color: transparent; } From a19243068b7de40fbd52f50046fd19bb575adbcd Mon Sep 17 00:00:00 2001 From: fisherOne1 Date: Tue, 31 Mar 2026 10:07:37 +0800 Subject: [PATCH 025/199] fix(web): fix document detail page status inconsistency with list page (#33740) Co-authored-by: fisher <1186907891@qq.com> Co-authored-by: Wu Tianwei <30284043+WTW0313@users.noreply.github.com> Co-authored-by: Crazywoola <100913391+crazywoola@users.noreply.github.com> --- .../datasets/documents/detail/index.tsx | 105 +++++++++++------- web/service/knowledge/use-document.ts | 6 +- 2 files changed, 69 insertions(+), 42 deletions(-) diff --git a/web/app/components/datasets/documents/detail/index.tsx b/web/app/components/datasets/documents/detail/index.tsx index 891c177169..cf9aa62fab 100644 --- a/web/app/components/datasets/documents/detail/index.tsx +++ b/web/app/components/datasets/documents/detail/index.tsx @@ -1,8 +1,8 @@ 'use client' import type { FC } from 'react' -import type { DataSourceInfo, FileItem, FullDocumentDetail, LegacyDataSourceInfo } from '@/models/datasets' +import type { DataSourceInfo, DocumentDisplayStatus, FileItem, FullDocumentDetail, LegacyDataSourceInfo } from '@/models/datasets' import * as React from 'react' -import { useMemo, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' import FloatRightContainer from '@/app/components/base/float-right-container' @@ -11,7 +11,7 @@ import Toast from '@/app/components/base/toast' import Metadata from '@/app/components/datasets/metadata/metadata-document' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' -import { ChunkingMode } from '@/models/datasets' +import { ChunkingMode, DisplayStatusList } from '@/models/datasets' import { useRouter, useSearchParams } from '@/next/navigation' import { useDocumentDetail, useDocumentMetadata, useInvalidDocumentList } from '@/service/knowledge/use-document' import { useCheckSegmentBatchImportProgress, useChildSegmentListKey, useSegmentBatchImport, useSegmentListKey } from '@/service/knowledge/use-segment' @@ -32,6 +32,14 @@ type DocumentDetailProps = { documentId: string } +const NON_TERMINAL_DISPLAY_STATUSES = new Set( + DisplayStatusList.filter(s => s === 'queuing' || s === 'indexing' || s === 'paused'), +) + +const isLegacyDataSourceInfo = (info?: DataSourceInfo): info is LegacyDataSourceInfo => { + return !!info && 'upload_file' in info +} + const DocumentDetail: FC = ({ datasetId, documentId }) => { const router = useRouter() const searchParams = useSearchParams() @@ -89,6 +97,12 @@ const DocumentDetail: FC = ({ datasetId, documentId }) => { datasetId, documentId, params: { metadata: 'without' }, + refetchInterval: (query) => { + const status = query.state.data?.display_status + if (!status || NON_TERMINAL_DISPLAY_STATUSES.has(status)) + return 2500 + return false + }, }) const { data: documentMetadata } = useDocumentMetadata({ @@ -97,19 +111,15 @@ const DocumentDetail: FC = ({ datasetId, documentId }) => { params: { metadata: 'only' }, }) - const backToPrev = () => { + const backToPrev = useCallback(() => { const queryString = searchParams.toString() const backPath = `/datasets/${datasetId}/documents${queryString ? `?${queryString}` : ''}` router.push(backPath) - } + }, [searchParams, datasetId, router]) const isDetailLoading = !documentDetail && !error - const embedding = ['queuing', 'indexing', 'paused'].includes((documentDetail?.display_status || '').toLowerCase()) - - const isLegacyDataSourceInfo = (info?: DataSourceInfo): info is LegacyDataSourceInfo => { - return !!info && 'upload_file' in info - } + const embedding = NON_TERMINAL_DISPLAY_STATUSES.has(documentDetail?.display_status as DocumentDisplayStatus) const documentUploadFile = useMemo(() => { if (!documentDetail?.data_source_info) @@ -123,7 +133,7 @@ const DocumentDetail: FC = ({ datasetId, documentId }) => { const invalidChildChunkList = useInvalid(useChildSegmentListKey) const invalidDocumentList = useInvalidDocumentList(datasetId) - const handleOperate = (operateName?: string) => { + const handleOperate = useCallback((operateName?: string) => { invalidDocumentList() if (operateName === 'delete') { backToPrev() @@ -138,7 +148,7 @@ const DocumentDetail: FC = ({ datasetId, documentId }) => { }, 5000) } } - } + }, [invalidDocumentList, backToPrev, detailMutate, invalidChunkList, invalidChildChunkList]) const parentMode = useMemo(() => { return documentDetail?.document_process_rule?.rules?.parent_mode || documentDetail?.dataset_process_rule?.rules?.parent_mode || 'paragraph' @@ -149,19 +159,41 @@ const DocumentDetail: FC = ({ datasetId, documentId }) => { return chunkMode === ChunkingMode.parentChild && parentMode === 'full-doc' }, [documentDetail?.doc_form, parentMode]) + const contextValue = useMemo(() => ({ + datasetId, + documentId, + docForm: documentDetail?.doc_form as ChunkingMode, + parentMode, + }), [datasetId, documentId, documentDetail?.doc_form, parentMode]) + + const statusDetail = useMemo(() => ({ + enabled: documentDetail?.enabled || false, + archived: documentDetail?.archived || false, + id: documentId, + }), [documentDetail?.enabled, documentDetail?.archived, documentId]) + + const operationsDetail = useMemo(() => ({ + name: documentDetail?.name || '', + enabled: documentDetail?.enabled || false, + archived: documentDetail?.archived || false, + id: documentId, + data_source_type: documentDetail?.data_source_type || '', + doc_form: documentDetail?.doc_form || '', + }), [documentDetail?.name, documentDetail?.enabled, documentDetail?.archived, documentId, documentDetail?.data_source_type, documentDetail?.doc_form]) + + const docDetail = useMemo(() => ({ + ...documentDetail, + ...documentMetadata, + doc_type: documentMetadata?.doc_type === 'others' ? '' : documentMetadata?.doc_type, + } as FullDocumentDetail), [documentDetail, documentMetadata]) + const backButtonLabel = t('operation.back', { ns: 'common' }) const metadataToggleLabel = `${showMetadata ? t('operation.close', { ns: 'common' }) : t('operation.view', { ns: 'common' })} ${t('metadata.title', { ns: 'datasetDocuments' })}` return ( - +
diff --git a/web/service/knowledge/use-document.ts b/web/service/knowledge/use-document.ts index 4eb2b7d282..6e48d7b6f4 100644 --- a/web/service/knowledge/use-document.ts +++ b/web/service/knowledge/use-document.ts @@ -133,15 +133,19 @@ export const useSyncWebsite = () => { } const useDocumentDetailKey = [NAME_SPACE, 'documentDetail', 'withoutMetaData'] +type DocumentDetailRefetchInterval = UseQueryOptions['refetchInterval'] + export const useDocumentDetail = (payload: { datasetId: string documentId: string params: { metadata: MetadataType } + refetchInterval?: DocumentDetailRefetchInterval }) => { - const { datasetId, documentId, params } = payload + const { datasetId, documentId, params, refetchInterval } = payload return useQuery({ queryKey: [...useDocumentDetailKey, 'withoutMetaData', datasetId, documentId, params], queryFn: () => get(`/datasets/${datasetId}/documents/${documentId}`, { params }), + refetchInterval, }) } From f0e6f11c1c871d794b124c2bc41c9099d7508df2 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Tue, 31 Mar 2026 11:11:21 +0900 Subject: [PATCH 026/199] fix: silent diff when number count are the same (#34097) --- .github/workflows/pyrefly-diff.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pyrefly-diff.yml b/.github/workflows/pyrefly-diff.yml index 0b2a7b8e9e..8623d35b04 100644 --- a/.github/workflows/pyrefly-diff.yml +++ b/.github/workflows/pyrefly-diff.yml @@ -50,6 +50,17 @@ jobs: run: | diff -u /tmp/pyrefly_base.txt /tmp/pyrefly_pr.txt > pyrefly_diff.txt || true + - name: Check if line counts match + id: line_count_check + run: | + base_lines=$(wc -l < /tmp/pyrefly_base.txt) + pr_lines=$(wc -l < /tmp/pyrefly_pr.txt) + if [ "$base_lines" -eq "$pr_lines" ]; then + echo "same=true" >> $GITHUB_OUTPUT + else + echo "same=false" >> $GITHUB_OUTPUT + fi + - name: Save PR number run: | echo ${{ github.event.pull_request.number }} > pr_number.txt @@ -63,7 +74,7 @@ jobs: pr_number.txt - name: Comment PR with pyrefly diff - if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + if: ${{ github.event.pull_request.head.repo.full_name == github.repository && steps.line_count_check.outputs.same == 'false' }} uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} From f7b78b08fd2adc12980606a4a975aae840ae5e7e Mon Sep 17 00:00:00 2001 From: wangji0923 Date: Tue, 31 Mar 2026 10:13:31 +0800 Subject: [PATCH 027/199] refactor(api): narrow otel instrumentor typing (#33853) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 复试资料 Co-authored-by: Asuka Minato --- api/extensions/otel/instrumentation.py | 44 +++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/api/extensions/otel/instrumentation.py b/api/extensions/otel/instrumentation.py index b73ba8df8c..0a70f6ebe9 100644 --- a/api/extensions/otel/instrumentation.py +++ b/api/extensions/otel/instrumentation.py @@ -1,5 +1,7 @@ import contextlib import logging +from collections.abc import Callable +from typing import Protocol, cast import flask from opentelemetry.instrumentation.celery import CeleryInstrumentor @@ -21,6 +23,38 @@ from extensions.otel.runtime import is_celery_worker logger = logging.getLogger(__name__) +class SupportsInstrument(Protocol): + def instrument(self, **kwargs: object) -> None: ... + + +class SupportsFlaskInstrumentor(Protocol): + def instrument_app( + self, app: DifyApp, response_hook: Callable[[Span, str, list], None] | None = None, **kwargs: object + ) -> None: ... + + +# Some OpenTelemetry instrumentor constructors are typed loosely enough that +# pyrefly infers `NoneType`. Narrow the instances to just the methods we use +# while leaving runtime behavior unchanged. +def _new_celery_instrumentor() -> SupportsInstrument: + return cast( + SupportsInstrument, + CeleryInstrumentor(tracer_provider=get_tracer_provider(), meter_provider=get_meter_provider()), + ) + + +def _new_httpx_instrumentor() -> SupportsInstrument: + return cast(SupportsInstrument, HTTPXClientInstrumentor()) + + +def _new_redis_instrumentor() -> SupportsInstrument: + return cast(SupportsInstrument, RedisInstrumentor()) + + +def _new_sqlalchemy_instrumentor() -> SupportsInstrument: + return cast(SupportsInstrument, SQLAlchemyInstrumentor()) + + class ExceptionLoggingHandler(logging.Handler): """ Handler that records exceptions to the current OpenTelemetry span. @@ -97,7 +131,7 @@ def init_flask_instrumentor(app: DifyApp) -> None: from opentelemetry.instrumentation.flask import FlaskInstrumentor - instrumentor = FlaskInstrumentor() + instrumentor = cast(SupportsFlaskInstrumentor, FlaskInstrumentor()) if dify_config.DEBUG: logger.info("Initializing Flask instrumentor") instrumentor.instrument_app(app, response_hook=response_hook) @@ -106,21 +140,21 @@ def init_flask_instrumentor(app: DifyApp) -> None: def init_sqlalchemy_instrumentor(app: DifyApp) -> None: with app.app_context(): engines = list(app.extensions["sqlalchemy"].engines.values()) - SQLAlchemyInstrumentor().instrument(enable_commenter=True, engines=engines) + _new_sqlalchemy_instrumentor().instrument(enable_commenter=True, engines=engines) def init_redis_instrumentor() -> None: - RedisInstrumentor().instrument() + _new_redis_instrumentor().instrument() def init_httpx_instrumentor() -> None: - HTTPXClientInstrumentor().instrument() + _new_httpx_instrumentor().instrument() def init_instruments(app: DifyApp) -> None: if not is_celery_worker(): init_flask_instrumentor(app) - CeleryInstrumentor(tracer_provider=get_tracer_provider(), meter_provider=get_meter_provider()).instrument() + _new_celery_instrumentor().instrument() instrument_exception_logging() init_sqlalchemy_instrumentor(app) From 2c2cc72150af67410426f66b491200a9b25c7ab7 Mon Sep 17 00:00:00 2001 From: Jordan Date: Tue, 31 Mar 2026 11:20:21 +0900 Subject: [PATCH 028/199] fix(http): expose structured vars in HTTP body selector (#34185) Co-authored-by: Jordan <175169034+owldev127@users.noreply.github.com> --- .../http/components/edit-body/index.spec.tsx | 30 +++++++++++++++++++ .../nodes/http/components/edit-body/index.tsx | 3 +- .../edit-body/supported-body-vars.ts | 15 ++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 web/app/components/workflow/nodes/http/components/edit-body/index.spec.tsx create mode 100644 web/app/components/workflow/nodes/http/components/edit-body/supported-body-vars.ts diff --git a/web/app/components/workflow/nodes/http/components/edit-body/index.spec.tsx b/web/app/components/workflow/nodes/http/components/edit-body/index.spec.tsx new file mode 100644 index 0000000000..a3078464b8 --- /dev/null +++ b/web/app/components/workflow/nodes/http/components/edit-body/index.spec.tsx @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest' +import { VarType } from '@/app/components/workflow/types' +import { + HTTP_BODY_VARIABLE_TYPES, + isSupportedHttpBodyVariable, +} from './supported-body-vars' + +describe('HTTP body variable support', () => { + it('should include structured variables in the selector', () => { + expect(HTTP_BODY_VARIABLE_TYPES).toEqual([ + VarType.string, + VarType.number, + VarType.secret, + VarType.object, + VarType.arrayNumber, + VarType.arrayString, + VarType.arrayObject, + ]) + }) + + it('should accept object and array object variables', () => { + expect(isSupportedHttpBodyVariable(VarType.object)).toBe(true) + expect(isSupportedHttpBodyVariable(VarType.arrayObject)).toBe(true) + }) + + it('should keep unsupported file variables excluded', () => { + expect(isSupportedHttpBodyVariable(VarType.file)).toBe(false) + expect(isSupportedHttpBodyVariable(VarType.arrayFile)).toBe(false) + }) +}) diff --git a/web/app/components/workflow/nodes/http/components/edit-body/index.tsx b/web/app/components/workflow/nodes/http/components/edit-body/index.tsx index 90c230b6bb..7e44ad5aeb 100644 --- a/web/app/components/workflow/nodes/http/components/edit-body/index.tsx +++ b/web/app/components/workflow/nodes/http/components/edit-body/index.tsx @@ -13,6 +13,7 @@ import VarReferencePicker from '../../../_base/components/variable/var-reference import useAvailableVarList from '../../../_base/hooks/use-available-var-list' import { BodyPayloadValueType, BodyType } from '../../types' import KeyValue from '../key-value' +import { isSupportedHttpBodyVariable } from './supported-body-vars' const UNIQUE_ID_PREFIX = 'key-value-' @@ -58,7 +59,7 @@ const EditBody: FC = ({ const { availableVars, availableNodes } = useAvailableVarList(nodeId, { onlyLeafNodeVar: false, filterVar: (varPayload: Var) => { - return [VarType.string, VarType.number, VarType.secret, VarType.arrayNumber, VarType.arrayString].includes(varPayload.type) + return isSupportedHttpBodyVariable(varPayload.type) }, }) diff --git a/web/app/components/workflow/nodes/http/components/edit-body/supported-body-vars.ts b/web/app/components/workflow/nodes/http/components/edit-body/supported-body-vars.ts new file mode 100644 index 0000000000..416777abfa --- /dev/null +++ b/web/app/components/workflow/nodes/http/components/edit-body/supported-body-vars.ts @@ -0,0 +1,15 @@ +import { VarType } from '@/app/components/workflow/types' + +export const HTTP_BODY_VARIABLE_TYPES: VarType[] = [ + VarType.string, + VarType.number, + VarType.secret, + VarType.object, + VarType.arrayNumber, + VarType.arrayString, + VarType.arrayObject, +] + +export const isSupportedHttpBodyVariable = (type: VarType) => { + return HTTP_BODY_VARIABLE_TYPES.includes(type) +} From 01c857a67ab74e70af35b85d84bb5a093d76d083 Mon Sep 17 00:00:00 2001 From: Dominic <161279854+dominciyue@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:20:45 +0800 Subject: [PATCH 029/199] fix(dev): load middleware env in start-docker-compose (#33927) --- dev/start-docker-compose | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dev/start-docker-compose b/dev/start-docker-compose index 9652be169d..aa4f66a6cf 100755 --- a/dev/start-docker-compose +++ b/dev/start-docker-compose @@ -1,8 +1,8 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(dirname "$(realpath "$0")")" -ROOT="$(dirname "$SCRIPT_DIR")" - -cd "$ROOT/docker" -docker compose -f docker-compose.middleware.yaml --profile postgresql --profile weaviate -p dify up -d +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(dirname "$(realpath "$0")")" +ROOT="$(dirname "$SCRIPT_DIR")" + +cd "$ROOT/docker" +docker compose --env-file middleware.env -f docker-compose.middleware.yaml -p dify up -d From 7e4754392d74dcfbf976cac16b3c2ead1b9aaace Mon Sep 17 00:00:00 2001 From: Weichen Zhao <61238101+SeasonPilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:17:47 +0800 Subject: [PATCH 030/199] feat: increase default celery worker concurrency to 4 (#33105) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Crazywoola <100913391+crazywoola@users.noreply.github.com> --- docker/.env.example | 6 ++++-- docker/docker-compose.yaml | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docker/.env.example b/docker/.env.example index 9fbf9a9e72..b2d6244b46 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -186,8 +186,10 @@ CELERY_WORKER_CLASS= # it is recommended to set it to 360 to support a longer sse connection time. GUNICORN_TIMEOUT=360 -# The number of Celery workers. The default is 1, and can be set as needed. -CELERY_WORKER_AMOUNT= +# The number of Celery workers. The default is 4 for development environments +# to allow parallel processing of workflows, document indexing, and other async tasks. +# Adjust based on your system resources and workload requirements. +CELERY_WORKER_AMOUNT=4 # Flag indicating whether to enable autoscaling of Celery workers. # diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 737a62020c..ed68107f46 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -46,7 +46,7 @@ x-shared-env: &shared-api-worker-env SERVER_WORKER_CONNECTIONS: ${SERVER_WORKER_CONNECTIONS:-10} CELERY_WORKER_CLASS: ${CELERY_WORKER_CLASS:-} GUNICORN_TIMEOUT: ${GUNICORN_TIMEOUT:-360} - CELERY_WORKER_AMOUNT: ${CELERY_WORKER_AMOUNT:-} + CELERY_WORKER_AMOUNT: ${CELERY_WORKER_AMOUNT:-4} CELERY_AUTO_SCALE: ${CELERY_AUTO_SCALE:-false} CELERY_MAX_WORKERS: ${CELERY_MAX_WORKERS:-} CELERY_MIN_WORKERS: ${CELERY_MIN_WORKERS:-} From 2de818530bf384dff43d473456a2e09ea60190f1 Mon Sep 17 00:00:00 2001 From: Dev Sharma <50591491+cryptus-neoxys@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:46:42 +0530 Subject: [PATCH 031/199] test: add tests for api/services retention, enterprise, plugin (#32648) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: QuantumGhost --- .../services/plugin/__init__.py | 0 .../services/plugin/test_plugin_lifecycle.py | 182 ++++++ .../services/retention/__init__.py | 0 .../retention/test_messages_clean_service.py | 348 ++++++++++ .../retention/test_workflow_run_archiver.py | 177 ++++++ .../enterprise/test_enterprise_service.py | 170 +++++ .../enterprise/test_plugin_manager_service.py | 49 ++ .../test_plugin_auto_upgrade_service.py | 183 ++++++ .../plugin/test_plugin_permission_service.py | 75 +++ .../unit_tests/services/retention/__init__.py | 0 .../retention/test_messages_clean_policy.py | 135 ++++ .../tools/test_tools_transform_service.py | 598 ++++++++++++++++++ 12 files changed, 1917 insertions(+) create mode 100644 api/tests/integration_tests/services/plugin/__init__.py create mode 100644 api/tests/integration_tests/services/plugin/test_plugin_lifecycle.py create mode 100644 api/tests/integration_tests/services/retention/__init__.py create mode 100644 api/tests/integration_tests/services/retention/test_messages_clean_service.py create mode 100644 api/tests/integration_tests/services/retention/test_workflow_run_archiver.py create mode 100644 api/tests/unit_tests/services/plugin/test_plugin_auto_upgrade_service.py create mode 100644 api/tests/unit_tests/services/plugin/test_plugin_permission_service.py create mode 100644 api/tests/unit_tests/services/retention/__init__.py create mode 100644 api/tests/unit_tests/services/retention/test_messages_clean_policy.py create mode 100644 api/tests/unit_tests/services/tools/test_tools_transform_service.py diff --git a/api/tests/integration_tests/services/plugin/__init__.py b/api/tests/integration_tests/services/plugin/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/integration_tests/services/plugin/test_plugin_lifecycle.py b/api/tests/integration_tests/services/plugin/test_plugin_lifecycle.py new file mode 100644 index 0000000000..951a5ab4b4 --- /dev/null +++ b/api/tests/integration_tests/services/plugin/test_plugin_lifecycle.py @@ -0,0 +1,182 @@ +import pytest +from sqlalchemy import delete + +from core.db.session_factory import session_factory +from models import Tenant +from models.account import TenantPluginAutoUpgradeStrategy, TenantPluginPermission +from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService +from services.plugin.plugin_permission_service import PluginPermissionService + + +@pytest.fixture +def tenant(flask_req_ctx): + with session_factory.create_session() as session: + t = Tenant(name="plugin_it_tenant") + session.add(t) + session.commit() + tenant_id = t.id + + yield tenant_id + + with session_factory.create_session() as session: + session.execute(delete(TenantPluginPermission).where(TenantPluginPermission.tenant_id == tenant_id)) + session.execute( + delete(TenantPluginAutoUpgradeStrategy).where(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id) + ) + session.execute(delete(Tenant).where(Tenant.id == tenant_id)) + session.commit() + + +class TestPluginPermissionLifecycle: + def test_get_returns_none_for_new_tenant(self, tenant): + assert PluginPermissionService.get_permission(tenant) is None + + def test_change_creates_row(self, tenant): + result = PluginPermissionService.change_permission( + tenant, + TenantPluginPermission.InstallPermission.ADMINS, + TenantPluginPermission.DebugPermission.EVERYONE, + ) + assert result is True + + perm = PluginPermissionService.get_permission(tenant) + assert perm is not None + assert perm.install_permission == TenantPluginPermission.InstallPermission.ADMINS + assert perm.debug_permission == TenantPluginPermission.DebugPermission.EVERYONE + + def test_change_updates_existing_row(self, tenant): + PluginPermissionService.change_permission( + tenant, + TenantPluginPermission.InstallPermission.ADMINS, + TenantPluginPermission.DebugPermission.NOBODY, + ) + PluginPermissionService.change_permission( + tenant, + TenantPluginPermission.InstallPermission.EVERYONE, + TenantPluginPermission.DebugPermission.ADMINS, + ) + perm = PluginPermissionService.get_permission(tenant) + assert perm is not None + assert perm.install_permission == TenantPluginPermission.InstallPermission.EVERYONE + assert perm.debug_permission == TenantPluginPermission.DebugPermission.ADMINS + + with session_factory.create_session() as session: + count = session.query(TenantPluginPermission).where(TenantPluginPermission.tenant_id == tenant).count() + assert count == 1 + + +class TestPluginAutoUpgradeLifecycle: + def test_get_returns_none_for_new_tenant(self, tenant): + assert PluginAutoUpgradeService.get_strategy(tenant) is None + + def test_change_creates_row(self, tenant): + result = PluginAutoUpgradeService.change_strategy( + tenant, + strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST, + upgrade_time_of_day=3, + upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL, + exclude_plugins=[], + include_plugins=[], + ) + assert result is True + + strategy = PluginAutoUpgradeService.get_strategy(tenant) + assert strategy is not None + assert strategy.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST + assert strategy.upgrade_time_of_day == 3 + + def test_change_updates_existing_row(self, tenant): + PluginAutoUpgradeService.change_strategy( + tenant, + strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY, + upgrade_time_of_day=0, + upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL, + exclude_plugins=[], + include_plugins=[], + ) + PluginAutoUpgradeService.change_strategy( + tenant, + strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST, + upgrade_time_of_day=12, + upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL, + exclude_plugins=[], + include_plugins=["plugin-a"], + ) + + strategy = PluginAutoUpgradeService.get_strategy(tenant) + assert strategy is not None + assert strategy.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST + assert strategy.upgrade_time_of_day == 12 + assert strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL + assert strategy.include_plugins == ["plugin-a"] + + def test_exclude_plugin_creates_strategy_when_none_exists(self, tenant): + PluginAutoUpgradeService.exclude_plugin(tenant, "my-plugin") + + strategy = PluginAutoUpgradeService.get_strategy(tenant) + assert strategy is not None + assert strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE + assert "my-plugin" in strategy.exclude_plugins + + def test_exclude_plugin_appends_in_exclude_mode(self, tenant): + PluginAutoUpgradeService.change_strategy( + tenant, + strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY, + upgrade_time_of_day=0, + upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE, + exclude_plugins=["existing"], + include_plugins=[], + ) + PluginAutoUpgradeService.exclude_plugin(tenant, "new-plugin") + + strategy = PluginAutoUpgradeService.get_strategy(tenant) + assert strategy is not None + assert "existing" in strategy.exclude_plugins + assert "new-plugin" in strategy.exclude_plugins + + def test_exclude_plugin_dedup_in_exclude_mode(self, tenant): + PluginAutoUpgradeService.change_strategy( + tenant, + strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY, + upgrade_time_of_day=0, + upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE, + exclude_plugins=["same-plugin"], + include_plugins=[], + ) + PluginAutoUpgradeService.exclude_plugin(tenant, "same-plugin") + + strategy = PluginAutoUpgradeService.get_strategy(tenant) + assert strategy is not None + assert strategy.exclude_plugins.count("same-plugin") == 1 + + def test_exclude_from_partial_mode_removes_from_include(self, tenant): + PluginAutoUpgradeService.change_strategy( + tenant, + strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY, + upgrade_time_of_day=0, + upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL, + exclude_plugins=[], + include_plugins=["p1", "p2"], + ) + PluginAutoUpgradeService.exclude_plugin(tenant, "p1") + + strategy = PluginAutoUpgradeService.get_strategy(tenant) + assert strategy is not None + assert "p1" not in strategy.include_plugins + assert "p2" in strategy.include_plugins + + def test_exclude_from_all_mode_switches_to_exclude(self, tenant): + PluginAutoUpgradeService.change_strategy( + tenant, + strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST, + upgrade_time_of_day=0, + upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL, + exclude_plugins=[], + include_plugins=[], + ) + PluginAutoUpgradeService.exclude_plugin(tenant, "excluded-plugin") + + strategy = PluginAutoUpgradeService.get_strategy(tenant) + assert strategy is not None + assert strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE + assert "excluded-plugin" in strategy.exclude_plugins diff --git a/api/tests/integration_tests/services/retention/__init__.py b/api/tests/integration_tests/services/retention/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/integration_tests/services/retention/test_messages_clean_service.py b/api/tests/integration_tests/services/retention/test_messages_clean_service.py new file mode 100644 index 0000000000..348bb0af4a --- /dev/null +++ b/api/tests/integration_tests/services/retention/test_messages_clean_service.py @@ -0,0 +1,348 @@ +import datetime +import math +import uuid + +import pytest +from sqlalchemy import delete + +from core.db.session_factory import session_factory +from models import Tenant +from models.enums import FeedbackFromSource, FeedbackRating +from models.model import ( + App, + Conversation, + Message, + MessageAnnotation, + MessageFeedback, +) +from services.retention.conversation.messages_clean_policy import BillingDisabledPolicy +from services.retention.conversation.messages_clean_service import MessagesCleanService + +_NOW = datetime.datetime(2026, 1, 15, 12, 0, 0, tzinfo=datetime.UTC) +_OLD = _NOW - datetime.timedelta(days=60) +_VERY_OLD = _NOW - datetime.timedelta(days=90) +_RECENT = _NOW - datetime.timedelta(days=5) + +_WINDOW_START = _VERY_OLD - datetime.timedelta(hours=1) +_WINDOW_END = _RECENT + datetime.timedelta(hours=1) + +_DEFAULT_BATCH_SIZE = 100 +_PAGINATION_MESSAGE_COUNT = 25 +_PAGINATION_BATCH_SIZE = 8 + + +@pytest.fixture +def tenant_and_app(flask_req_ctx): + """Creates a Tenant, App and Conversation for the test and cleans up after.""" + with session_factory.create_session() as session: + tenant = Tenant(name="retention_it_tenant") + session.add(tenant) + session.flush() + + app = App( + tenant_id=tenant.id, + name="Retention IT App", + mode="chat", + enable_site=True, + enable_api=True, + ) + session.add(app) + session.flush() + + conv = Conversation( + app_id=app.id, + mode="chat", + name="test_conv", + status="normal", + from_source="console", + _inputs={}, + ) + session.add(conv) + session.commit() + + tenant_id = tenant.id + app_id = app.id + conv_id = conv.id + + yield {"tenant_id": tenant_id, "app_id": app_id, "conversation_id": conv_id} + + with session_factory.create_session() as session: + session.execute(delete(Conversation).where(Conversation.id == conv_id)) + session.execute(delete(App).where(App.id == app_id)) + session.execute(delete(Tenant).where(Tenant.id == tenant_id)) + session.commit() + + +def _make_message(app_id: str, conversation_id: str, created_at: datetime.datetime) -> Message: + return Message( + app_id=app_id, + conversation_id=conversation_id, + query="test", + message=[{"text": "hello"}], + answer="world", + message_tokens=1, + message_unit_price=0, + answer_tokens=1, + answer_unit_price=0, + from_source="console", + currency="USD", + _inputs={}, + created_at=created_at, + ) + + +class TestMessagesCleanServiceIntegration: + @pytest.fixture + def seed_messages(self, tenant_and_app): + """Seeds one message at each of _VERY_OLD, _OLD, and _RECENT. + Yields a semantic mapping keyed by age label. + """ + data = tenant_and_app + app_id = data["app_id"] + conv_id = data["conversation_id"] + # Ordered tuple of (label, timestamp) for deterministic seeding + timestamps = [ + ("very_old", _VERY_OLD), + ("old", _OLD), + ("recent", _RECENT), + ] + msg_ids: dict[str, str] = {} + + with session_factory.create_session() as session: + for label, ts in timestamps: + msg = _make_message(app_id, conv_id, ts) + session.add(msg) + session.flush() + msg_ids[label] = msg.id + session.commit() + + yield {"msg_ids": msg_ids, **data} + + with session_factory.create_session() as session: + session.execute( + delete(Message) + .where(Message.id.in_(list(msg_ids.values()))) + .execution_options(synchronize_session=False) + ) + session.commit() + + @pytest.fixture + def paginated_seed_messages(self, tenant_and_app): + """Seeds multiple messages separated by 1-second increments starting at _OLD.""" + data = tenant_and_app + app_id = data["app_id"] + conv_id = data["conversation_id"] + msg_ids: list[str] = [] + + with session_factory.create_session() as session: + for i in range(_PAGINATION_MESSAGE_COUNT): + ts = _OLD + datetime.timedelta(seconds=i) + msg = _make_message(app_id, conv_id, ts) + session.add(msg) + session.flush() + msg_ids.append(msg.id) + session.commit() + + yield {"msg_ids": msg_ids, **data} + + with session_factory.create_session() as session: + session.execute(delete(Message).where(Message.id.in_(msg_ids)).execution_options(synchronize_session=False)) + session.commit() + + @pytest.fixture + def cascade_test_data(self, tenant_and_app): + """Seeds one Message with an associated Feedback and Annotation.""" + data = tenant_and_app + app_id = data["app_id"] + conv_id = data["conversation_id"] + + with session_factory.create_session() as session: + msg = _make_message(app_id, conv_id, _OLD) + session.add(msg) + session.flush() + + feedback = MessageFeedback( + app_id=app_id, + conversation_id=conv_id, + message_id=msg.id, + rating=FeedbackRating.LIKE, + from_source=FeedbackFromSource.USER, + ) + annotation = MessageAnnotation( + app_id=app_id, + conversation_id=conv_id, + message_id=msg.id, + question="q", + content="a", + account_id=str(uuid.uuid4()), + ) + session.add_all([feedback, annotation]) + session.commit() + + msg_id = msg.id + fb_id = feedback.id + ann_id = annotation.id + + yield {"msg_id": msg_id, "fb_id": fb_id, "ann_id": ann_id, **data} + + with session_factory.create_session() as session: + session.execute(delete(MessageAnnotation).where(MessageAnnotation.id == ann_id)) + session.execute(delete(MessageFeedback).where(MessageFeedback.id == fb_id)) + session.execute(delete(Message).where(Message.id == msg_id)) + session.commit() + + def test_dry_run_does_not_delete(self, seed_messages): + """Dry-run must count eligible rows without deleting any of them.""" + data = seed_messages + msg_ids = data["msg_ids"] + all_ids = list(msg_ids.values()) + + svc = MessagesCleanService.from_time_range( + policy=BillingDisabledPolicy(), + start_from=_WINDOW_START, + end_before=_WINDOW_END, + batch_size=_DEFAULT_BATCH_SIZE, + dry_run=True, + ) + stats = svc.run() + + assert stats["filtered_messages"] == len(all_ids) + assert stats["total_deleted"] == 0 + + with session_factory.create_session() as session: + remaining = session.query(Message).where(Message.id.in_(all_ids)).count() + assert remaining == len(all_ids) + + def test_billing_disabled_deletes_all_in_range(self, seed_messages): + """All 3 seeded messages fall within the window and must be deleted.""" + data = seed_messages + msg_ids = data["msg_ids"] + all_ids = list(msg_ids.values()) + + svc = MessagesCleanService.from_time_range( + policy=BillingDisabledPolicy(), + start_from=_WINDOW_START, + end_before=_WINDOW_END, + batch_size=_DEFAULT_BATCH_SIZE, + dry_run=False, + ) + stats = svc.run() + + assert stats["total_deleted"] == len(all_ids) + + with session_factory.create_session() as session: + remaining = session.query(Message).where(Message.id.in_(all_ids)).count() + assert remaining == 0 + + def test_start_from_filters_correctly(self, seed_messages): + """Only the message at _OLD falls within the narrow ±1 h window.""" + data = seed_messages + msg_ids = data["msg_ids"] + + start = _OLD - datetime.timedelta(hours=1) + end = _OLD + datetime.timedelta(hours=1) + + svc = MessagesCleanService.from_time_range( + policy=BillingDisabledPolicy(), + start_from=start, + end_before=end, + batch_size=_DEFAULT_BATCH_SIZE, + ) + stats = svc.run() + + assert stats["total_deleted"] == 1 + + with session_factory.create_session() as session: + all_ids = list(msg_ids.values()) + remaining_ids = {r[0] for r in session.query(Message.id).where(Message.id.in_(all_ids)).all()} + + assert msg_ids["old"] not in remaining_ids + assert msg_ids["very_old"] in remaining_ids + assert msg_ids["recent"] in remaining_ids + + def test_cursor_pagination_across_batches(self, paginated_seed_messages): + """Messages must be deleted across multiple batches.""" + data = paginated_seed_messages + msg_ids = data["msg_ids"] + + # _OLD is the earliest; the last one is _OLD + (_PAGINATION_MESSAGE_COUNT - 1) s. + pagination_window_start = _OLD - datetime.timedelta(seconds=1) + pagination_window_end = _OLD + datetime.timedelta(seconds=_PAGINATION_MESSAGE_COUNT) + + svc = MessagesCleanService.from_time_range( + policy=BillingDisabledPolicy(), + start_from=pagination_window_start, + end_before=pagination_window_end, + batch_size=_PAGINATION_BATCH_SIZE, + ) + stats = svc.run() + + assert stats["total_deleted"] == _PAGINATION_MESSAGE_COUNT + expected_batches = math.ceil(_PAGINATION_MESSAGE_COUNT / _PAGINATION_BATCH_SIZE) + assert stats["batches"] >= expected_batches + + with session_factory.create_session() as session: + remaining = session.query(Message).where(Message.id.in_(msg_ids)).count() + assert remaining == 0 + + def test_no_messages_in_range_returns_empty_stats(self, seed_messages): + """A window entirely in the future must yield zero matches.""" + far_future = _NOW + datetime.timedelta(days=365) + even_further = far_future + datetime.timedelta(days=1) + + svc = MessagesCleanService.from_time_range( + policy=BillingDisabledPolicy(), + start_from=far_future, + end_before=even_further, + batch_size=_DEFAULT_BATCH_SIZE, + ) + stats = svc.run() + + assert stats["total_messages"] == 0 + assert stats["total_deleted"] == 0 + + def test_relation_cascade_deletes(self, cascade_test_data): + """Deleting a Message must cascade to its Feedback and Annotation rows.""" + data = cascade_test_data + msg_id = data["msg_id"] + fb_id = data["fb_id"] + ann_id = data["ann_id"] + + svc = MessagesCleanService.from_time_range( + policy=BillingDisabledPolicy(), + start_from=_OLD - datetime.timedelta(hours=1), + end_before=_OLD + datetime.timedelta(hours=1), + batch_size=_DEFAULT_BATCH_SIZE, + ) + stats = svc.run() + + assert stats["total_deleted"] == 1 + + with session_factory.create_session() as session: + assert session.query(Message).where(Message.id == msg_id).count() == 0 + assert session.query(MessageFeedback).where(MessageFeedback.id == fb_id).count() == 0 + assert session.query(MessageAnnotation).where(MessageAnnotation.id == ann_id).count() == 0 + + def test_factory_from_time_range_validation(self): + with pytest.raises(ValueError, match="start_from"): + MessagesCleanService.from_time_range( + policy=BillingDisabledPolicy(), + start_from=_NOW, + end_before=_OLD, + ) + + def test_factory_from_days_validation(self): + with pytest.raises(ValueError, match="days"): + MessagesCleanService.from_days( + policy=BillingDisabledPolicy(), + days=-1, + ) + + def test_factory_batch_size_validation(self): + with pytest.raises(ValueError, match="batch_size"): + MessagesCleanService.from_time_range( + policy=BillingDisabledPolicy(), + start_from=_OLD, + end_before=_NOW, + batch_size=0, + ) diff --git a/api/tests/integration_tests/services/retention/test_workflow_run_archiver.py b/api/tests/integration_tests/services/retention/test_workflow_run_archiver.py new file mode 100644 index 0000000000..5728eacdfb --- /dev/null +++ b/api/tests/integration_tests/services/retention/test_workflow_run_archiver.py @@ -0,0 +1,177 @@ +import datetime +import io +import json +import uuid +import zipfile +from unittest.mock import MagicMock, patch + +import pytest + +from services.retention.workflow_run.archive_paid_plan_workflow_run import ( + ArchiveSummary, + WorkflowRunArchiver, +) +from services.retention.workflow_run.constants import ARCHIVE_SCHEMA_VERSION + + +class TestWorkflowRunArchiverInit: + def test_start_from_without_end_before_raises(self): + with pytest.raises(ValueError, match="start_from and end_before must be provided together"): + WorkflowRunArchiver(start_from=datetime.datetime(2025, 1, 1)) + + def test_end_before_without_start_from_raises(self): + with pytest.raises(ValueError, match="start_from and end_before must be provided together"): + WorkflowRunArchiver(end_before=datetime.datetime(2025, 1, 1)) + + def test_start_equals_end_raises(self): + ts = datetime.datetime(2025, 1, 1) + with pytest.raises(ValueError, match="start_from must be earlier than end_before"): + WorkflowRunArchiver(start_from=ts, end_before=ts) + + def test_start_after_end_raises(self): + with pytest.raises(ValueError, match="start_from must be earlier than end_before"): + WorkflowRunArchiver( + start_from=datetime.datetime(2025, 6, 1), + end_before=datetime.datetime(2025, 1, 1), + ) + + def test_workers_zero_raises(self): + with pytest.raises(ValueError, match="workers must be at least 1"): + WorkflowRunArchiver(workers=0) + + def test_valid_init_defaults(self): + archiver = WorkflowRunArchiver(days=30, batch_size=50) + assert archiver.days == 30 + assert archiver.batch_size == 50 + assert archiver.dry_run is False + assert archiver.delete_after_archive is False + assert archiver.start_from is None + + def test_valid_init_with_time_range(self): + start = datetime.datetime(2025, 1, 1) + end = datetime.datetime(2025, 6, 1) + archiver = WorkflowRunArchiver(start_from=start, end_before=end, workers=2) + assert archiver.start_from is not None + assert archiver.end_before is not None + assert archiver.workers == 2 + + +class TestBuildArchiveBundle: + def test_bundle_contains_manifest_and_all_tables(self): + archiver = WorkflowRunArchiver(days=90) + + manifest_data = json.dumps({"schema_version": ARCHIVE_SCHEMA_VERSION}).encode("utf-8") + table_payloads = dict.fromkeys(archiver.ARCHIVED_TABLES, b"") + + bundle_bytes = archiver._build_archive_bundle(manifest_data, table_payloads) + + with zipfile.ZipFile(io.BytesIO(bundle_bytes), "r") as zf: + names = set(zf.namelist()) + assert "manifest.json" in names + for table in archiver.ARCHIVED_TABLES: + assert f"{table}.jsonl" in names, f"Missing {table}.jsonl in bundle" + + def test_bundle_missing_table_payload_raises(self): + archiver = WorkflowRunArchiver(days=90) + manifest_data = b"{}" + incomplete_payloads = {archiver.ARCHIVED_TABLES[0]: b"data"} + + with pytest.raises(ValueError, match="Missing archive payload"): + archiver._build_archive_bundle(manifest_data, incomplete_payloads) + + +class TestGenerateManifest: + def test_manifest_structure(self): + archiver = WorkflowRunArchiver(days=90) + from services.retention.workflow_run.archive_paid_plan_workflow_run import TableStats + + run = MagicMock() + run.id = str(uuid.uuid4()) + run.tenant_id = str(uuid.uuid4()) + run.app_id = str(uuid.uuid4()) + run.workflow_id = str(uuid.uuid4()) + run.created_at = datetime.datetime(2025, 3, 15, 10, 0, 0) + + stats = [ + TableStats(table_name="workflow_runs", row_count=1, checksum="abc123", size_bytes=512), + TableStats(table_name="workflow_app_logs", row_count=2, checksum="def456", size_bytes=1024), + ] + + manifest = archiver._generate_manifest(run, stats) + + assert manifest["schema_version"] == ARCHIVE_SCHEMA_VERSION + assert manifest["workflow_run_id"] == run.id + assert manifest["tenant_id"] == run.tenant_id + assert manifest["app_id"] == run.app_id + assert "tables" in manifest + assert manifest["tables"]["workflow_runs"]["row_count"] == 1 + assert manifest["tables"]["workflow_runs"]["checksum"] == "abc123" + assert manifest["tables"]["workflow_app_logs"]["row_count"] == 2 + + +class TestFilterPaidTenants: + def test_all_tenants_paid_when_billing_disabled(self): + archiver = WorkflowRunArchiver(days=90) + tenant_ids = {"t1", "t2", "t3"} + + with patch("services.retention.workflow_run.archive_paid_plan_workflow_run.dify_config") as cfg: + cfg.BILLING_ENABLED = False + result = archiver._filter_paid_tenants(tenant_ids) + + assert result == tenant_ids + + def test_empty_tenants_returns_empty(self): + archiver = WorkflowRunArchiver(days=90) + + with patch("services.retention.workflow_run.archive_paid_plan_workflow_run.dify_config") as cfg: + cfg.BILLING_ENABLED = True + result = archiver._filter_paid_tenants(set()) + + assert result == set() + + def test_only_paid_plans_returned(self): + archiver = WorkflowRunArchiver(days=90) + + mock_bulk = { + "t1": {"plan": "professional"}, + "t2": {"plan": "sandbox"}, + "t3": {"plan": "team"}, + } + + with ( + patch("services.retention.workflow_run.archive_paid_plan_workflow_run.dify_config") as cfg, + patch("services.retention.workflow_run.archive_paid_plan_workflow_run.BillingService") as billing, + ): + cfg.BILLING_ENABLED = True + billing.get_plan_bulk_with_cache.return_value = mock_bulk + result = archiver._filter_paid_tenants({"t1", "t2", "t3"}) + + assert "t1" in result + assert "t3" in result + assert "t2" not in result + + def test_billing_api_failure_returns_empty(self): + archiver = WorkflowRunArchiver(days=90) + + with ( + patch("services.retention.workflow_run.archive_paid_plan_workflow_run.dify_config") as cfg, + patch("services.retention.workflow_run.archive_paid_plan_workflow_run.BillingService") as billing, + ): + cfg.BILLING_ENABLED = True + billing.get_plan_bulk_with_cache.side_effect = RuntimeError("API down") + result = archiver._filter_paid_tenants({"t1"}) + + assert result == set() + + +class TestDryRunArchive: + @patch("services.retention.workflow_run.archive_paid_plan_workflow_run.get_archive_storage") + def test_dry_run_does_not_call_storage(self, mock_get_storage, flask_req_ctx): + archiver = WorkflowRunArchiver(days=90, dry_run=True) + + with patch.object(archiver, "_get_runs_batch", return_value=[]): + summary = archiver.run() + + mock_get_storage.assert_not_called() + assert isinstance(summary, ArchiveSummary) + assert summary.runs_failed == 0 diff --git a/api/tests/unit_tests/services/enterprise/test_enterprise_service.py b/api/tests/unit_tests/services/enterprise/test_enterprise_service.py index 59c07bfb37..6ad6a490b0 100644 --- a/api/tests/unit_tests/services/enterprise/test_enterprise_service.py +++ b/api/tests/unit_tests/services/enterprise/test_enterprise_service.py @@ -5,6 +5,7 @@ Covers: - License status caching (get_cached_license_status) """ +from datetime import datetime from unittest.mock import patch import pytest @@ -15,9 +16,178 @@ from services.enterprise.enterprise_service import ( VALID_LICENSE_CACHE_TTL, DefaultWorkspaceJoinResult, EnterpriseService, + WebAppSettings, + WorkspacePermission, try_join_default_workspace, ) +MODULE = "services.enterprise.enterprise_service" + + +class TestEnterpriseServiceInfo: + def test_get_info_delegates(self): + with patch(f"{MODULE}.EnterpriseRequest") as req: + req.send_request.return_value = {"version": "1.0"} + result = EnterpriseService.get_info() + + req.send_request.assert_called_once_with("GET", "/info") + assert result == {"version": "1.0"} + + def test_get_workspace_info_delegates(self): + with patch(f"{MODULE}.EnterpriseRequest") as req: + req.send_request.return_value = {"name": "ws"} + result = EnterpriseService.get_workspace_info("tenant-1") + + req.send_request.assert_called_once_with("GET", "/workspace/tenant-1/info") + assert result == {"name": "ws"} + + +class TestSsoSettingsLastUpdateTime: + def test_app_sso_parses_valid_timestamp(self): + with patch(f"{MODULE}.EnterpriseRequest") as req: + req.send_request.return_value = "2025-01-15T10:30:00+00:00" + result = EnterpriseService.get_app_sso_settings_last_update_time() + + assert isinstance(result, datetime) + assert result.year == 2025 + + def test_app_sso_raises_on_empty(self): + with patch(f"{MODULE}.EnterpriseRequest") as req: + req.send_request.return_value = "" + with pytest.raises(ValueError, match="No data found"): + EnterpriseService.get_app_sso_settings_last_update_time() + + def test_app_sso_raises_on_invalid_format(self): + with patch(f"{MODULE}.EnterpriseRequest") as req: + req.send_request.return_value = "not-a-date" + with pytest.raises(ValueError, match="Invalid date format"): + EnterpriseService.get_app_sso_settings_last_update_time() + + def test_workspace_sso_parses_valid_timestamp(self): + with patch(f"{MODULE}.EnterpriseRequest") as req: + req.send_request.return_value = "2025-06-01T00:00:00+00:00" + result = EnterpriseService.get_workspace_sso_settings_last_update_time() + + assert isinstance(result, datetime) + + def test_workspace_sso_raises_on_empty(self): + with patch(f"{MODULE}.EnterpriseRequest") as req: + req.send_request.return_value = None + with pytest.raises(ValueError, match="No data found"): + EnterpriseService.get_workspace_sso_settings_last_update_time() + + +class TestWorkspacePermissionService: + def test_raises_on_empty_workspace_id(self): + with pytest.raises(ValueError, match="workspace_id must be provided"): + EnterpriseService.WorkspacePermissionService.get_permission("") + + def test_raises_on_missing_data(self): + with patch(f"{MODULE}.EnterpriseRequest") as req: + req.send_request.return_value = None + with pytest.raises(ValueError, match="No data found"): + EnterpriseService.WorkspacePermissionService.get_permission("ws-1") + + def test_raises_on_missing_permission_key(self): + with patch(f"{MODULE}.EnterpriseRequest") as req: + req.send_request.return_value = {"other": "data"} + with pytest.raises(ValueError, match="No data found"): + EnterpriseService.WorkspacePermissionService.get_permission("ws-1") + + def test_returns_parsed_permission(self): + with patch(f"{MODULE}.EnterpriseRequest") as req: + req.send_request.return_value = { + "permission": { + "workspaceId": "ws-1", + "allowMemberInvite": True, + "allowOwnerTransfer": False, + } + } + result = EnterpriseService.WorkspacePermissionService.get_permission("ws-1") + + assert isinstance(result, WorkspacePermission) + assert result.workspace_id == "ws-1" + assert result.allow_member_invite is True + assert result.allow_owner_transfer is False + + +class TestWebAppAuth: + def test_is_user_allowed_returns_result_field(self): + with patch(f"{MODULE}.EnterpriseRequest") as req: + req.send_request.return_value = {"result": True} + assert EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp("u1", "a1") is True + + def test_is_user_allowed_defaults_false(self): + with patch(f"{MODULE}.EnterpriseRequest") as req: + req.send_request.return_value = {} + assert EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp("u1", "a1") is False + + def test_batch_is_user_allowed_returns_empty_for_no_apps(self): + assert EnterpriseService.WebAppAuth.batch_is_user_allowed_to_access_webapps("u1", []) == {} + + def test_batch_is_user_allowed_raises_on_empty_response(self): + with patch(f"{MODULE}.EnterpriseRequest") as req: + req.send_request.return_value = None + with pytest.raises(ValueError, match="No data found"): + EnterpriseService.WebAppAuth.batch_is_user_allowed_to_access_webapps("u1", ["a1"]) + + def test_get_app_access_mode_raises_on_empty_app_id(self): + with pytest.raises(ValueError, match="app_id must be provided"): + EnterpriseService.WebAppAuth.get_app_access_mode_by_id("") + + def test_get_app_access_mode_returns_settings(self): + with patch(f"{MODULE}.EnterpriseRequest") as req: + req.send_request.return_value = {"accessMode": "public"} + result = EnterpriseService.WebAppAuth.get_app_access_mode_by_id("a1") + + assert isinstance(result, WebAppSettings) + assert result.access_mode == "public" + + def test_batch_get_returns_empty_for_no_apps(self): + assert EnterpriseService.WebAppAuth.batch_get_app_access_mode_by_id([]) == {} + + def test_batch_get_maps_access_modes(self): + with patch(f"{MODULE}.EnterpriseRequest") as req: + req.send_request.return_value = {"accessModes": {"a1": "public", "a2": "private"}} + result = EnterpriseService.WebAppAuth.batch_get_app_access_mode_by_id(["a1", "a2"]) + + assert result["a1"].access_mode == "public" + assert result["a2"].access_mode == "private" + + def test_batch_get_raises_on_invalid_format(self): + with patch(f"{MODULE}.EnterpriseRequest") as req: + req.send_request.return_value = {"accessModes": "not-a-dict"} + with pytest.raises(ValueError, match="Invalid data format"): + EnterpriseService.WebAppAuth.batch_get_app_access_mode_by_id(["a1"]) + + def test_update_access_mode_raises_on_empty_app_id(self): + with pytest.raises(ValueError, match="app_id must be provided"): + EnterpriseService.WebAppAuth.update_app_access_mode("", "public") + + def test_update_access_mode_raises_on_invalid_mode(self): + with pytest.raises(ValueError, match="access_mode must be"): + EnterpriseService.WebAppAuth.update_app_access_mode("a1", "invalid") + + def test_update_access_mode_delegates_and_returns(self): + with patch(f"{MODULE}.EnterpriseRequest") as req: + req.send_request.return_value = {"result": True} + result = EnterpriseService.WebAppAuth.update_app_access_mode("a1", "public") + + assert result is True + req.send_request.assert_called_once_with( + "POST", "/webapp/access-mode", json={"appId": "a1", "accessMode": "public"} + ) + + def test_cleanup_webapp_raises_on_empty_app_id(self): + with pytest.raises(ValueError, match="app_id must be provided"): + EnterpriseService.WebAppAuth.cleanup_webapp("") + + def test_cleanup_webapp_delegates(self): + with patch(f"{MODULE}.EnterpriseRequest") as req: + EnterpriseService.WebAppAuth.cleanup_webapp("a1") + + req.send_request.assert_called_once_with("DELETE", "/webapp/clean", params={"appId": "a1"}) + class TestJoinDefaultWorkspace: def test_join_default_workspace_success(self): diff --git a/api/tests/unit_tests/services/enterprise/test_plugin_manager_service.py b/api/tests/unit_tests/services/enterprise/test_plugin_manager_service.py index 6ee328ae2c..759d907934 100644 --- a/api/tests/unit_tests/services/enterprise/test_plugin_manager_service.py +++ b/api/tests/unit_tests/services/enterprise/test_plugin_manager_service.py @@ -7,14 +7,20 @@ This module covers the pre-uninstall plugin hook behavior: from unittest.mock import patch +import pytest from httpx import HTTPStatusError from configs import dify_config from services.enterprise.plugin_manager_service import ( + CheckCredentialPolicyComplianceRequest, + CredentialPolicyViolationError, + PluginCredentialType, PluginManagerService, PreUninstallPluginRequest, ) +MODULE = "services.enterprise.plugin_manager_service" + class TestTryPreUninstallPlugin: def test_try_pre_uninstall_plugin_success(self): @@ -88,3 +94,46 @@ class TestTryPreUninstallPlugin: timeout=dify_config.ENTERPRISE_REQUEST_TIMEOUT, ) mock_logger.exception.assert_called_once() + + +class TestCheckCredentialPolicyCompliance: + def _request(self, cred_type=PluginCredentialType.MODEL): + return CheckCredentialPolicyComplianceRequest( + dify_credential_id="cred-1", provider="openai", credential_type=cred_type + ) + + def test_passes_when_result_true(self): + with patch(f"{MODULE}.EnterprisePluginManagerRequest") as req: + req.send_request.return_value = {"result": True} + PluginManagerService.check_credential_policy_compliance(self._request()) + + req.send_request.assert_called_once() + + def test_raises_violation_when_result_false(self): + with patch(f"{MODULE}.EnterprisePluginManagerRequest") as req: + req.send_request.return_value = {"result": False} + with pytest.raises(CredentialPolicyViolationError, match="Credentials not available"): + PluginManagerService.check_credential_policy_compliance(self._request()) + + def test_raises_violation_on_invalid_response_format(self): + with patch(f"{MODULE}.EnterprisePluginManagerRequest") as req: + req.send_request.return_value = "not-a-dict" + with pytest.raises(CredentialPolicyViolationError, match="error occurred"): + PluginManagerService.check_credential_policy_compliance(self._request()) + + def test_raises_violation_on_api_exception(self): + with patch(f"{MODULE}.EnterprisePluginManagerRequest") as req: + req.send_request.side_effect = ConnectionError("network fail") + with pytest.raises(CredentialPolicyViolationError, match="error occurred"): + PluginManagerService.check_credential_policy_compliance(self._request()) + + def test_model_dump_serializes_credential_type_as_number(self): + body = self._request(PluginCredentialType.TOOL) + data = body.model_dump() + + assert data["credential_type"] == 1 + assert data["dify_credential_id"] == "cred-1" + + def test_model_credential_type_values(self): + assert PluginCredentialType.MODEL.to_number() == 0 + assert PluginCredentialType.TOOL.to_number() == 1 diff --git a/api/tests/unit_tests/services/plugin/test_plugin_auto_upgrade_service.py b/api/tests/unit_tests/services/plugin/test_plugin_auto_upgrade_service.py new file mode 100644 index 0000000000..edb50d09a6 --- /dev/null +++ b/api/tests/unit_tests/services/plugin/test_plugin_auto_upgrade_service.py @@ -0,0 +1,183 @@ +from unittest.mock import MagicMock, patch + +from models.account import TenantPluginAutoUpgradeStrategy + +MODULE = "services.plugin.plugin_auto_upgrade_service" + + +def _patched_session(): + """Patch Session(db.engine) to return a mock session as context manager.""" + session = MagicMock() + session_cls = MagicMock() + session_cls.return_value.__enter__ = MagicMock(return_value=session) + session_cls.return_value.__exit__ = MagicMock(return_value=False) + patcher = patch(f"{MODULE}.Session", session_cls) + db_patcher = patch(f"{MODULE}.db") + return patcher, db_patcher, session + + +class TestGetStrategy: + def test_returns_strategy_when_found(self): + p1, p2, session = _patched_session() + strategy = MagicMock() + session.query.return_value.where.return_value.first.return_value = strategy + + with p1, p2: + from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService + + result = PluginAutoUpgradeService.get_strategy("t1") + + assert result is strategy + + def test_returns_none_when_not_found(self): + p1, p2, session = _patched_session() + session.query.return_value.where.return_value.first.return_value = None + + with p1, p2: + from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService + + result = PluginAutoUpgradeService.get_strategy("t1") + + assert result is None + + +class TestChangeStrategy: + def test_creates_new_strategy(self): + p1, p2, session = _patched_session() + session.query.return_value.where.return_value.first.return_value = None + + with p1, p2, patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls: + strat_cls.return_value = MagicMock() + from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService + + result = PluginAutoUpgradeService.change_strategy( + "t1", + TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY, + 3, + TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL, + [], + [], + ) + + assert result is True + session.add.assert_called_once() + session.commit.assert_called_once() + + def test_updates_existing_strategy(self): + p1, p2, session = _patched_session() + existing = MagicMock() + session.query.return_value.where.return_value.first.return_value = existing + + with p1, p2: + from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService + + result = PluginAutoUpgradeService.change_strategy( + "t1", + TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST, + 5, + TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL, + ["p1"], + ["p2"], + ) + + assert result is True + assert existing.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST + assert existing.upgrade_time_of_day == 5 + assert existing.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL + assert existing.exclude_plugins == ["p1"] + assert existing.include_plugins == ["p2"] + session.commit.assert_called_once() + + +class TestExcludePlugin: + def test_creates_default_strategy_when_none_exists(self): + p1, p2, session = _patched_session() + session.query.return_value.where.return_value.first.return_value = None + + with ( + p1, + p2, + patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls, + patch(f"{MODULE}.PluginAutoUpgradeService.change_strategy") as cs, + ): + strat_cls.StrategySetting.FIX_ONLY = "fix_only" + strat_cls.UpgradeMode.EXCLUDE = "exclude" + cs.return_value = True + from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService + + result = PluginAutoUpgradeService.exclude_plugin("t1", "plugin-1") + + assert result is True + cs.assert_called_once() + + def test_appends_to_exclude_list_in_exclude_mode(self): + p1, p2, session = _patched_session() + existing = MagicMock() + existing.upgrade_mode = "exclude" + existing.exclude_plugins = ["p-existing"] + session.query.return_value.where.return_value.first.return_value = existing + + with p1, p2, patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls: + strat_cls.UpgradeMode.EXCLUDE = "exclude" + strat_cls.UpgradeMode.PARTIAL = "partial" + strat_cls.UpgradeMode.ALL = "all" + from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService + + result = PluginAutoUpgradeService.exclude_plugin("t1", "p-new") + + assert result is True + assert existing.exclude_plugins == ["p-existing", "p-new"] + session.commit.assert_called_once() + + def test_removes_from_include_list_in_partial_mode(self): + p1, p2, session = _patched_session() + existing = MagicMock() + existing.upgrade_mode = "partial" + existing.include_plugins = ["p1", "p2"] + session.query.return_value.where.return_value.first.return_value = existing + + with p1, p2, patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls: + strat_cls.UpgradeMode.EXCLUDE = "exclude" + strat_cls.UpgradeMode.PARTIAL = "partial" + strat_cls.UpgradeMode.ALL = "all" + from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService + + result = PluginAutoUpgradeService.exclude_plugin("t1", "p1") + + assert result is True + assert existing.include_plugins == ["p2"] + + def test_switches_to_exclude_mode_from_all(self): + p1, p2, session = _patched_session() + existing = MagicMock() + existing.upgrade_mode = "all" + session.query.return_value.where.return_value.first.return_value = existing + + with p1, p2, patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls: + strat_cls.UpgradeMode.EXCLUDE = "exclude" + strat_cls.UpgradeMode.PARTIAL = "partial" + strat_cls.UpgradeMode.ALL = "all" + from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService + + result = PluginAutoUpgradeService.exclude_plugin("t1", "p1") + + assert result is True + assert existing.upgrade_mode == "exclude" + assert existing.exclude_plugins == ["p1"] + + def test_no_duplicate_in_exclude_list(self): + p1, p2, session = _patched_session() + existing = MagicMock() + existing.upgrade_mode = "exclude" + existing.exclude_plugins = ["p1"] + session.query.return_value.where.return_value.first.return_value = existing + + with p1, p2, patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls: + strat_cls.UpgradeMode.EXCLUDE = "exclude" + strat_cls.UpgradeMode.PARTIAL = "partial" + strat_cls.UpgradeMode.ALL = "all" + from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService + + PluginAutoUpgradeService.exclude_plugin("t1", "p1") + + assert existing.exclude_plugins == ["p1"] diff --git a/api/tests/unit_tests/services/plugin/test_plugin_permission_service.py b/api/tests/unit_tests/services/plugin/test_plugin_permission_service.py new file mode 100644 index 0000000000..69091110db --- /dev/null +++ b/api/tests/unit_tests/services/plugin/test_plugin_permission_service.py @@ -0,0 +1,75 @@ +from unittest.mock import MagicMock, patch + +from models.account import TenantPluginPermission + +MODULE = "services.plugin.plugin_permission_service" + + +def _patched_session(): + """Patch Session(db.engine) to return a mock session as context manager.""" + session = MagicMock() + session_cls = MagicMock() + session_cls.return_value.__enter__ = MagicMock(return_value=session) + session_cls.return_value.__exit__ = MagicMock(return_value=False) + patcher = patch(f"{MODULE}.Session", session_cls) + db_patcher = patch(f"{MODULE}.db") + return patcher, db_patcher, session + + +class TestGetPermission: + def test_returns_permission_when_found(self): + p1, p2, session = _patched_session() + permission = MagicMock() + session.query.return_value.where.return_value.first.return_value = permission + + with p1, p2: + from services.plugin.plugin_permission_service import PluginPermissionService + + result = PluginPermissionService.get_permission("t1") + + assert result is permission + + def test_returns_none_when_not_found(self): + p1, p2, session = _patched_session() + session.query.return_value.where.return_value.first.return_value = None + + with p1, p2: + from services.plugin.plugin_permission_service import PluginPermissionService + + result = PluginPermissionService.get_permission("t1") + + assert result is None + + +class TestChangePermission: + def test_creates_new_permission_when_not_exists(self): + p1, p2, session = _patched_session() + session.query.return_value.where.return_value.first.return_value = None + + with p1, p2, patch(f"{MODULE}.TenantPluginPermission") as perm_cls: + perm_cls.return_value = MagicMock() + from services.plugin.plugin_permission_service import PluginPermissionService + + result = PluginPermissionService.change_permission( + "t1", TenantPluginPermission.InstallPermission.EVERYONE, TenantPluginPermission.DebugPermission.EVERYONE + ) + + session.add.assert_called_once() + session.commit.assert_called_once() + + def test_updates_existing_permission(self): + p1, p2, session = _patched_session() + existing = MagicMock() + session.query.return_value.where.return_value.first.return_value = existing + + with p1, p2: + from services.plugin.plugin_permission_service import PluginPermissionService + + result = PluginPermissionService.change_permission( + "t1", TenantPluginPermission.InstallPermission.ADMINS, TenantPluginPermission.DebugPermission.ADMINS + ) + + assert existing.install_permission == TenantPluginPermission.InstallPermission.ADMINS + assert existing.debug_permission == TenantPluginPermission.DebugPermission.ADMINS + session.commit.assert_called_once() + session.add.assert_not_called() diff --git a/api/tests/unit_tests/services/retention/__init__.py b/api/tests/unit_tests/services/retention/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/unit_tests/services/retention/test_messages_clean_policy.py b/api/tests/unit_tests/services/retention/test_messages_clean_policy.py new file mode 100644 index 0000000000..79c079c683 --- /dev/null +++ b/api/tests/unit_tests/services/retention/test_messages_clean_policy.py @@ -0,0 +1,135 @@ +import datetime +from unittest.mock import MagicMock, patch + +from services.retention.conversation.messages_clean_policy import ( + BillingDisabledPolicy, + BillingSandboxPolicy, + SimpleMessage, + create_message_clean_policy, +) + +MODULE = "services.retention.conversation.messages_clean_policy" + + +def _msg(msg_id: str, app_id: str, days_ago: int = 0) -> SimpleMessage: + return SimpleMessage( + id=msg_id, + app_id=app_id, + created_at=datetime.datetime.now(datetime.UTC) - datetime.timedelta(days=days_ago), + ) + + +class TestBillingDisabledPolicy: + def test_returns_all_message_ids(self): + policy = BillingDisabledPolicy() + msgs = [_msg("m1", "app1"), _msg("m2", "app2"), _msg("m3", "app1")] + + result = policy.filter_message_ids(msgs, {"app1": "t1", "app2": "t2"}) + + assert set(result) == {"m1", "m2", "m3"} + + def test_empty_messages_returns_empty(self): + assert BillingDisabledPolicy().filter_message_ids([], {}) == [] + + +class TestBillingSandboxPolicy: + def _policy(self, plans, *, graceful_days=21, whitelist=None, now=1_000_000_000): + return BillingSandboxPolicy( + plan_provider=lambda _ids: plans, + graceful_period_days=graceful_days, + tenant_whitelist=whitelist, + current_timestamp=now, + ) + + def test_empty_messages_returns_empty(self): + policy = self._policy({}) + assert policy.filter_message_ids([], {"app1": "t1"}) == [] + + def test_empty_app_to_tenant_returns_empty(self): + policy = self._policy({}) + assert policy.filter_message_ids([_msg("m1", "app1")], {}) == [] + + def test_empty_plans_returns_empty(self): + policy = self._policy({}) + msgs = [_msg("m1", "app1")] + assert policy.filter_message_ids(msgs, {"app1": "t1"}) == [] + + def test_non_sandbox_tenant_skipped(self): + plans = {"t1": {"plan": "professional", "expiration_date": 0}} + policy = self._policy(plans) + msgs = [_msg("m1", "app1")] + + assert policy.filter_message_ids(msgs, {"app1": "t1"}) == [] + + def test_sandbox_no_previous_subscription_deletes(self): + plans = {"t1": {"plan": "sandbox", "expiration_date": -1}} + policy = self._policy(plans) + msgs = [_msg("m1", "app1")] + + assert policy.filter_message_ids(msgs, {"app1": "t1"}) == ["m1"] + + def test_sandbox_expired_beyond_grace_period_deletes(self): + now = 1_000_000_000 + expired_long_ago = now - (22 * 24 * 60 * 60) # 22 days ago > 21 day grace + plans = {"t1": {"plan": "sandbox", "expiration_date": expired_long_ago}} + policy = self._policy(plans, now=now) + msgs = [_msg("m1", "app1")] + + assert policy.filter_message_ids(msgs, {"app1": "t1"}) == ["m1"] + + def test_sandbox_within_grace_period_kept(self): + now = 1_000_000_000 + expired_recently = now - (10 * 24 * 60 * 60) # 10 days ago < 21 day grace + plans = {"t1": {"plan": "sandbox", "expiration_date": expired_recently}} + policy = self._policy(plans, now=now) + msgs = [_msg("m1", "app1")] + + assert policy.filter_message_ids(msgs, {"app1": "t1"}) == [] + + def test_whitelisted_tenant_skipped(self): + plans = {"t1": {"plan": "sandbox", "expiration_date": -1}} + policy = self._policy(plans, whitelist=["t1"]) + msgs = [_msg("m1", "app1")] + + assert policy.filter_message_ids(msgs, {"app1": "t1"}) == [] + + def test_message_without_tenant_mapping_skipped(self): + plans = {"t1": {"plan": "sandbox", "expiration_date": -1}} + policy = self._policy(plans) + msgs = [_msg("m1", "unmapped_app")] + + assert policy.filter_message_ids(msgs, {"app1": "t1"}) == [] + + def test_mixed_tenants_only_sandbox_deleted(self): + plans = { + "t_sandbox": {"plan": "sandbox", "expiration_date": -1}, + "t_pro": {"plan": "professional", "expiration_date": 0}, + } + policy = self._policy(plans) + msgs = [_msg("m1", "app_sandbox"), _msg("m2", "app_pro")] + app_map = {"app_sandbox": "t_sandbox", "app_pro": "t_pro"} + + result = policy.filter_message_ids(msgs, app_map) + + assert result == ["m1"] + + +class TestCreateMessageCleanPolicy: + def test_billing_disabled_returns_disabled_policy(self): + with patch(f"{MODULE}.dify_config") as cfg: + cfg.BILLING_ENABLED = False + policy = create_message_clean_policy() + + assert isinstance(policy, BillingDisabledPolicy) + + def test_billing_enabled_returns_sandbox_policy(self): + with ( + patch(f"{MODULE}.dify_config") as cfg, + patch(f"{MODULE}.BillingService") as bs, + ): + cfg.BILLING_ENABLED = True + bs.get_expired_subscription_cleanup_whitelist.return_value = ["wl1"] + bs.get_plan_bulk_with_cache = MagicMock() + policy = create_message_clean_policy(graceful_period_days=30) + + assert isinstance(policy, BillingSandboxPolicy) diff --git a/api/tests/unit_tests/services/tools/test_tools_transform_service.py b/api/tests/unit_tests/services/tools/test_tools_transform_service.py new file mode 100644 index 0000000000..32c1a00d30 --- /dev/null +++ b/api/tests/unit_tests/services/tools/test_tools_transform_service.py @@ -0,0 +1,598 @@ +from unittest.mock import MagicMock, Mock, patch + +from core.tools.__base.tool import Tool +from core.tools.entities.api_entities import ToolApiEntity, ToolProviderApiEntity +from core.tools.entities.common_entities import I18nObject +from core.tools.entities.tool_entities import ApiProviderAuthType, ToolParameter, ToolProviderType +from services.tools.tools_transform_service import ToolTransformService + +MODULE = "services.tools.tools_transform_service" + + +class TestToolTransformService: + """Test cases for ToolTransformService.convert_tool_entity_to_api_entity method""" + + def test_convert_tool_with_parameter_override(self): + """Test that runtime parameters correctly override base parameters""" + # Create mock base parameters + base_param1 = Mock(spec=ToolParameter) + base_param1.name = "param1" + base_param1.form = ToolParameter.ToolParameterForm.FORM + base_param1.type = "string" + base_param1.label = "Base Param 1" + + base_param2 = Mock(spec=ToolParameter) + base_param2.name = "param2" + base_param2.form = ToolParameter.ToolParameterForm.FORM + base_param2.type = "string" + base_param2.label = "Base Param 2" + + # Create mock runtime parameters that override base parameters + runtime_param1 = Mock(spec=ToolParameter) + runtime_param1.name = "param1" + runtime_param1.form = ToolParameter.ToolParameterForm.FORM + runtime_param1.type = "string" + runtime_param1.label = "Runtime Param 1" # Different label to verify override + + # Create mock tool + mock_tool = Mock(spec=Tool) + mock_tool.entity = Mock() + mock_tool.entity.parameters = [base_param1, base_param2] + mock_tool.entity.identity = Mock() + mock_tool.entity.identity.author = "test_author" + mock_tool.entity.identity.name = "test_tool" + mock_tool.entity.identity.label = I18nObject(en_US="Test Tool") + mock_tool.entity.description = Mock() + mock_tool.entity.description.human = I18nObject(en_US="Test description") + mock_tool.entity.output_schema = {} + mock_tool.get_runtime_parameters.return_value = [runtime_param1] + + # Mock fork_tool_runtime to return the same tool + mock_tool.fork_tool_runtime.return_value = mock_tool + + # Call the method + result = ToolTransformService.convert_tool_entity_to_api_entity(mock_tool, "test_tenant", None) + + # Verify the result + assert isinstance(result, ToolApiEntity) + assert result.author == "test_author" + assert result.name == "test_tool" + assert result.parameters is not None + assert len(result.parameters) == 2 + + # Find the overridden parameter + overridden_param = next((p for p in result.parameters if p.name == "param1"), None) + assert overridden_param is not None + assert overridden_param.label == "Runtime Param 1" # Should be runtime version + + # Find the non-overridden parameter + original_param = next((p for p in result.parameters if p.name == "param2"), None) + assert original_param is not None + assert original_param.label == "Base Param 2" # Should be base version + + def test_convert_tool_with_additional_runtime_parameters(self): + """Test that additional runtime parameters are added to the final list""" + # Create mock base parameters + base_param1 = Mock(spec=ToolParameter) + base_param1.name = "param1" + base_param1.form = ToolParameter.ToolParameterForm.FORM + base_param1.type = "string" + base_param1.label = "Base Param 1" + + # Create mock runtime parameters - one that overrides and one that's new + runtime_param1 = Mock(spec=ToolParameter) + runtime_param1.name = "param1" + runtime_param1.form = ToolParameter.ToolParameterForm.FORM + runtime_param1.type = "string" + runtime_param1.label = "Runtime Param 1" + + runtime_param2 = Mock(spec=ToolParameter) + runtime_param2.name = "runtime_only" + runtime_param2.form = ToolParameter.ToolParameterForm.FORM + runtime_param2.type = "string" + runtime_param2.label = "Runtime Only Param" + + # Create mock tool + mock_tool = Mock(spec=Tool) + mock_tool.entity = Mock() + mock_tool.entity.parameters = [base_param1] + mock_tool.entity.identity = Mock() + mock_tool.entity.identity.author = "test_author" + mock_tool.entity.identity.name = "test_tool" + mock_tool.entity.identity.label = I18nObject(en_US="Test Tool") + mock_tool.entity.description = Mock() + mock_tool.entity.description.human = I18nObject(en_US="Test description") + mock_tool.entity.output_schema = {} + mock_tool.get_runtime_parameters.return_value = [runtime_param1, runtime_param2] + + # Mock fork_tool_runtime to return the same tool + mock_tool.fork_tool_runtime.return_value = mock_tool + + # Call the method + result = ToolTransformService.convert_tool_entity_to_api_entity(mock_tool, "test_tenant", None) + + # Verify the result + assert isinstance(result, ToolApiEntity) + assert result.parameters is not None + assert len(result.parameters) == 2 + + # Check that both parameters are present + param_names = [p.name for p in result.parameters] + assert "param1" in param_names + assert "runtime_only" in param_names + + # Verify the overridden parameter has runtime version + overridden_param = next((p for p in result.parameters if p.name == "param1"), None) + assert overridden_param is not None + assert overridden_param.label == "Runtime Param 1" + + # Verify the new runtime parameter is included + new_param = next((p for p in result.parameters if p.name == "runtime_only"), None) + assert new_param is not None + assert new_param.label == "Runtime Only Param" + + def test_convert_tool_with_non_form_runtime_parameters(self): + """Test that non-FORM runtime parameters are not added as new parameters""" + # Create mock base parameters + base_param1 = Mock(spec=ToolParameter) + base_param1.name = "param1" + base_param1.form = ToolParameter.ToolParameterForm.FORM + base_param1.type = "string" + base_param1.label = "Base Param 1" + + # Create mock runtime parameters with different forms + runtime_param1 = Mock(spec=ToolParameter) + runtime_param1.name = "param1" + runtime_param1.form = ToolParameter.ToolParameterForm.FORM + runtime_param1.type = "string" + runtime_param1.label = "Runtime Param 1" + + runtime_param2 = Mock(spec=ToolParameter) + runtime_param2.name = "llm_param" + runtime_param2.form = ToolParameter.ToolParameterForm.LLM + runtime_param2.type = "string" + runtime_param2.label = "LLM Param" + + # Create mock tool + mock_tool = Mock(spec=Tool) + mock_tool.entity = Mock() + mock_tool.entity.parameters = [base_param1] + mock_tool.entity.identity = Mock() + mock_tool.entity.identity.author = "test_author" + mock_tool.entity.identity.name = "test_tool" + mock_tool.entity.identity.label = I18nObject(en_US="Test Tool") + mock_tool.entity.description = Mock() + mock_tool.entity.description.human = I18nObject(en_US="Test description") + mock_tool.entity.output_schema = {} + mock_tool.get_runtime_parameters.return_value = [runtime_param1, runtime_param2] + + # Mock fork_tool_runtime to return the same tool + mock_tool.fork_tool_runtime.return_value = mock_tool + + # Call the method + result = ToolTransformService.convert_tool_entity_to_api_entity(mock_tool, "test_tenant", None) + + # Verify the result + assert isinstance(result, ToolApiEntity) + assert result.parameters is not None + assert len(result.parameters) == 1 # Only the FORM parameter should be present + + # Check that only the FORM parameter is present + param_names = [p.name for p in result.parameters] + assert "param1" in param_names + assert "llm_param" not in param_names + + def test_convert_tool_with_empty_parameters(self): + """Test conversion with empty base and runtime parameters""" + # Create mock tool with no parameters + mock_tool = Mock(spec=Tool) + mock_tool.entity = Mock() + mock_tool.entity.parameters = [] + mock_tool.entity.identity = Mock() + mock_tool.entity.identity.author = "test_author" + mock_tool.entity.identity.name = "test_tool" + mock_tool.entity.identity.label = I18nObject(en_US="Test Tool") + mock_tool.entity.description = Mock() + mock_tool.entity.description.human = I18nObject(en_US="Test description") + mock_tool.entity.output_schema = {} + mock_tool.get_runtime_parameters.return_value = [] + + # Mock fork_tool_runtime to return the same tool + mock_tool.fork_tool_runtime.return_value = mock_tool + + # Call the method + result = ToolTransformService.convert_tool_entity_to_api_entity(mock_tool, "test_tenant", None) + + # Verify the result + assert isinstance(result, ToolApiEntity) + assert result.parameters is not None + assert len(result.parameters) == 0 + + def test_convert_tool_with_none_parameters(self): + """Test conversion when base parameters is None""" + # Create mock tool with None parameters + mock_tool = Mock(spec=Tool) + mock_tool.entity = Mock() + mock_tool.entity.parameters = None + mock_tool.entity.identity = Mock() + mock_tool.entity.identity.author = "test_author" + mock_tool.entity.identity.name = "test_tool" + mock_tool.entity.identity.label = I18nObject(en_US="Test Tool") + mock_tool.entity.description = Mock() + mock_tool.entity.description.human = I18nObject(en_US="Test description") + mock_tool.entity.output_schema = {} + mock_tool.get_runtime_parameters.return_value = [] + + # Mock fork_tool_runtime to return the same tool + mock_tool.fork_tool_runtime.return_value = mock_tool + + # Call the method + result = ToolTransformService.convert_tool_entity_to_api_entity(mock_tool, "test_tenant", None) + + # Verify the result + assert isinstance(result, ToolApiEntity) + assert result.parameters is not None + assert len(result.parameters) == 0 + + def test_convert_tool_parameter_order_preserved(self): + """Test that parameter order is preserved correctly""" + # Create mock base parameters in specific order + base_param1 = Mock(spec=ToolParameter) + base_param1.name = "param1" + base_param1.form = ToolParameter.ToolParameterForm.FORM + base_param1.type = "string" + base_param1.label = "Base Param 1" + + base_param2 = Mock(spec=ToolParameter) + base_param2.name = "param2" + base_param2.form = ToolParameter.ToolParameterForm.FORM + base_param2.type = "string" + base_param2.label = "Base Param 2" + + base_param3 = Mock(spec=ToolParameter) + base_param3.name = "param3" + base_param3.form = ToolParameter.ToolParameterForm.FORM + base_param3.type = "string" + base_param3.label = "Base Param 3" + + # Create runtime parameter that overrides middle parameter + runtime_param2 = Mock(spec=ToolParameter) + runtime_param2.name = "param2" + runtime_param2.form = ToolParameter.ToolParameterForm.FORM + runtime_param2.type = "string" + runtime_param2.label = "Runtime Param 2" + + # Create new runtime parameter + runtime_param4 = Mock(spec=ToolParameter) + runtime_param4.name = "param4" + runtime_param4.form = ToolParameter.ToolParameterForm.FORM + runtime_param4.type = "string" + runtime_param4.label = "Runtime Param 4" + + # Create mock tool + mock_tool = Mock(spec=Tool) + mock_tool.entity = Mock() + mock_tool.entity.parameters = [base_param1, base_param2, base_param3] + mock_tool.entity.identity = Mock() + mock_tool.entity.identity.author = "test_author" + mock_tool.entity.identity.name = "test_tool" + mock_tool.entity.identity.label = I18nObject(en_US="Test Tool") + mock_tool.entity.description = Mock() + mock_tool.entity.description.human = I18nObject(en_US="Test description") + mock_tool.entity.output_schema = {} + mock_tool.get_runtime_parameters.return_value = [runtime_param2, runtime_param4] + + # Mock fork_tool_runtime to return the same tool + mock_tool.fork_tool_runtime.return_value = mock_tool + + # Call the method + result = ToolTransformService.convert_tool_entity_to_api_entity(mock_tool, "test_tenant", None) + + # Verify the result + assert isinstance(result, ToolApiEntity) + assert result.parameters is not None + assert len(result.parameters) == 4 + + # Check that order is maintained: base parameters first, then new runtime parameters + param_names = [p.name for p in result.parameters] + assert param_names == ["param1", "param2", "param3", "param4"] + + # Verify that param2 was overridden with runtime version + param2 = result.parameters[1] + assert param2.name == "param2" + assert param2.label == "Runtime Param 2" + + +class TestWorkflowProviderToUserProvider: + """Test cases for ToolTransformService.workflow_provider_to_user_provider method""" + + def test_workflow_provider_to_user_provider_with_workflow_app_id(self): + """Test that workflow_provider_to_user_provider correctly sets workflow_app_id.""" + from core.tools.workflow_as_tool.provider import WorkflowToolProviderController + + # Create mock workflow tool provider controller + workflow_app_id = "app_123" + provider_id = "provider_123" + mock_controller = Mock(spec=WorkflowToolProviderController) + mock_controller.provider_id = provider_id + mock_controller.entity = Mock() + mock_controller.entity.identity = Mock() + mock_controller.entity.identity.author = "test_author" + mock_controller.entity.identity.name = "test_workflow_tool" + mock_controller.entity.identity.description = I18nObject(en_US="Test description") + mock_controller.entity.identity.icon = {"type": "emoji", "content": "🔧"} + mock_controller.entity.identity.icon_dark = None + mock_controller.entity.identity.label = I18nObject(en_US="Test Workflow Tool") + + # Call the method + result = ToolTransformService.workflow_provider_to_user_provider( + provider_controller=mock_controller, + labels=["label1", "label2"], + workflow_app_id=workflow_app_id, + ) + + # Verify the result + assert isinstance(result, ToolProviderApiEntity) + assert result.id == provider_id + assert result.author == "test_author" + assert result.name == "test_workflow_tool" + assert result.type == ToolProviderType.WORKFLOW + assert result.workflow_app_id == workflow_app_id + assert result.labels == ["label1", "label2"] + assert result.is_team_authorization is True + assert result.plugin_id is None + assert result.plugin_unique_identifier is None + assert result.tools == [] + + def test_workflow_provider_to_user_provider_without_workflow_app_id(self): + """Test that workflow_provider_to_user_provider works when workflow_app_id is not provided.""" + from core.tools.workflow_as_tool.provider import WorkflowToolProviderController + + # Create mock workflow tool provider controller + provider_id = "provider_123" + mock_controller = Mock(spec=WorkflowToolProviderController) + mock_controller.provider_id = provider_id + mock_controller.entity = Mock() + mock_controller.entity.identity = Mock() + mock_controller.entity.identity.author = "test_author" + mock_controller.entity.identity.name = "test_workflow_tool" + mock_controller.entity.identity.description = I18nObject(en_US="Test description") + mock_controller.entity.identity.icon = {"type": "emoji", "content": "🔧"} + mock_controller.entity.identity.icon_dark = None + mock_controller.entity.identity.label = I18nObject(en_US="Test Workflow Tool") + + # Call the method without workflow_app_id + result = ToolTransformService.workflow_provider_to_user_provider( + provider_controller=mock_controller, + labels=["label1"], + ) + + # Verify the result + assert isinstance(result, ToolProviderApiEntity) + assert result.id == provider_id + assert result.workflow_app_id is None + assert result.labels == ["label1"] + + def test_workflow_provider_to_user_provider_workflow_app_id_none(self): + """Test that workflow_provider_to_user_provider handles None workflow_app_id explicitly.""" + from core.tools.workflow_as_tool.provider import WorkflowToolProviderController + + # Create mock workflow tool provider controller + provider_id = "provider_123" + mock_controller = Mock(spec=WorkflowToolProviderController) + mock_controller.provider_id = provider_id + mock_controller.entity = Mock() + mock_controller.entity.identity = Mock() + mock_controller.entity.identity.author = "test_author" + mock_controller.entity.identity.name = "test_workflow_tool" + mock_controller.entity.identity.description = I18nObject(en_US="Test description") + mock_controller.entity.identity.icon = {"type": "emoji", "content": "🔧"} + mock_controller.entity.identity.icon_dark = None + mock_controller.entity.identity.label = I18nObject(en_US="Test Workflow Tool") + + # Call the method with explicit None values + result = ToolTransformService.workflow_provider_to_user_provider( + provider_controller=mock_controller, + labels=None, + workflow_app_id=None, + ) + + # Verify the result + assert isinstance(result, ToolProviderApiEntity) + assert result.id == provider_id + assert result.workflow_app_id is None + assert result.labels == [] + + def test_workflow_provider_to_user_provider_preserves_other_fields(self): + """Test that workflow_provider_to_user_provider preserves all other entity fields.""" + from core.tools.workflow_as_tool.provider import WorkflowToolProviderController + + # Create mock workflow tool provider controller with various fields + workflow_app_id = "app_456" + provider_id = "provider_456" + mock_controller = Mock(spec=WorkflowToolProviderController) + mock_controller.provider_id = provider_id + mock_controller.entity = Mock() + mock_controller.entity.identity = Mock() + mock_controller.entity.identity.author = "another_author" + mock_controller.entity.identity.name = "another_workflow_tool" + mock_controller.entity.identity.description = I18nObject( + en_US="Another description", zh_Hans="Another description" + ) + mock_controller.entity.identity.icon = {"type": "emoji", "content": "⚙️"} + mock_controller.entity.identity.icon_dark = {"type": "emoji", "content": "🔧"} + mock_controller.entity.identity.label = I18nObject( + en_US="Another Workflow Tool", zh_Hans="Another Workflow Tool" + ) + + # Call the method + result = ToolTransformService.workflow_provider_to_user_provider( + provider_controller=mock_controller, + labels=["automation", "workflow"], + workflow_app_id=workflow_app_id, + ) + + # Verify all fields are preserved correctly + assert isinstance(result, ToolProviderApiEntity) + assert result.id == provider_id + assert result.author == "another_author" + assert result.name == "another_workflow_tool" + assert result.description.en_US == "Another description" + assert result.description.zh_Hans == "Another description" + assert result.icon == {"type": "emoji", "content": "⚙️"} + assert result.icon_dark == {"type": "emoji", "content": "🔧"} + assert result.label.en_US == "Another Workflow Tool" + assert result.label.zh_Hans == "Another Workflow Tool" + assert result.type == ToolProviderType.WORKFLOW + assert result.workflow_app_id == workflow_app_id + assert result.labels == ["automation", "workflow"] + assert result.masked_credentials == {} + assert result.is_team_authorization is True + assert result.allow_delete is True + assert result.plugin_id is None + assert result.plugin_unique_identifier is None + assert result.tools == [] + + +class TestGetToolProviderIconUrl: + def test_builtin_provider_returns_console_url(self): + with patch(f"{MODULE}.dify_config") as cfg: + cfg.CONSOLE_API_URL = "https://app.dify.ai" + url = ToolTransformService.get_tool_provider_icon_url("builtin", "google", "icon.png") + + assert "/builtin/google/icon" in url + assert url.startswith("https://app.dify.ai/console/api/workspaces/current/tool-provider") + + def test_builtin_provider_with_no_console_url(self): + with patch(f"{MODULE}.dify_config") as cfg: + cfg.CONSOLE_API_URL = None + url = ToolTransformService.get_tool_provider_icon_url("builtin", "slack", "icon.png") + + assert "/builtin/slack/icon" in url + + def test_api_provider_parses_json_icon(self): + icon_json = '{"background": "#fff", "content": "A"}' + result = ToolTransformService.get_tool_provider_icon_url("api", "my-api", icon_json) + assert result == {"background": "#fff", "content": "A"} + + def test_api_provider_returns_dict_icon_directly(self): + icon = {"background": "#000", "content": "B"} + result = ToolTransformService.get_tool_provider_icon_url("api", "my-api", icon) + assert result == icon + + def test_api_provider_returns_fallback_on_invalid_json(self): + result = ToolTransformService.get_tool_provider_icon_url("api", "my-api", "not-json") + assert result == {"background": "#252525", "content": "\ud83d\ude01"} + + def test_workflow_provider_behaves_like_api(self): + icon = {"background": "#123", "content": "W"} + assert ToolTransformService.get_tool_provider_icon_url("workflow", "wf", icon) == icon + + def test_mcp_returns_icon_as_is(self): + assert ToolTransformService.get_tool_provider_icon_url("mcp", "srv", "icon-value") == "icon-value" + + def test_unknown_type_returns_empty(self): + assert ToolTransformService.get_tool_provider_icon_url("unknown", "x", "i") == "" + + +class TestRepackProvider: + def test_repacks_dict_provider_icon(self): + provider = {"type": "builtin", "name": "google", "icon": "old"} + with patch.object(ToolTransformService, "get_tool_provider_icon_url", return_value="/new-url") as mock_fn: + ToolTransformService.repack_provider("t1", provider) + + assert provider["icon"] == "/new-url" + mock_fn.assert_called_once_with(provider_type="builtin", provider_name="google", icon="old") + + def test_repacks_tool_provider_api_entity_without_plugin(self): + entity = MagicMock(spec=ToolProviderApiEntity) + entity.plugin_id = None + entity.type = ToolProviderType.BUILT_IN + entity.name = "slack" + entity.icon = "icon.svg" + entity.icon_dark = "dark.svg" + + with patch.object(ToolTransformService, "get_tool_provider_icon_url", return_value="/url"): + ToolTransformService.repack_provider("t1", entity) + + assert entity.icon == "/url" + assert entity.icon_dark == "/url" + + +class TestConvertMcpSchemaToParameter: + def test_simple_object_schema(self): + schema = { + "type": "object", + "properties": { + "query": {"type": "string", "description": "Search query"}, + "count": {"type": "integer", "description": "Result count"}, + }, + "required": ["query"], + } + + params = ToolTransformService.convert_mcp_schema_to_parameter(schema) + + assert len(params) == 2 + query_param = next(p for p in params if p.name == "query") + count_param = next(p for p in params if p.name == "count") + assert query_param.required is True + assert count_param.required is False + assert count_param.type.value == "number" + + def test_float_maps_to_number(self): + schema = {"type": "object", "properties": {"rate": {"type": "float"}}, "required": []} + assert ToolTransformService.convert_mcp_schema_to_parameter(schema)[0].type.value == "number" + + def test_array_type_attaches_input_schema(self): + prop = {"type": "array", "description": "Items", "items": {"type": "string"}} + schema = {"type": "object", "properties": {"items": prop}, "required": []} + param = ToolTransformService.convert_mcp_schema_to_parameter(schema)[0] + assert param.input_schema is not None + + def test_non_object_schema_returns_empty(self): + assert ToolTransformService.convert_mcp_schema_to_parameter({"type": "string"}) == [] + + def test_missing_properties_returns_empty(self): + assert ToolTransformService.convert_mcp_schema_to_parameter({"type": "object"}) == [] + + def test_list_type_uses_first_element(self): + schema = {"type": "object", "properties": {"f": {"type": ["string", "null"]}}, "required": []} + assert ToolTransformService.convert_mcp_schema_to_parameter(schema)[0].type.value == "string" + + def test_missing_description_defaults_empty(self): + schema = {"type": "object", "properties": {"f": {"type": "string"}}, "required": []} + assert ToolTransformService.convert_mcp_schema_to_parameter(schema)[0].llm_description == "" + + +class TestApiProviderToController: + def test_api_key_header_auth(self): + db_provider = MagicMock() + db_provider.credentials = {"auth_type": "api_key_header"} + with patch(f"{MODULE}.ApiToolProviderController") as ctrl_cls: + ctrl_cls.from_db.return_value = MagicMock() + ToolTransformService.api_provider_to_controller(db_provider) + ctrl_cls.from_db.assert_called_once_with(db_provider=db_provider, auth_type=ApiProviderAuthType.API_KEY_HEADER) + + def test_api_key_query_auth(self): + db_provider = MagicMock() + db_provider.credentials = {"auth_type": "api_key_query"} + with patch(f"{MODULE}.ApiToolProviderController") as ctrl_cls: + ctrl_cls.from_db.return_value = MagicMock() + ToolTransformService.api_provider_to_controller(db_provider) + ctrl_cls.from_db.assert_called_once_with(db_provider=db_provider, auth_type=ApiProviderAuthType.API_KEY_QUERY) + + def test_legacy_api_key_maps_to_header(self): + db_provider = MagicMock() + db_provider.credentials = {"auth_type": "api_key"} + with patch(f"{MODULE}.ApiToolProviderController") as ctrl_cls: + ctrl_cls.from_db.return_value = MagicMock() + ToolTransformService.api_provider_to_controller(db_provider) + ctrl_cls.from_db.assert_called_once_with(db_provider=db_provider, auth_type=ApiProviderAuthType.API_KEY_HEADER) + + def test_unknown_auth_defaults_to_none(self): + db_provider = MagicMock() + db_provider.credentials = {"auth_type": "something_else"} + with patch(f"{MODULE}.ApiToolProviderController") as ctrl_cls: + ctrl_cls.from_db.return_value = MagicMock() + ToolTransformService.api_provider_to_controller(db_provider) + ctrl_cls.from_db.assert_called_once_with(db_provider=db_provider, auth_type=ApiProviderAuthType.NONE) From adc6c6c13b182216f4c53a077ffe996187610b1f Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Tue, 31 Mar 2026 11:46:02 +0800 Subject: [PATCH 032/199] chore: try to avoid supply chain security (#34317) --- pnpm-lock.yaml | 6 ++---- pnpm-workspace.yaml | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 01a96c5585..b6c234d8ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -249,9 +249,6 @@ catalogs: autoprefixer: specifier: 10.4.27 version: 10.4.27 - axios: - specifier: ^1.14.0 - version: 1.14.0 class-variance-authority: specifier: 0.7.1 version: 0.7.1 @@ -573,6 +570,7 @@ overrides: array.prototype.flatmap: npm:@nolyfill/array.prototype.flatmap@^1.0.44 array.prototype.tosorted: npm:@nolyfill/array.prototype.tosorted@^1.0.44 assert: npm:@nolyfill/assert@^1.0.26 + axios: 1.14.0 brace-expansion@<2.0.2: 2.0.2 canvas: ^3.2.2 devalue@<5.3.2: 5.3.2 @@ -652,7 +650,7 @@ importers: sdks/nodejs-client: dependencies: axios: - specifier: 'catalog:' + specifier: 1.14.0 version: 1.14.0 devDependencies: '@eslint/js': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index dece6f3f4f..ae53a57832 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,12 @@ +trustPolicy: no-downgrade +minimumReleaseAge: 1440 +blockExoticSubdeps: true +strictDepBuilds: true +allowBuilds: + '@parcel/watcher': false + canvas: false + esbuild: false + sharp: false packages: - web - e2e @@ -13,6 +22,7 @@ overrides: array.prototype.flatmap: npm:@nolyfill/array.prototype.flatmap@^1.0.44 array.prototype.tosorted: npm:@nolyfill/array.prototype.tosorted@^1.0.44 assert: npm:@nolyfill/assert@^1.0.26 + axios: 1.14.0 brace-expansion@<2.0.2: 2.0.2 canvas: ^3.2.2 devalue@<5.3.2: 5.3.2 @@ -59,13 +69,6 @@ overrides: which-typed-array: npm:@nolyfill/which-typed-array@^1.0.44 yaml@>=2.0.0 <2.8.3: 2.8.3 yauzl@<3.2.1: 3.2.1 -ignoredBuiltDependencies: - - canvas - - core-js-pure -onlyBuiltDependencies: - - "@parcel/watcher" - - esbuild - - sharp catalog: "@amplitude/analytics-browser": 2.38.0 "@amplitude/plugin-session-replay-browser": 1.27.5 @@ -149,7 +152,7 @@ catalog: agentation: 3.0.2 ahooks: 3.9.7 autoprefixer: 10.4.27 - axios: ^1.14.0 + axios: 1.14.0 class-variance-authority: 0.7.1 clsx: 2.1.1 cmdk: 1.1.1 From 88863609e979d5eb080f6e61be6b1a406773cdcd Mon Sep 17 00:00:00 2001 From: YBoy Date: Tue, 31 Mar 2026 07:56:53 +0300 Subject: [PATCH 033/199] test: migrate rag pipeline controller tests to testcontainers (#34303) --- .../rag_pipeline/test_rag_pipeline.py | 91 +++++++++---------- 1 file changed, 44 insertions(+), 47 deletions(-) rename api/tests/{unit_tests => test_containers_integration_tests}/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py (77%) diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py b/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py similarity index 77% rename from api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py rename to api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py index ebbb34e069..d5ae95dfb7 100644 --- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py +++ b/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py @@ -1,6 +1,12 @@ +"""Testcontainers integration tests for rag_pipeline controller endpoints.""" + +from __future__ import annotations + from unittest.mock import MagicMock, patch +from uuid import uuid4 import pytest +from sqlalchemy.orm import Session from controllers.console import console_ns from controllers.console.datasets.rag_pipeline.rag_pipeline import ( @@ -9,6 +15,7 @@ from controllers.console.datasets.rag_pipeline.rag_pipeline import ( PipelineTemplateListApi, PublishCustomizedPipelineTemplateApi, ) +from models.dataset import PipelineCustomizedTemplate def unwrap(func): @@ -18,6 +25,10 @@ def unwrap(func): class TestPipelineTemplateListApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_get_success(self, app): api = PipelineTemplateListApi() method = unwrap(api.get) @@ -38,6 +49,10 @@ class TestPipelineTemplateListApi: class TestPipelineTemplateDetailApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_get_success(self, app): api = PipelineTemplateDetailApi() method = unwrap(api.get) @@ -99,6 +114,10 @@ class TestPipelineTemplateDetailApi: class TestCustomizedPipelineTemplateApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_patch_success(self, app): api = CustomizedPipelineTemplateApi() method = unwrap(api.patch) @@ -136,35 +155,29 @@ class TestCustomizedPipelineTemplateApi: delete_mock.assert_called_once_with("tpl-1") assert response == 200 - def test_post_success(self, app): + def test_post_success(self, app, db_session_with_containers: Session): api = CustomizedPipelineTemplateApi() method = unwrap(api.post) - template = MagicMock() - template.yaml_content = "yaml-data" + tenant_id = str(uuid4()) + template = PipelineCustomizedTemplate( + tenant_id=tenant_id, + name="Test Template", + description="Test", + chunk_structure="hierarchical", + icon={"icon": "📘"}, + position=0, + yaml_content="yaml-data", + install_count=0, + language="en-US", + created_by=str(uuid4()), + ) + db_session_with_containers.add(template) + db_session_with_containers.commit() + db_session_with_containers.expire_all() - fake_db = MagicMock() - fake_db.engine = MagicMock() - - session = MagicMock() - session.query.return_value.where.return_value.first.return_value = template - - session_ctx = MagicMock() - session_ctx.__enter__.return_value = session - session_ctx.__exit__.return_value = None - - with ( - app.test_request_context("/"), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline.db", - fake_db, - ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline.Session", - return_value=session_ctx, - ), - ): - response, status = method(api, "tpl-1") + with app.test_request_context("/"): + response, status = method(api, template.id) assert status == 200 assert response == {"data": "yaml-data"} @@ -173,32 +186,16 @@ class TestCustomizedPipelineTemplateApi: api = CustomizedPipelineTemplateApi() method = unwrap(api.post) - fake_db = MagicMock() - fake_db.engine = MagicMock() - - session = MagicMock() - session.query.return_value.where.return_value.first.return_value = None - - session_ctx = MagicMock() - session_ctx.__enter__.return_value = session - session_ctx.__exit__.return_value = None - - with ( - app.test_request_context("/"), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline.db", - fake_db, - ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline.Session", - return_value=session_ctx, - ), - ): + with app.test_request_context("/"): with pytest.raises(ValueError): - method(api, "tpl-1") + method(api, str(uuid4())) class TestPublishCustomizedPipelineTemplateApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_post_success(self, app): api = PublishCustomizedPipelineTemplateApi() method = unwrap(api.post) From 9b7b432e0822c4fffd09d3166132dddd4ae969e7 Mon Sep 17 00:00:00 2001 From: YBoy Date: Tue, 31 Mar 2026 07:57:53 +0300 Subject: [PATCH 034/199] test: migrate rag pipeline import controller tests to testcontainers (#34305) --- .../rag_pipeline/test_rag_pipeline_import.py | 130 +++--------------- 1 file changed, 22 insertions(+), 108 deletions(-) rename api/tests/{unit_tests => test_containers_integration_tests}/controllers/console/datasets/rag_pipeline/test_rag_pipeline_import.py (66%) diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_import.py b/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_import.py similarity index 66% rename from api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_import.py rename to api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_import.py index a72ad45110..cb67892878 100644 --- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_import.py +++ b/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_import.py @@ -1,5 +1,11 @@ +"""Testcontainers integration tests for rag_pipeline_import controller endpoints.""" + +from __future__ import annotations + from unittest.mock import MagicMock, patch +import pytest + from controllers.console import console_ns from controllers.console.datasets.rag_pipeline.rag_pipeline_import import ( RagPipelineExportApi, @@ -18,6 +24,10 @@ def unwrap(func): class TestRagPipelineImportApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def _payload(self, mode="create"): return { "mode": mode, @@ -30,7 +40,6 @@ class TestRagPipelineImportApi: method = unwrap(api.post) payload = self._payload() - user = MagicMock() result = MagicMock() result.status = "completed" @@ -39,13 +48,6 @@ class TestRagPipelineImportApi: service = MagicMock() service.import_rag_pipeline.return_value = result - fake_db = MagicMock() - fake_db.engine = MagicMock() - - session_ctx = MagicMock() - session_ctx.__enter__.return_value = MagicMock() - session_ctx.__exit__.return_value = None - with ( app.test_request_context("/", json=payload), patch.object(type(console_ns), "payload", payload), @@ -53,14 +55,6 @@ class TestRagPipelineImportApi: "controllers.console.datasets.rag_pipeline.rag_pipeline_import.current_account_with_tenant", return_value=(user, "tenant"), ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_import.db", - fake_db, - ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_import.Session", - return_value=session_ctx, - ), patch( "controllers.console.datasets.rag_pipeline.rag_pipeline_import.RagPipelineDslService", return_value=service, @@ -76,7 +70,6 @@ class TestRagPipelineImportApi: method = unwrap(api.post) payload = self._payload() - user = MagicMock() result = MagicMock() result.status = ImportStatus.FAILED @@ -85,13 +78,6 @@ class TestRagPipelineImportApi: service = MagicMock() service.import_rag_pipeline.return_value = result - fake_db = MagicMock() - fake_db.engine = MagicMock() - - session_ctx = MagicMock() - session_ctx.__enter__.return_value = MagicMock() - session_ctx.__exit__.return_value = None - with ( app.test_request_context("/", json=payload), patch.object(type(console_ns), "payload", payload), @@ -99,14 +85,6 @@ class TestRagPipelineImportApi: "controllers.console.datasets.rag_pipeline.rag_pipeline_import.current_account_with_tenant", return_value=(user, "tenant"), ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_import.db", - fake_db, - ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_import.Session", - return_value=session_ctx, - ), patch( "controllers.console.datasets.rag_pipeline.rag_pipeline_import.RagPipelineDslService", return_value=service, @@ -122,7 +100,6 @@ class TestRagPipelineImportApi: method = unwrap(api.post) payload = self._payload() - user = MagicMock() result = MagicMock() result.status = ImportStatus.PENDING @@ -131,13 +108,6 @@ class TestRagPipelineImportApi: service = MagicMock() service.import_rag_pipeline.return_value = result - fake_db = MagicMock() - fake_db.engine = MagicMock() - - session_ctx = MagicMock() - session_ctx.__enter__.return_value = MagicMock() - session_ctx.__exit__.return_value = None - with ( app.test_request_context("/", json=payload), patch.object(type(console_ns), "payload", payload), @@ -145,14 +115,6 @@ class TestRagPipelineImportApi: "controllers.console.datasets.rag_pipeline.rag_pipeline_import.current_account_with_tenant", return_value=(user, "tenant"), ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_import.db", - fake_db, - ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_import.Session", - return_value=session_ctx, - ), patch( "controllers.console.datasets.rag_pipeline.rag_pipeline_import.RagPipelineDslService", return_value=service, @@ -165,6 +127,10 @@ class TestRagPipelineImportApi: class TestRagPipelineImportConfirmApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_confirm_success(self, app): api = RagPipelineImportConfirmApi() method = unwrap(api.post) @@ -177,27 +143,12 @@ class TestRagPipelineImportConfirmApi: service = MagicMock() service.confirm_import.return_value = result - fake_db = MagicMock() - fake_db.engine = MagicMock() - - session_ctx = MagicMock() - session_ctx.__enter__.return_value = MagicMock() - session_ctx.__exit__.return_value = None - with ( app.test_request_context("/"), patch( "controllers.console.datasets.rag_pipeline.rag_pipeline_import.current_account_with_tenant", return_value=(user, "tenant"), ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_import.db", - fake_db, - ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_import.Session", - return_value=session_ctx, - ), patch( "controllers.console.datasets.rag_pipeline.rag_pipeline_import.RagPipelineDslService", return_value=service, @@ -220,27 +171,12 @@ class TestRagPipelineImportConfirmApi: service = MagicMock() service.confirm_import.return_value = result - fake_db = MagicMock() - fake_db.engine = MagicMock() - - session_ctx = MagicMock() - session_ctx.__enter__.return_value = MagicMock() - session_ctx.__exit__.return_value = None - with ( app.test_request_context("/"), patch( "controllers.console.datasets.rag_pipeline.rag_pipeline_import.current_account_with_tenant", return_value=(user, "tenant"), ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_import.db", - fake_db, - ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_import.Session", - return_value=session_ctx, - ), patch( "controllers.console.datasets.rag_pipeline.rag_pipeline_import.RagPipelineDslService", return_value=service, @@ -253,6 +189,10 @@ class TestRagPipelineImportConfirmApi: class TestRagPipelineImportCheckDependenciesApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_get_success(self, app): api = RagPipelineImportCheckDependenciesApi() method = unwrap(api.get) @@ -264,23 +204,8 @@ class TestRagPipelineImportCheckDependenciesApi: service = MagicMock() service.check_dependencies.return_value = result - fake_db = MagicMock() - fake_db.engine = MagicMock() - - session_ctx = MagicMock() - session_ctx.__enter__.return_value = MagicMock() - session_ctx.__exit__.return_value = None - with ( app.test_request_context("/"), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_import.db", - fake_db, - ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_import.Session", - return_value=session_ctx, - ), patch( "controllers.console.datasets.rag_pipeline.rag_pipeline_import.RagPipelineDslService", return_value=service, @@ -293,6 +218,10 @@ class TestRagPipelineImportCheckDependenciesApi: class TestRagPipelineExportApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_get_with_include_secret(self, app): api = RagPipelineExportApi() method = unwrap(api.get) @@ -301,23 +230,8 @@ class TestRagPipelineExportApi: service = MagicMock() service.export_rag_pipeline_dsl.return_value = {"yaml": "data"} - fake_db = MagicMock() - fake_db.engine = MagicMock() - - session_ctx = MagicMock() - session_ctx.__enter__.return_value = MagicMock() - session_ctx.__exit__.return_value = None - with ( app.test_request_context("/?include_secret=true"), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_import.db", - fake_db, - ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_import.Session", - return_value=session_ctx, - ), patch( "controllers.console.datasets.rag_pipeline.rag_pipeline_import.RagPipelineDslService", return_value=service, From cc68f0e640f0932e4e037ab307e6dc7c4ae8b53f Mon Sep 17 00:00:00 2001 From: YBoy Date: Tue, 31 Mar 2026 07:58:14 +0300 Subject: [PATCH 035/199] test: migrate rag pipeline workflow controller tests to testcontainers (#34306) --- .../test_rag_pipeline_workflow.py | 145 +++++++++--------- 1 file changed, 70 insertions(+), 75 deletions(-) rename api/tests/{unit_tests => test_containers_integration_tests}/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py (91%) diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py b/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py similarity index 91% rename from api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py rename to api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py index a3c0592d76..c1f3122c2b 100644 --- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py +++ b/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py @@ -1,7 +1,13 @@ +"""Testcontainers integration tests for rag_pipeline_workflow controller endpoints.""" + +from __future__ import annotations + from datetime import datetime from unittest.mock import MagicMock, patch +from uuid import uuid4 import pytest +from sqlalchemy.orm import Session from werkzeug.exceptions import BadRequest, Forbidden, HTTPException, NotFound import services @@ -38,6 +44,10 @@ def unwrap(func): class TestDraftWorkflowApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_get_draft_success(self, app): api = DraftRagPipelineApi() method = unwrap(api.get) @@ -200,6 +210,10 @@ class TestDraftWorkflowApi: class TestDraftRunNodes: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_iteration_node_success(self, app): api = RagPipelineDraftRunIterationNodeApi() method = unwrap(api.post) @@ -275,6 +289,10 @@ class TestDraftRunNodes: class TestPipelineRunApis: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_draft_run_success(self, app): api = DraftRagPipelineRunApi() method = unwrap(api.post) @@ -337,6 +355,10 @@ class TestPipelineRunApis: class TestDraftNodeRun: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_execution_not_found(self, app): api = RagPipelineDraftNodeRunApi() method = unwrap(api.post) @@ -364,45 +386,43 @@ class TestDraftNodeRun: class TestPublishedPipelineApis: - def test_publish_success(self, app): + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + + def test_publish_success(self, app, db_session_with_containers: Session): + from models.dataset import Pipeline + api = PublishedRagPipelineApi() method = unwrap(api.post) - pipeline = MagicMock() + tenant_id = str(uuid4()) + pipeline = Pipeline( + tenant_id=tenant_id, + name="test-pipeline", + description="test", + created_by=str(uuid4()), + ) + db_session_with_containers.add(pipeline) + db_session_with_containers.commit() + db_session_with_containers.expire_all() + user = MagicMock(id="u1") workflow = MagicMock( - id="w1", + id=str(uuid4()), created_at=naive_utc_now(), ) - session = MagicMock() - session.merge.return_value = pipeline - - session_ctx = MagicMock() - session_ctx.__enter__.return_value = session - session_ctx.__exit__.return_value = None - service = MagicMock() service.publish_workflow.return_value = workflow - fake_db = MagicMock() - fake_db.engine = MagicMock() - with ( app.test_request_context("/"), patch( "controllers.console.datasets.rag_pipeline.rag_pipeline_workflow.current_account_with_tenant", return_value=(user, "t"), ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_workflow.db", - fake_db, - ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_workflow.Session", - return_value=session_ctx, - ), patch( "controllers.console.datasets.rag_pipeline.rag_pipeline_workflow.RagPipelineService", return_value=service, @@ -415,6 +435,10 @@ class TestPublishedPipelineApis: class TestMiscApis: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_task_stop(self, app): api = RagPipelineTaskStopApi() method = unwrap(api.post) @@ -471,6 +495,10 @@ class TestMiscApis: class TestPublishedRagPipelineRunApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_published_run_success(self, app): api = PublishedRagPipelineRunApi() method = unwrap(api.post) @@ -536,6 +564,10 @@ class TestPublishedRagPipelineRunApi: class TestDefaultBlockConfigApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_get_block_config_success(self, app): api = DefaultRagPipelineBlockConfigApi() method = unwrap(api.get) @@ -567,6 +599,10 @@ class TestDefaultBlockConfigApi: class TestPublishedAllRagPipelineApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_get_published_workflows_success(self, app): api = PublishedAllRagPipelineApi() method = unwrap(api.get) @@ -577,28 +613,12 @@ class TestPublishedAllRagPipelineApi: service = MagicMock() service.get_all_published_workflow.return_value = ([{"id": "w1"}], False) - session = MagicMock() - session_ctx = MagicMock() - session_ctx.__enter__.return_value = session - session_ctx.__exit__.return_value = None - - fake_db = MagicMock() - fake_db.engine = MagicMock() - with ( app.test_request_context("/"), patch( "controllers.console.datasets.rag_pipeline.rag_pipeline_workflow.current_account_with_tenant", return_value=(user, "t"), ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_workflow.db", - fake_db, - ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_workflow.Session", - return_value=session_ctx, - ), patch( "controllers.console.datasets.rag_pipeline.rag_pipeline_workflow.RagPipelineService", return_value=service, @@ -628,6 +648,10 @@ class TestPublishedAllRagPipelineApi: class TestRagPipelineByIdApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_patch_success(self, app): api = RagPipelineByIdApi() method = unwrap(api.patch) @@ -640,14 +664,6 @@ class TestRagPipelineByIdApi: service = MagicMock() service.update_workflow.return_value = workflow - session = MagicMock() - session_ctx = MagicMock() - session_ctx.__enter__.return_value = session - session_ctx.__exit__.return_value = None - - fake_db = MagicMock() - fake_db.engine = MagicMock() - payload = {"marked_name": "test"} with ( @@ -657,14 +673,6 @@ class TestRagPipelineByIdApi: "controllers.console.datasets.rag_pipeline.rag_pipeline_workflow.current_account_with_tenant", return_value=(user, "t"), ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_workflow.db", - fake_db, - ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_workflow.Session", - return_value=session_ctx, - ), patch( "controllers.console.datasets.rag_pipeline.rag_pipeline_workflow.RagPipelineService", return_value=service, @@ -700,24 +708,8 @@ class TestRagPipelineByIdApi: workflow_service = MagicMock() - session = MagicMock() - session_ctx = MagicMock() - session_ctx.__enter__.return_value = session - session_ctx.__exit__.return_value = None - - fake_db = MagicMock() - fake_db.engine = MagicMock() - with ( app.test_request_context("/", method="DELETE"), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_workflow.db", - fake_db, - ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_workflow.Session", - return_value=session_ctx, - ), patch( "controllers.console.datasets.rag_pipeline.rag_pipeline_workflow.WorkflowService", return_value=workflow_service, @@ -725,12 +717,7 @@ class TestRagPipelineByIdApi: ): result = method(api, pipeline, "old-workflow") - workflow_service.delete_workflow.assert_called_once_with( - session=session, - workflow_id="old-workflow", - tenant_id="t1", - ) - session.commit.assert_called_once() + workflow_service.delete_workflow.assert_called_once() assert result == (None, 204) def test_delete_active_workflow_rejected(self, app): @@ -745,6 +732,10 @@ class TestRagPipelineByIdApi: class TestRagPipelineWorkflowLastRunApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_last_run_success(self, app): api = RagPipelineWorkflowLastRunApi() method = unwrap(api.get) @@ -788,6 +779,10 @@ class TestRagPipelineWorkflowLastRunApi: class TestRagPipelineDatasourceVariableApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_set_datasource_variables_success(self, app): api = RagPipelineDatasourceVariableApi() method = unwrap(api.post) From 303f5484085b16a088d2a8d734cfa5d20749ddec Mon Sep 17 00:00:00 2001 From: YBoy Date: Tue, 31 Mar 2026 07:59:13 +0300 Subject: [PATCH 036/199] test: migrate rag pipeline datasets controller tests to testcontainers (#34304) --- .../test_rag_pipeline_datasets.py | 42 ++++++------------- 1 file changed, 12 insertions(+), 30 deletions(-) rename api/tests/{unit_tests => test_containers_integration_tests}/controllers/console/datasets/rag_pipeline/test_rag_pipeline_datasets.py (83%) diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_datasets.py b/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_datasets.py similarity index 83% rename from api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_datasets.py rename to api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_datasets.py index fd38fcbb5e..64e3de2ca3 100644 --- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_datasets.py +++ b/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_datasets.py @@ -1,3 +1,7 @@ +"""Testcontainers integration tests for rag_pipeline_datasets controller endpoints.""" + +from __future__ import annotations + from unittest.mock import MagicMock, patch import pytest @@ -19,6 +23,10 @@ def unwrap(func): class TestCreateRagPipelineDatasetApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def _valid_payload(self): return {"yaml_content": "name: test"} @@ -33,13 +41,6 @@ class TestCreateRagPipelineDatasetApi: mock_service = MagicMock() mock_service.create_rag_pipeline_dataset.return_value = import_info - mock_session_ctx = MagicMock() - mock_session_ctx.__enter__.return_value = MagicMock() - mock_session_ctx.__exit__.return_value = None - - fake_db = MagicMock() - fake_db.engine = MagicMock() - with ( app.test_request_context("/", json=payload), patch.object(type(console_ns), "payload", payload), @@ -47,14 +48,6 @@ class TestCreateRagPipelineDatasetApi: "controllers.console.datasets.rag_pipeline.rag_pipeline_datasets.current_account_with_tenant", return_value=(user, "tenant-1"), ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_datasets.db", - fake_db, - ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_datasets.Session", - return_value=mock_session_ctx, - ), patch( "controllers.console.datasets.rag_pipeline.rag_pipeline_datasets.RagPipelineDslService", return_value=mock_service, @@ -93,13 +86,6 @@ class TestCreateRagPipelineDatasetApi: mock_service = MagicMock() mock_service.create_rag_pipeline_dataset.side_effect = services.errors.dataset.DatasetNameDuplicateError() - mock_session_ctx = MagicMock() - mock_session_ctx.__enter__.return_value = MagicMock() - mock_session_ctx.__exit__.return_value = None - - fake_db = MagicMock() - fake_db.engine = MagicMock() - with ( app.test_request_context("/", json=payload), patch.object(type(console_ns), "payload", payload), @@ -107,14 +93,6 @@ class TestCreateRagPipelineDatasetApi: "controllers.console.datasets.rag_pipeline.rag_pipeline_datasets.current_account_with_tenant", return_value=(user, "tenant-1"), ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_datasets.db", - fake_db, - ), - patch( - "controllers.console.datasets.rag_pipeline.rag_pipeline_datasets.Session", - return_value=mock_session_ctx, - ), patch( "controllers.console.datasets.rag_pipeline.rag_pipeline_datasets.RagPipelineDslService", return_value=mock_service, @@ -143,6 +121,10 @@ class TestCreateRagPipelineDatasetApi: class TestCreateEmptyRagPipelineDatasetApi: + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + def test_post_success(self, app): api = CreateEmptyRagPipelineDatasetApi() method = unwrap(api.post) From 1063e021f237922541621a9741595788a389345d Mon Sep 17 00:00:00 2001 From: YBoy Date: Tue, 31 Mar 2026 08:00:22 +0300 Subject: [PATCH 037/199] test: migrate explore conversation controller tests to testcontainers (#34312) --- .../console/explore/test_conversation.py | 61 +++++++++++-------- 1 file changed, 34 insertions(+), 27 deletions(-) rename api/tests/{unit_tests => test_containers_integration_tests}/controllers/console/explore/test_conversation.py (82%) diff --git a/api/tests/unit_tests/controllers/console/explore/test_conversation.py b/api/tests/test_containers_integration_tests/controllers/console/explore/test_conversation.py similarity index 82% rename from api/tests/unit_tests/controllers/console/explore/test_conversation.py rename to api/tests/test_containers_integration_tests/controllers/console/explore/test_conversation.py index 65cc209725..83492048ef 100644 --- a/api/tests/unit_tests/controllers/console/explore/test_conversation.py +++ b/api/tests/test_containers_integration_tests/controllers/console/explore/test_conversation.py @@ -1,7 +1,10 @@ +"""Testcontainers integration tests for controllers.console.explore.conversation endpoints.""" + +from __future__ import annotations + from unittest.mock import MagicMock, patch import pytest -from flask import Flask from werkzeug.exceptions import NotFound import controllers.console.explore.conversation as conversation_module @@ -48,24 +51,12 @@ def user(): return user -@pytest.fixture(autouse=True) -def mock_db_and_session(): - with ( - patch.object( - conversation_module, - "db", - MagicMock(session=MagicMock(), engine=MagicMock()), - ), - patch( - "controllers.console.explore.conversation.Session", - MagicMock(), - ), - ): - yield - - class TestConversationListApi: - def test_get_success(self, app: Flask, chat_app, user): + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + + def test_get_success(self, app, chat_app, user): api = conversation_module.ConversationListApi() method = unwrap(api.get) @@ -90,7 +81,7 @@ class TestConversationListApi: assert result["has_more"] is False assert len(result["data"]) == 2 - def test_last_conversation_not_exists(self, app: Flask, chat_app, user): + def test_last_conversation_not_exists(self, app, chat_app, user): api = conversation_module.ConversationListApi() method = unwrap(api.get) @@ -106,7 +97,7 @@ class TestConversationListApi: with pytest.raises(NotFound): method(chat_app) - def test_wrong_app_mode(self, app: Flask, non_chat_app): + def test_wrong_app_mode(self, app, non_chat_app): api = conversation_module.ConversationListApi() method = unwrap(api.get) @@ -116,7 +107,11 @@ class TestConversationListApi: class TestConversationApi: - def test_delete_success(self, app: Flask, chat_app, user): + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + + def test_delete_success(self, app, chat_app, user): api = conversation_module.ConversationApi() method = unwrap(api.delete) @@ -134,7 +129,7 @@ class TestConversationApi: assert status == 204 assert body["result"] == "success" - def test_delete_not_found(self, app: Flask, chat_app, user): + def test_delete_not_found(self, app, chat_app, user): api = conversation_module.ConversationApi() method = unwrap(api.delete) @@ -150,7 +145,7 @@ class TestConversationApi: with pytest.raises(NotFound): method(chat_app, "cid") - def test_delete_wrong_app_mode(self, app: Flask, non_chat_app): + def test_delete_wrong_app_mode(self, app, non_chat_app): api = conversation_module.ConversationApi() method = unwrap(api.delete) @@ -160,7 +155,11 @@ class TestConversationApi: class TestConversationRenameApi: - def test_rename_success(self, app: Flask, chat_app, user): + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + + def test_rename_success(self, app, chat_app, user): api = conversation_module.ConversationRenameApi() method = unwrap(api.post) @@ -179,7 +178,7 @@ class TestConversationRenameApi: assert result["id"] == "cid" - def test_rename_not_found(self, app: Flask, chat_app, user): + def test_rename_not_found(self, app, chat_app, user): api = conversation_module.ConversationRenameApi() method = unwrap(api.post) @@ -197,7 +196,11 @@ class TestConversationRenameApi: class TestConversationPinApi: - def test_pin_success(self, app: Flask, chat_app, user): + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + + def test_pin_success(self, app, chat_app, user): api = conversation_module.ConversationPinApi() method = unwrap(api.patch) @@ -215,7 +218,11 @@ class TestConversationPinApi: class TestConversationUnPinApi: - def test_unpin_success(self, app: Flask, chat_app, user): + @pytest.fixture + def app(self, flask_app_with_containers): + return flask_app_with_containers + + def test_unpin_success(self, app, chat_app, user): api = conversation_module.ConversationUnPinApi() method = unwrap(api.patch) From 6b0c6d0cde33f2f5b5dc07aa274ce5ec5afabbc8 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:06:16 +0800 Subject: [PATCH 038/199] fix(web): internationalize DSL export modal labels (#34323) --- web/app/components/workflow/dsl-export-confirm-modal.tsx | 6 +++--- web/i18n/en-US/workflow.json | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/web/app/components/workflow/dsl-export-confirm-modal.tsx b/web/app/components/workflow/dsl-export-confirm-modal.tsx index e698de722e..a92698c8b7 100644 --- a/web/app/components/workflow/dsl-export-confirm-modal.tsx +++ b/web/app/components/workflow/dsl-export-confirm-modal.tsx @@ -45,8 +45,8 @@ const DSLExportConfirmModal = ({ - - + + @@ -56,7 +56,7 @@ const DSLExportConfirmModal = ({
{env.name}
-
Secret
+
{t('env.export.secret', { ns: 'workflow' })}
diff --git a/web/i18n/en-US/workflow.json b/web/i18n/en-US/workflow.json index dd9337ecc0..5c7df02791 100644 --- a/web/i18n/en-US/workflow.json +++ b/web/i18n/en-US/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Export secret values", "env.export.export": "Export DSL with secret values ", "env.export.ignore": "Export DSL", + "env.export.name": "Name", + "env.export.secret": "Secret", "env.export.title": "Export Secret environment variables?", + "env.export.value": "Value", "env.modal.description": "Description", "env.modal.descriptionPlaceholder": "Describe the variable", "env.modal.editTitle": "Edit Environment Variable", From fcf04629d3609f606bbdbad80790c7b90489a06a Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:01:17 +0800 Subject: [PATCH 039/199] fix(ci): restore i18n dispatch bridge (#34331) --- .github/workflows/translate-i18n-claude.yml | 54 +++++++++----- .github/workflows/trigger-i18n-sync.yml | 81 +++++++++++++++++++++ 2 files changed, 117 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/trigger-i18n-sync.yml diff --git a/.github/workflows/translate-i18n-claude.yml b/.github/workflows/translate-i18n-claude.yml index aaf51aa606..f3fbfe60e2 100644 --- a/.github/workflows/translate-i18n-claude.yml +++ b/.github/workflows/translate-i18n-claude.yml @@ -1,10 +1,10 @@ name: Translate i18n Files with Claude Code +# Note: claude-code-action doesn't support push events directly. +# Push events are bridged by trigger-i18n-sync.yml via repository_dispatch. on: - push: - branches: [main] - paths: - - 'web/i18n/en-US/*.json' + repository_dispatch: + types: [i18n-sync] workflow_dispatch: inputs: files: @@ -30,7 +30,7 @@ permissions: concurrency: group: translate-i18n-${{ github.event_name }}-${{ github.ref }} - cancel-in-progress: ${{ github.event_name == 'push' }} + cancel-in-progress: false jobs: translate: @@ -67,19 +67,20 @@ jobs: } " web/i18n-config/languages.ts | sed 's/[[:space:]]*$//') - if [ "${{ github.event_name }}" = "push" ]; then - BASE_SHA="${{ github.event.before }}" - if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then - BASE_SHA=$(git rev-parse HEAD~1 2>/dev/null || true) - fi - HEAD_SHA="${{ github.sha }}" - if [ -n "$BASE_SHA" ]; then - CHANGED_FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- 'web/i18n/en-US/*.json' 2>/dev/null | sed -n 's@^.*/@@p' | sed 's/\.json$//' | tr '\n' ' ' | sed 's/[[:space:]]*$//') - else - CHANGED_FILES=$(find web/i18n/en-US -maxdepth 1 -type f -name '*.json' -print | sed -n 's@^.*/@@p' | sed 's/\.json$//' | sort | tr '\n' ' ' | sed 's/[[:space:]]*$//') - fi + if [ "${{ github.event_name }}" = "repository_dispatch" ]; then + BASE_SHA="${{ github.event.client_payload.base_sha }}" + HEAD_SHA="${{ github.event.client_payload.head_sha }}" + CHANGED_FILES="${{ github.event.client_payload.changed_files }}" TARGET_LANGS="$DEFAULT_TARGET_LANGS" - SYNC_MODE="incremental" + SYNC_MODE="${{ github.event.client_payload.sync_mode || 'incremental' }}" + + if [ -n "${{ github.event.client_payload.diff_base64 }}" ]; then + printf '%s' '${{ github.event.client_payload.diff_base64 }}' | base64 -d > /tmp/i18n-diff.txt + DIFF_AVAILABLE="true" + else + : > /tmp/i18n-diff.txt + DIFF_AVAILABLE="false" + fi else BASE_SHA="" HEAD_SHA=$(git rev-parse HEAD) @@ -104,6 +105,18 @@ jobs: else CHANGED_FILES="" fi + + if [ "$SYNC_MODE" = "incremental" ] && [ -n "$BASE_SHA" ]; then + git diff "$BASE_SHA" "$HEAD_SHA" -- 'web/i18n/en-US/*.json' > /tmp/i18n-diff.txt 2>/dev/null || : > /tmp/i18n-diff.txt + else + : > /tmp/i18n-diff.txt + fi + + if [ -s /tmp/i18n-diff.txt ]; then + DIFF_AVAILABLE="true" + else + DIFF_AVAILABLE="false" + fi fi FILE_ARGS="" @@ -123,6 +136,7 @@ jobs: echo "CHANGED_FILES=$CHANGED_FILES" echo "TARGET_LANGS=$TARGET_LANGS" echo "SYNC_MODE=$SYNC_MODE" + echo "DIFF_AVAILABLE=$DIFF_AVAILABLE" echo "FILE_ARGS=$FILE_ARGS" echo "LANG_ARGS=$LANG_ARGS" } >> "$GITHUB_OUTPUT" @@ -156,6 +170,7 @@ jobs: - Head SHA: `${{ steps.context.outputs.HEAD_SHA }}` - Scoped file args: `${{ steps.context.outputs.FILE_ARGS }}` - Scoped language args: `${{ steps.context.outputs.LANG_ARGS }}` + - Full English diff available: `${{ steps.context.outputs.DIFF_AVAILABLE }}` Tool rules: - Use Read for repository files. @@ -173,6 +188,9 @@ jobs: - Do not touch unrelated i18n files. - Do not modify `${{ github.workspace }}/web/i18n/en-US/`. 3. Detect English changes per file. + - Treat the current English JSON files under `${{ github.workspace }}/web/i18n/en-US/` plus the scoped `i18n:check` result as the primary source of truth. + - Use `/tmp/i18n-diff.txt` only as supporting context to understand what changed between `Base SHA` and `Head SHA`. + - Never rely on diff alone when deciding final keys or values. - Read the current English JSON file for each file in scope. - If sync mode is `incremental` and `Base SHA` is not empty, run: `git -C ${{ github.workspace }} show :web/i18n/en-US/.json` @@ -182,7 +200,7 @@ jobs: - ADD: key only in current - UPDATE: key exists in both and the English value changed - DELETE: key only in previous - - Do not rely on a truncated diff file. + - If `/tmp/i18n-diff.txt` is available, read it before translating so wording changes are grounded in the full English patch, but resolve any ambiguity by trusting the actual English files and scoped checks. 4. Run a scoped pre-check before editing: - `pnpm --dir ${{ github.workspace }}/web run i18n:check ${{ steps.context.outputs.FILE_ARGS }} ${{ steps.context.outputs.LANG_ARGS }}` - Use this command as the source of truth for missing and extra keys inside the current scope. diff --git a/.github/workflows/trigger-i18n-sync.yml b/.github/workflows/trigger-i18n-sync.yml new file mode 100644 index 0000000000..ee44fbb0c0 --- /dev/null +++ b/.github/workflows/trigger-i18n-sync.yml @@ -0,0 +1,81 @@ +name: Trigger i18n Sync on Push + +on: + push: + branches: [main] + paths: + - 'web/i18n/en-US/*.json' + +permissions: + contents: write + +concurrency: + group: trigger-i18n-sync-${{ github.ref }} + cancel-in-progress: true + +jobs: + trigger: + if: github.repository == 'langgenius/dify' + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: Detect changed files and generate full diff + id: detect + shell: bash + run: | + BASE_SHA="${{ github.event.before }}" + if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then + BASE_SHA=$(git rev-parse HEAD~1 2>/dev/null || true) + fi + HEAD_SHA="${{ github.sha }}" + + if [ -n "$BASE_SHA" ]; then + CHANGED_FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- 'web/i18n/en-US/*.json' 2>/dev/null | sed -n 's@^.*/@@p' | sed 's/\.json$//' | tr '\n' ' ' | sed 's/[[:space:]]*$//') + git diff "$BASE_SHA" "$HEAD_SHA" -- 'web/i18n/en-US/*.json' > /tmp/i18n-diff.txt 2>/dev/null || : > /tmp/i18n-diff.txt + else + CHANGED_FILES=$(find web/i18n/en-US -maxdepth 1 -type f -name '*.json' -print | sed -n 's@^.*/@@p' | sed 's/\.json$//' | sort | tr '\n' ' ' | sed 's/[[:space:]]*$//') + : > /tmp/i18n-diff.txt + fi + + if [ -n "$CHANGED_FILES" ]; then + echo "has_changes=true" >> "$GITHUB_OUTPUT" + else + echo "has_changes=false" >> "$GITHUB_OUTPUT" + fi + + echo "base_sha=$BASE_SHA" >> "$GITHUB_OUTPUT" + echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT" + echo "changed_files=$CHANGED_FILES" >> "$GITHUB_OUTPUT" + + - name: Trigger i18n sync workflow + if: steps.detect.outputs.has_changes == 'true' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + BASE_SHA: ${{ steps.detect.outputs.base_sha }} + HEAD_SHA: ${{ steps.detect.outputs.head_sha }} + CHANGED_FILES: ${{ steps.detect.outputs.changed_files }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs') + + const diffBase64 = fs.readFileSync('/tmp/i18n-diff.txt').toString('base64') + + await github.rest.repos.createDispatchEvent({ + owner: context.repo.owner, + repo: context.repo.repo, + event_type: 'i18n-sync', + client_payload: { + changed_files: process.env.CHANGED_FILES, + diff_base64: diffBase64, + sync_mode: 'incremental', + base_sha: process.env.BASE_SHA, + head_sha: process.env.HEAD_SHA, + }, + }) From f27d669f87d554378cddfd901777081cc4f8a533 Mon Sep 17 00:00:00 2001 From: 99 Date: Tue, 31 Mar 2026 16:21:22 +0800 Subject: [PATCH 040/199] chore: normalize frozenset literals and myscale typing (#34327) --- api/constants/__init__.py | 37 +- api/controllers/common/file_response.py | 4 +- api/core/helper/csv_sanitizer.py | 2 +- .../jieba/jieba_keyword_table_handler.py | 2 +- .../rag/datasource/keyword/jieba/stopwords.py | 2742 +++++++++-------- .../datasource/vdb/myscale/myscale_vector.py | 2 +- api/core/rag/extractor/pdf_extractor.py | 4 +- api/core/trigger/constants.py | 4 +- .../nodes/trigger_webhook/entities.py | 18 +- api/libs/collection_utils.py | 9 +- api/models/workflow.py | 2 +- .../workflow_draft_variable_service.py | 4 +- .../core/datasource/test_file_upload.py | 24 +- .../keyword/jieba/test_stopwords.py | 1 + .../core/workflow/nodes/llm/test_node.py | 22 +- .../factories/test_variable_factory.py | 2 +- dev/pytest/pytest_config_tests.py | 168 +- 17 files changed, 1536 insertions(+), 1511 deletions(-) diff --git a/api/constants/__init__.py b/api/constants/__init__.py index e441395afc..8698fb855d 100644 --- a/api/constants/__init__.py +++ b/api/constants/__init__.py @@ -7,15 +7,16 @@ UUID_NIL = "00000000-0000-0000-0000-000000000000" DEFAULT_FILE_NUMBER_LIMITS = 3 -IMAGE_EXTENSIONS = convert_to_lower_and_upper_set({"jpg", "jpeg", "png", "webp", "gif", "svg"}) +_IMAGE_EXTENSION_BASE: frozenset[str] = frozenset(("jpg", "jpeg", "png", "webp", "gif", "svg")) +_VIDEO_EXTENSION_BASE: frozenset[str] = frozenset(("mp4", "mov", "mpeg", "webm")) +_AUDIO_EXTENSION_BASE: frozenset[str] = frozenset(("mp3", "m4a", "wav", "amr", "mpga")) -VIDEO_EXTENSIONS = convert_to_lower_and_upper_set({"mp4", "mov", "mpeg", "webm"}) +IMAGE_EXTENSIONS: frozenset[str] = frozenset(convert_to_lower_and_upper_set(_IMAGE_EXTENSION_BASE)) +VIDEO_EXTENSIONS: frozenset[str] = frozenset(convert_to_lower_and_upper_set(_VIDEO_EXTENSION_BASE)) +AUDIO_EXTENSIONS: frozenset[str] = frozenset(convert_to_lower_and_upper_set(_AUDIO_EXTENSION_BASE)) -AUDIO_EXTENSIONS = convert_to_lower_and_upper_set({"mp3", "m4a", "wav", "amr", "mpga"}) - -_doc_extensions: set[str] -if dify_config.ETL_TYPE == "Unstructured": - _doc_extensions = { +_UNSTRUCTURED_DOCUMENT_EXTENSION_BASE: frozenset[str] = frozenset( + ( "txt", "markdown", "md", @@ -35,11 +36,10 @@ if dify_config.ETL_TYPE == "Unstructured": "pptx", "xml", "epub", - } - if dify_config.UNSTRUCTURED_API_URL: - _doc_extensions.add("ppt") -else: - _doc_extensions = { + ) +) +_DEFAULT_DOCUMENT_EXTENSION_BASE: frozenset[str] = frozenset( + ( "txt", "markdown", "md", @@ -53,8 +53,17 @@ else: "csv", "vtt", "properties", - } -DOCUMENT_EXTENSIONS: set[str] = convert_to_lower_and_upper_set(_doc_extensions) + ) +) + +_doc_extensions: set[str] +if dify_config.ETL_TYPE == "Unstructured": + _doc_extensions = set(_UNSTRUCTURED_DOCUMENT_EXTENSION_BASE) + if dify_config.UNSTRUCTURED_API_URL: + _doc_extensions.add("ppt") +else: + _doc_extensions = set(_DEFAULT_DOCUMENT_EXTENSION_BASE) +DOCUMENT_EXTENSIONS: frozenset[str] = frozenset(convert_to_lower_and_upper_set(_doc_extensions)) # console COOKIE_NAME_ACCESS_TOKEN = "access_token" diff --git a/api/controllers/common/file_response.py b/api/controllers/common/file_response.py index ca8ea3d52e..79df978012 100644 --- a/api/controllers/common/file_response.py +++ b/api/controllers/common/file_response.py @@ -4,8 +4,8 @@ from urllib.parse import quote from flask import Response -HTML_MIME_TYPES = frozenset({"text/html", "application/xhtml+xml"}) -HTML_EXTENSIONS = frozenset({"html", "htm"}) +HTML_MIME_TYPES: frozenset[str] = frozenset(("text/html", "application/xhtml+xml")) +HTML_EXTENSIONS: frozenset[str] = frozenset(("html", "htm")) def _normalize_mime_type(mime_type: str | None) -> str: diff --git a/api/core/helper/csv_sanitizer.py b/api/core/helper/csv_sanitizer.py index 0023de5a35..c4fa230b3b 100644 --- a/api/core/helper/csv_sanitizer.py +++ b/api/core/helper/csv_sanitizer.py @@ -17,7 +17,7 @@ class CSVSanitizer: """ # Characters that can start a formula in Excel/LibreOffice/Google Sheets - FORMULA_CHARS = frozenset({"=", "+", "-", "@", "\t", "\r"}) + FORMULA_CHARS = frozenset(("=", "+", "-", "@", "\t", "\r")) @classmethod def sanitize_value(cls, value: Any) -> str: diff --git a/api/core/rag/datasource/keyword/jieba/jieba_keyword_table_handler.py b/api/core/rag/datasource/keyword/jieba/jieba_keyword_table_handler.py index 57a60e6970..84f35c25f8 100644 --- a/api/core/rag/datasource/keyword/jieba/jieba_keyword_table_handler.py +++ b/api/core/rag/datasource/keyword/jieba/jieba_keyword_table_handler.py @@ -122,6 +122,6 @@ class JiebaKeywordTableHandler: results.add(token) sub_tokens = re.findall(r"\w+", token) if len(sub_tokens) > 1: - results.update({w for w in sub_tokens if w not in list(STOPWORDS)}) + results.update({w for w in sub_tokens if w not in STOPWORDS}) return results diff --git a/api/core/rag/datasource/keyword/jieba/stopwords.py b/api/core/rag/datasource/keyword/jieba/stopwords.py index 54b65d9a2d..78ed1cf594 100644 --- a/api/core/rag/datasource/keyword/jieba/stopwords.py +++ b/api/core/rag/datasource/keyword/jieba/stopwords.py @@ -1,1370 +1,1372 @@ -STOPWORDS = { - "during", - "when", - "but", - "then", - "further", - "isn", - "mustn't", - "until", - "own", - "i", - "couldn", - "y", - "only", - "you've", - "ours", - "who", - "where", - "ourselves", - "has", - "to", - "was", - "didn't", - "themselves", - "if", - "against", - "through", - "her", - "an", - "your", - "can", - "those", - "didn", - "about", - "aren't", - "shan't", - "be", - "not", - "these", - "again", - "so", - "t", - "theirs", - "weren", - "won't", - "won", - "itself", - "just", - "same", - "while", - "why", - "doesn", - "aren", - "him", - "haven", - "for", - "you'll", - "that", - "we", - "am", - "d", - "by", - "having", - "wasn't", - "than", - "weren't", - "out", - "from", - "now", - "their", - "too", - "hadn", - "o", - "needn", - "most", - "it", - "under", - "needn't", - "any", - "some", - "few", - "ll", - "hers", - "which", - "m", - "you're", - "off", - "other", - "had", - "she", - "you'd", - "do", - "you", - "does", - "s", - "will", - "each", - "wouldn't", - "hasn't", - "such", - "more", - "whom", - "she's", - "my", - "yours", - "yourself", - "of", - "on", - "very", - "hadn't", - "with", - "yourselves", - "been", - "ma", - "them", - "mightn't", - "shan", - "mustn", - "they", - "what", - "both", - "that'll", - "how", - "is", - "he", - "because", - "down", - "haven't", - "are", - "no", - "it's", - "our", - "being", - "the", - "or", - "above", - "myself", - "once", - "don't", - "doesn't", - "as", - "nor", - "here", - "herself", - "hasn", - "mightn", - "have", - "its", - "all", - "were", - "ain", - "this", - "at", - "after", - "over", - "shouldn't", - "into", - "before", - "don", - "wouldn", - "re", - "couldn't", - "wasn", - "in", - "should", - "there", - "himself", - "isn't", - "should've", - "doing", - "ve", - "shouldn", - "a", - "did", - "and", - "his", - "between", - "me", - "up", - "below", - "人民", - "末##末", - "啊", - "阿", - "哎", - "哎呀", - "哎哟", - "唉", - "俺", - "俺们", - "按", - "按照", - "吧", - "吧哒", - "把", - "罢了", - "被", - "本", - "本着", - "比", - "比方", - "比如", - "鄙人", - "彼", - "彼此", - "边", - "别", - "别的", - "别说", - "并", - "并且", - "不比", - "不成", - "不单", - "不但", - "不独", - "不管", - "不光", - "不过", - "不仅", - "不拘", - "不论", - "不怕", - "不然", - "不如", - "不特", - "不惟", - "不问", - "不只", - "朝", - "朝着", - "趁", - "趁着", - "乘", - "冲", - "除", - "除此之外", - "除非", - "除了", - "此", - "此间", - "此外", - "从", - "从而", - "打", - "待", - "但", - "但是", - "当", - "当着", - "到", - "得", - "的", - "的话", - "等", - "等等", - "地", - "第", - "叮咚", - "对", - "对于", - "多", - "多少", - "而", - "而况", - "而且", - "而是", - "而外", - "而言", - "而已", - "尔后", - "反过来", - "反过来说", - "反之", - "非但", - "非徒", - "否则", - "嘎", - "嘎登", - "该", - "赶", - "个", - "各", - "各个", - "各位", - "各种", - "各自", - "给", - "根据", - "跟", - "故", - "故此", - "固然", - "关于", - "管", - "归", - "果然", - "果真", - "过", - "哈", - "哈哈", - "呵", - "和", - "何", - "何处", - "何况", - "何时", - "嘿", - "哼", - "哼唷", - "呼哧", - "乎", - "哗", - "还是", - "还有", - "换句话说", - "换言之", - "或", - "或是", - "或者", - "极了", - "及", - "及其", - "及至", - "即", - "即便", - "即或", - "即令", - "即若", - "即使", - "几", - "几时", - "己", - "既", - "既然", - "既是", - "继而", - "加之", - "假如", - "假若", - "假使", - "鉴于", - "将", - "较", - "较之", - "叫", - "接着", - "结果", - "借", - "紧接着", - "进而", - "尽", - "尽管", - "经", - "经过", - "就", - "就是", - "就是说", - "据", - "具体地说", - "具体说来", - "开始", - "开外", - "靠", - "咳", - "可", - "可见", - "可是", - "可以", - "况且", - "啦", - "来", - "来着", - "离", - "例如", - "哩", - "连", - "连同", - "两者", - "了", - "临", - "另", - "另外", - "另一方面", - "论", - "嘛", - "吗", - "慢说", - "漫说", - "冒", - "么", - "每", - "每当", - "们", - "莫若", - "某", - "某个", - "某些", - "拿", - "哪", - "哪边", - "哪儿", - "哪个", - "哪里", - "哪年", - "哪怕", - "哪天", - "哪些", - "哪样", - "那", - "那边", - "那儿", - "那个", - "那会儿", - "那里", - "那么", - "那么些", - "那么样", - "那时", - "那些", - "那样", - "乃", - "乃至", - "呢", - "能", - "你", - "你们", - "您", - "宁", - "宁可", - "宁肯", - "宁愿", - "哦", - "呕", - "啪达", - "旁人", - "呸", - "凭", - "凭借", - "其", - "其次", - "其二", - "其他", - "其它", - "其一", - "其余", - "其中", - "起", - "起见", - "岂但", - "恰恰相反", - "前后", - "前者", - "且", - "然而", - "然后", - "然则", - "让", - "人家", - "任", - "任何", - "任凭", - "如", - "如此", - "如果", - "如何", - "如其", - "如若", - "如上所述", - "若", - "若非", - "若是", - "啥", - "上下", - "尚且", - "设若", - "设使", - "甚而", - "甚么", - "甚至", - "省得", - "时候", - "什么", - "什么样", - "使得", - "是", - "是的", - "首先", - "谁", - "谁知", - "顺", - "顺着", - "似的", - "虽", - "虽然", - "虽说", - "虽则", - "随", - "随着", - "所", - "所以", - "他", - "他们", - "他人", - "它", - "它们", - "她", - "她们", - "倘", - "倘或", - "倘然", - "倘若", - "倘使", - "腾", - "替", - "通过", - "同", - "同时", - "哇", - "万一", - "往", - "望", - "为", - "为何", - "为了", - "为什么", - "为着", - "喂", - "嗡嗡", - "我", - "我们", - "呜", - "呜呼", - "乌乎", - "无论", - "无宁", - "毋宁", - "嘻", - "吓", - "相对而言", - "像", - "向", - "向着", - "嘘", - "呀", - "焉", - "沿", - "沿着", - "要", - "要不", - "要不然", - "要不是", - "要么", - "要是", - "也", - "也罢", - "也好", - "一", - "一般", - "一旦", - "一方面", - "一来", - "一切", - "一样", - "一则", - "依", - "依照", - "矣", - "以", - "以便", - "以及", - "以免", - "以至", - "以至于", - "以致", - "抑或", - "因", - "因此", - "因而", - "因为", - "哟", - "用", - "由", - "由此可见", - "由于", - "有", - "有的", - "有关", - "有些", - "又", - "于", - "于是", - "于是乎", - "与", - "与此同时", - "与否", - "与其", - "越是", - "云云", - "哉", - "再说", - "再者", - "在", - "在下", - "咱", - "咱们", - "则", - "怎", - "怎么", - "怎么办", - "怎么样", - "怎样", - "咋", - "照", - "照着", - "者", - "这", - "这边", - "这儿", - "这个", - "这会儿", - "这就是说", - "这里", - "这么", - "这么点儿", - "这么些", - "这么样", - "这时", - "这些", - "这样", - "正如", - "吱", - "之", - "之类", - "之所以", - "之一", - "只是", - "只限", - "只要", - "只有", - "至", - "至于", - "诸位", - "着", - "着呢", - "自", - "自从", - "自个儿", - "自各儿", - "自己", - "自家", - "自身", - "综上所述", - "总的来看", - "总的来说", - "总的说来", - "总而言之", - "总之", - "纵", - "纵令", - "纵然", - "纵使", - "遵照", - "作为", - "兮", - "呃", - "呗", - "咚", - "咦", - "喏", - "啐", - "喔唷", - "嗬", - "嗯", - "嗳", - "~", - "!", - ".", - ":", - '"', - "'", - "(", - ")", - "*", - "A", - "白", - "社会主义", - "--", - "..", - ">>", - " [", - " ]", - "", - "<", - ">", - "/", - "\\", - "|", - "-", - "_", - "+", - "=", - "&", - "^", - "%", - "#", - "@", - "`", - ";", - "$", - "(", - ")", - "——", - "—", - "¥", - "·", - "...", - "‘", - "’", - "〉", - "〈", - "…", - " ", - "0", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "二", - "三", - "四", - "五", - "六", - "七", - "八", - "九", - "零", - ">", - "<", - "@", - "#", - "$", - "%", - "︿", - "&", - "*", - "+", - "~", - "|", - "[", - "]", - "{", - "}", - "啊哈", - "啊呀", - "啊哟", - "挨次", - "挨个", - "挨家挨户", - "挨门挨户", - "挨门逐户", - "挨着", - "按理", - "按期", - "按时", - "按说", - "暗地里", - "暗中", - "暗自", - "昂然", - "八成", - "白白", - "半", - "梆", - "保管", - "保险", - "饱", - "背地里", - "背靠背", - "倍感", - "倍加", - "本人", - "本身", - "甭", - "比起", - "比如说", - "比照", - "毕竟", - "必", - "必定", - "必将", - "必须", - "便", - "别人", - "并非", - "并肩", - "并没", - "并没有", - "并排", - "并无", - "勃然", - "不", - "不必", - "不常", - "不大", - "不但...而且", - "不得", - "不得不", - "不得了", - "不得已", - "不迭", - "不定", - "不对", - "不妨", - "不管怎样", - "不会", - "不仅...而且", - "不仅仅", - "不仅仅是", - "不经意", - "不可开交", - "不可抗拒", - "不力", - "不了", - "不料", - "不满", - "不免", - "不能不", - "不起", - "不巧", - "不然的话", - "不日", - "不少", - "不胜", - "不时", - "不是", - "不同", - "不能", - "不要", - "不外", - "不外乎", - "不下", - "不限", - "不消", - "不已", - "不亦乐乎", - "不由得", - "不再", - "不择手段", - "不怎么", - "不曾", - "不知不觉", - "不止", - "不止一次", - "不至于", - "才", - "才能", - "策略地", - "差不多", - "差一点", - "常", - "常常", - "常言道", - "常言说", - "常言说得好", - "长此下去", - "长话短说", - "长期以来", - "长线", - "敞开儿", - "彻夜", - "陈年", - "趁便", - "趁机", - "趁热", - "趁势", - "趁早", - "成年", - "成年累月", - "成心", - "乘机", - "乘胜", - "乘势", - "乘隙", - "乘虚", - "诚然", - "迟早", - "充分", - "充其极", - "充其量", - "抽冷子", - "臭", - "初", - "出", - "出来", - "出去", - "除此", - "除此而外", - "除此以外", - "除开", - "除去", - "除却", - "除外", - "处处", - "川流不息", - "传", - "传说", - "传闻", - "串行", - "纯", - "纯粹", - "此后", - "此中", - "次第", - "匆匆", - "从不", - "从此", - "从此以后", - "从古到今", - "从古至今", - "从今以后", - "从宽", - "从来", - "从轻", - "从速", - "从头", - "从未", - "从无到有", - "从小", - "从新", - "从严", - "从优", - "从早到晚", - "从中", - "从重", - "凑巧", - "粗", - "存心", - "达旦", - "打从", - "打开天窗说亮话", - "大", - "大不了", - "大大", - "大抵", - "大都", - "大多", - "大凡", - "大概", - "大家", - "大举", - "大略", - "大面儿上", - "大事", - "大体", - "大体上", - "大约", - "大张旗鼓", - "大致", - "呆呆地", - "带", - "殆", - "待到", - "单", - "单纯", - "单单", - "但愿", - "弹指之间", - "当场", - "当儿", - "当即", - "当口儿", - "当然", - "当庭", - "当头", - "当下", - "当真", - "当中", - "倒不如", - "倒不如说", - "倒是", - "到处", - "到底", - "到了儿", - "到目前为止", - "到头", - "到头来", - "得起", - "得天独厚", - "的确", - "等到", - "叮当", - "顶多", - "定", - "动不动", - "动辄", - "陡然", - "都", - "独", - "独自", - "断然", - "顿时", - "多次", - "多多", - "多多少少", - "多多益善", - "多亏", - "多年来", - "多年前", - "而后", - "而论", - "而又", - "尔等", - "二话不说", - "二话没说", - "反倒", - "反倒是", - "反而", - "反手", - "反之亦然", - "反之则", - "方", - "方才", - "方能", - "放量", - "非常", - "非得", - "分期", - "分期分批", - "分头", - "奋勇", - "愤然", - "风雨无阻", - "逢", - "弗", - "甫", - "嘎嘎", - "该当", - "概", - "赶快", - "赶早不赶晚", - "敢", - "敢情", - "敢于", - "刚", - "刚才", - "刚好", - "刚巧", - "高低", - "格外", - "隔日", - "隔夜", - "个人", - "各式", - "更", - "更加", - "更进一步", - "更为", - "公然", - "共", - "共总", - "够瞧的", - "姑且", - "古来", - "故而", - "故意", - "固", - "怪", - "怪不得", - "惯常", - "光", - "光是", - "归根到底", - "归根结底", - "过于", - "毫不", - "毫无", - "毫无保留地", - "毫无例外", - "好在", - "何必", - "何尝", - "何妨", - "何苦", - "何乐而不为", - "何须", - "何止", - "很", - "很多", - "很少", - "轰然", - "后来", - "呼啦", - "忽地", - "忽然", - "互", - "互相", - "哗啦", - "话说", - "还", - "恍然", - "会", - "豁然", - "活", - "伙同", - "或多或少", - "或许", - "基本", - "基本上", - "基于", - "极", - "极大", - "极度", - "极端", - "极力", - "极其", - "极为", - "急匆匆", - "即将", - "即刻", - "即是说", - "几度", - "几番", - "几乎", - "几经", - "既...又", - "继之", - "加上", - "加以", - "间或", - "简而言之", - "简言之", - "简直", - "见", - "将才", - "将近", - "将要", - "交口", - "较比", - "较为", - "接连不断", - "接下来", - "皆可", - "截然", - "截至", - "藉以", - "借此", - "借以", - "届时", - "仅", - "仅仅", - "谨", - "进来", - "进去", - "近", - "近几年来", - "近来", - "近年来", - "尽管如此", - "尽可能", - "尽快", - "尽量", - "尽然", - "尽如人意", - "尽心竭力", - "尽心尽力", - "尽早", - "精光", - "经常", - "竟", - "竟然", - "究竟", - "就此", - "就地", - "就算", - "居然", - "局外", - "举凡", - "据称", - "据此", - "据实", - "据说", - "据我所知", - "据悉", - "具体来说", - "决不", - "决非", - "绝", - "绝不", - "绝顶", - "绝对", - "绝非", - "均", - "喀", - "看", - "看来", - "看起来", - "看上去", - "看样子", - "可好", - "可能", - "恐怕", - "快", - "快要", - "来不及", - "来得及", - "来讲", - "来看", - "拦腰", - "牢牢", - "老", - "老大", - "老老实实", - "老是", - "累次", - "累年", - "理当", - "理该", - "理应", - "历", - "立", - "立地", - "立刻", - "立马", - "立时", - "联袂", - "连连", - "连日", - "连日来", - "连声", - "连袂", - "临到", - "另方面", - "另行", - "另一个", - "路经", - "屡", - "屡次", - "屡次三番", - "屡屡", - "缕缕", - "率尔", - "率然", - "略", - "略加", - "略微", - "略为", - "论说", - "马上", - "蛮", - "满", - "没", - "没有", - "每逢", - "每每", - "每时每刻", - "猛然", - "猛然间", - "莫", - "莫不", - "莫非", - "莫如", - "默默地", - "默然", - "呐", - "那末", - "奈", - "难道", - "难得", - "难怪", - "难说", - "内", - "年复一年", - "凝神", - "偶而", - "偶尔", - "怕", - "砰", - "碰巧", - "譬如", - "偏偏", - "乒", - "平素", - "颇", - "迫于", - "扑通", - "其后", - "其实", - "奇", - "齐", - "起初", - "起来", - "起首", - "起头", - "起先", - "岂", - "岂非", - "岂止", - "迄", - "恰逢", - "恰好", - "恰恰", - "恰巧", - "恰如", - "恰似", - "千", - "千万", - "千万千万", - "切", - "切不可", - "切莫", - "切切", - "切勿", - "窃", - "亲口", - "亲身", - "亲手", - "亲眼", - "亲自", - "顷", - "顷刻", - "顷刻间", - "顷刻之间", - "请勿", - "穷年累月", - "取道", - "去", - "权时", - "全都", - "全力", - "全年", - "全然", - "全身心", - "然", - "人人", - "仍", - "仍旧", - "仍然", - "日复一日", - "日见", - "日渐", - "日益", - "日臻", - "如常", - "如此等等", - "如次", - "如今", - "如期", - "如前所述", - "如上", - "如下", - "汝", - "三番两次", - "三番五次", - "三天两头", - "瑟瑟", - "沙沙", - "上", - "上来", - "上去", - "一个", - "月", - "日", - "\n", -} +STOPWORDS: frozenset[str] = frozenset( + ( + "during", + "when", + "but", + "then", + "further", + "isn", + "mustn't", + "until", + "own", + "i", + "couldn", + "y", + "only", + "you've", + "ours", + "who", + "where", + "ourselves", + "has", + "to", + "was", + "didn't", + "themselves", + "if", + "against", + "through", + "her", + "an", + "your", + "can", + "those", + "didn", + "about", + "aren't", + "shan't", + "be", + "not", + "these", + "again", + "so", + "t", + "theirs", + "weren", + "won't", + "won", + "itself", + "just", + "same", + "while", + "why", + "doesn", + "aren", + "him", + "haven", + "for", + "you'll", + "that", + "we", + "am", + "d", + "by", + "having", + "wasn't", + "than", + "weren't", + "out", + "from", + "now", + "their", + "too", + "hadn", + "o", + "needn", + "most", + "it", + "under", + "needn't", + "any", + "some", + "few", + "ll", + "hers", + "which", + "m", + "you're", + "off", + "other", + "had", + "she", + "you'd", + "do", + "you", + "does", + "s", + "will", + "each", + "wouldn't", + "hasn't", + "such", + "more", + "whom", + "she's", + "my", + "yours", + "yourself", + "of", + "on", + "very", + "hadn't", + "with", + "yourselves", + "been", + "ma", + "them", + "mightn't", + "shan", + "mustn", + "they", + "what", + "both", + "that'll", + "how", + "is", + "he", + "because", + "down", + "haven't", + "are", + "no", + "it's", + "our", + "being", + "the", + "or", + "above", + "myself", + "once", + "don't", + "doesn't", + "as", + "nor", + "here", + "herself", + "hasn", + "mightn", + "have", + "its", + "all", + "were", + "ain", + "this", + "at", + "after", + "over", + "shouldn't", + "into", + "before", + "don", + "wouldn", + "re", + "couldn't", + "wasn", + "in", + "should", + "there", + "himself", + "isn't", + "should've", + "doing", + "ve", + "shouldn", + "a", + "did", + "and", + "his", + "between", + "me", + "up", + "below", + "人民", + "末##末", + "啊", + "阿", + "哎", + "哎呀", + "哎哟", + "唉", + "俺", + "俺们", + "按", + "按照", + "吧", + "吧哒", + "把", + "罢了", + "被", + "本", + "本着", + "比", + "比方", + "比如", + "鄙人", + "彼", + "彼此", + "边", + "别", + "别的", + "别说", + "并", + "并且", + "不比", + "不成", + "不单", + "不但", + "不独", + "不管", + "不光", + "不过", + "不仅", + "不拘", + "不论", + "不怕", + "不然", + "不如", + "不特", + "不惟", + "不问", + "不只", + "朝", + "朝着", + "趁", + "趁着", + "乘", + "冲", + "除", + "除此之外", + "除非", + "除了", + "此", + "此间", + "此外", + "从", + "从而", + "打", + "待", + "但", + "但是", + "当", + "当着", + "到", + "得", + "的", + "的话", + "等", + "等等", + "地", + "第", + "叮咚", + "对", + "对于", + "多", + "多少", + "而", + "而况", + "而且", + "而是", + "而外", + "而言", + "而已", + "尔后", + "反过来", + "反过来说", + "反之", + "非但", + "非徒", + "否则", + "嘎", + "嘎登", + "该", + "赶", + "个", + "各", + "各个", + "各位", + "各种", + "各自", + "给", + "根据", + "跟", + "故", + "故此", + "固然", + "关于", + "管", + "归", + "果然", + "果真", + "过", + "哈", + "哈哈", + "呵", + "和", + "何", + "何处", + "何况", + "何时", + "嘿", + "哼", + "哼唷", + "呼哧", + "乎", + "哗", + "还是", + "还有", + "换句话说", + "换言之", + "或", + "或是", + "或者", + "极了", + "及", + "及其", + "及至", + "即", + "即便", + "即或", + "即令", + "即若", + "即使", + "几", + "几时", + "己", + "既", + "既然", + "既是", + "继而", + "加之", + "假如", + "假若", + "假使", + "鉴于", + "将", + "较", + "较之", + "叫", + "接着", + "结果", + "借", + "紧接着", + "进而", + "尽", + "尽管", + "经", + "经过", + "就", + "就是", + "就是说", + "据", + "具体地说", + "具体说来", + "开始", + "开外", + "靠", + "咳", + "可", + "可见", + "可是", + "可以", + "况且", + "啦", + "来", + "来着", + "离", + "例如", + "哩", + "连", + "连同", + "两者", + "了", + "临", + "另", + "另外", + "另一方面", + "论", + "嘛", + "吗", + "慢说", + "漫说", + "冒", + "么", + "每", + "每当", + "们", + "莫若", + "某", + "某个", + "某些", + "拿", + "哪", + "哪边", + "哪儿", + "哪个", + "哪里", + "哪年", + "哪怕", + "哪天", + "哪些", + "哪样", + "那", + "那边", + "那儿", + "那个", + "那会儿", + "那里", + "那么", + "那么些", + "那么样", + "那时", + "那些", + "那样", + "乃", + "乃至", + "呢", + "能", + "你", + "你们", + "您", + "宁", + "宁可", + "宁肯", + "宁愿", + "哦", + "呕", + "啪达", + "旁人", + "呸", + "凭", + "凭借", + "其", + "其次", + "其二", + "其他", + "其它", + "其一", + "其余", + "其中", + "起", + "起见", + "岂但", + "恰恰相反", + "前后", + "前者", + "且", + "然而", + "然后", + "然则", + "让", + "人家", + "任", + "任何", + "任凭", + "如", + "如此", + "如果", + "如何", + "如其", + "如若", + "如上所述", + "若", + "若非", + "若是", + "啥", + "上下", + "尚且", + "设若", + "设使", + "甚而", + "甚么", + "甚至", + "省得", + "时候", + "什么", + "什么样", + "使得", + "是", + "是的", + "首先", + "谁", + "谁知", + "顺", + "顺着", + "似的", + "虽", + "虽然", + "虽说", + "虽则", + "随", + "随着", + "所", + "所以", + "他", + "他们", + "他人", + "它", + "它们", + "她", + "她们", + "倘", + "倘或", + "倘然", + "倘若", + "倘使", + "腾", + "替", + "通过", + "同", + "同时", + "哇", + "万一", + "往", + "望", + "为", + "为何", + "为了", + "为什么", + "为着", + "喂", + "嗡嗡", + "我", + "我们", + "呜", + "呜呼", + "乌乎", + "无论", + "无宁", + "毋宁", + "嘻", + "吓", + "相对而言", + "像", + "向", + "向着", + "嘘", + "呀", + "焉", + "沿", + "沿着", + "要", + "要不", + "要不然", + "要不是", + "要么", + "要是", + "也", + "也罢", + "也好", + "一", + "一般", + "一旦", + "一方面", + "一来", + "一切", + "一样", + "一则", + "依", + "依照", + "矣", + "以", + "以便", + "以及", + "以免", + "以至", + "以至于", + "以致", + "抑或", + "因", + "因此", + "因而", + "因为", + "哟", + "用", + "由", + "由此可见", + "由于", + "有", + "有的", + "有关", + "有些", + "又", + "于", + "于是", + "于是乎", + "与", + "与此同时", + "与否", + "与其", + "越是", + "云云", + "哉", + "再说", + "再者", + "在", + "在下", + "咱", + "咱们", + "则", + "怎", + "怎么", + "怎么办", + "怎么样", + "怎样", + "咋", + "照", + "照着", + "者", + "这", + "这边", + "这儿", + "这个", + "这会儿", + "这就是说", + "这里", + "这么", + "这么点儿", + "这么些", + "这么样", + "这时", + "这些", + "这样", + "正如", + "吱", + "之", + "之类", + "之所以", + "之一", + "只是", + "只限", + "只要", + "只有", + "至", + "至于", + "诸位", + "着", + "着呢", + "自", + "自从", + "自个儿", + "自各儿", + "自己", + "自家", + "自身", + "综上所述", + "总的来看", + "总的来说", + "总的说来", + "总而言之", + "总之", + "纵", + "纵令", + "纵然", + "纵使", + "遵照", + "作为", + "兮", + "呃", + "呗", + "咚", + "咦", + "喏", + "啐", + "喔唷", + "嗬", + "嗯", + "嗳", + "~", + "!", + ".", + ":", + '"', + "'", + "(", + ")", + "*", + "A", + "白", + "社会主义", + "--", + "..", + ">>", + " [", + " ]", + "", + "<", + ">", + "/", + "\\", + "|", + "-", + "_", + "+", + "=", + "&", + "^", + "%", + "#", + "@", + "`", + ";", + "$", + "(", + ")", + "——", + "—", + "¥", + "·", + "...", + "‘", + "’", + "〉", + "〈", + "…", + " ", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "二", + "三", + "四", + "五", + "六", + "七", + "八", + "九", + "零", + ">", + "<", + "@", + "#", + "$", + "%", + "︿", + "&", + "*", + "+", + "~", + "|", + "[", + "]", + "{", + "}", + "啊哈", + "啊呀", + "啊哟", + "挨次", + "挨个", + "挨家挨户", + "挨门挨户", + "挨门逐户", + "挨着", + "按理", + "按期", + "按时", + "按说", + "暗地里", + "暗中", + "暗自", + "昂然", + "八成", + "白白", + "半", + "梆", + "保管", + "保险", + "饱", + "背地里", + "背靠背", + "倍感", + "倍加", + "本人", + "本身", + "甭", + "比起", + "比如说", + "比照", + "毕竟", + "必", + "必定", + "必将", + "必须", + "便", + "别人", + "并非", + "并肩", + "并没", + "并没有", + "并排", + "并无", + "勃然", + "不", + "不必", + "不常", + "不大", + "不但...而且", + "不得", + "不得不", + "不得了", + "不得已", + "不迭", + "不定", + "不对", + "不妨", + "不管怎样", + "不会", + "不仅...而且", + "不仅仅", + "不仅仅是", + "不经意", + "不可开交", + "不可抗拒", + "不力", + "不了", + "不料", + "不满", + "不免", + "不能不", + "不起", + "不巧", + "不然的话", + "不日", + "不少", + "不胜", + "不时", + "不是", + "不同", + "不能", + "不要", + "不外", + "不外乎", + "不下", + "不限", + "不消", + "不已", + "不亦乐乎", + "不由得", + "不再", + "不择手段", + "不怎么", + "不曾", + "不知不觉", + "不止", + "不止一次", + "不至于", + "才", + "才能", + "策略地", + "差不多", + "差一点", + "常", + "常常", + "常言道", + "常言说", + "常言说得好", + "长此下去", + "长话短说", + "长期以来", + "长线", + "敞开儿", + "彻夜", + "陈年", + "趁便", + "趁机", + "趁热", + "趁势", + "趁早", + "成年", + "成年累月", + "成心", + "乘机", + "乘胜", + "乘势", + "乘隙", + "乘虚", + "诚然", + "迟早", + "充分", + "充其极", + "充其量", + "抽冷子", + "臭", + "初", + "出", + "出来", + "出去", + "除此", + "除此而外", + "除此以外", + "除开", + "除去", + "除却", + "除外", + "处处", + "川流不息", + "传", + "传说", + "传闻", + "串行", + "纯", + "纯粹", + "此后", + "此中", + "次第", + "匆匆", + "从不", + "从此", + "从此以后", + "从古到今", + "从古至今", + "从今以后", + "从宽", + "从来", + "从轻", + "从速", + "从头", + "从未", + "从无到有", + "从小", + "从新", + "从严", + "从优", + "从早到晚", + "从中", + "从重", + "凑巧", + "粗", + "存心", + "达旦", + "打从", + "打开天窗说亮话", + "大", + "大不了", + "大大", + "大抵", + "大都", + "大多", + "大凡", + "大概", + "大家", + "大举", + "大略", + "大面儿上", + "大事", + "大体", + "大体上", + "大约", + "大张旗鼓", + "大致", + "呆呆地", + "带", + "殆", + "待到", + "单", + "单纯", + "单单", + "但愿", + "弹指之间", + "当场", + "当儿", + "当即", + "当口儿", + "当然", + "当庭", + "当头", + "当下", + "当真", + "当中", + "倒不如", + "倒不如说", + "倒是", + "到处", + "到底", + "到了儿", + "到目前为止", + "到头", + "到头来", + "得起", + "得天独厚", + "的确", + "等到", + "叮当", + "顶多", + "定", + "动不动", + "动辄", + "陡然", + "都", + "独", + "独自", + "断然", + "顿时", + "多次", + "多多", + "多多少少", + "多多益善", + "多亏", + "多年来", + "多年前", + "而后", + "而论", + "而又", + "尔等", + "二话不说", + "二话没说", + "反倒", + "反倒是", + "反而", + "反手", + "反之亦然", + "反之则", + "方", + "方才", + "方能", + "放量", + "非常", + "非得", + "分期", + "分期分批", + "分头", + "奋勇", + "愤然", + "风雨无阻", + "逢", + "弗", + "甫", + "嘎嘎", + "该当", + "概", + "赶快", + "赶早不赶晚", + "敢", + "敢情", + "敢于", + "刚", + "刚才", + "刚好", + "刚巧", + "高低", + "格外", + "隔日", + "隔夜", + "个人", + "各式", + "更", + "更加", + "更进一步", + "更为", + "公然", + "共", + "共总", + "够瞧的", + "姑且", + "古来", + "故而", + "故意", + "固", + "怪", + "怪不得", + "惯常", + "光", + "光是", + "归根到底", + "归根结底", + "过于", + "毫不", + "毫无", + "毫无保留地", + "毫无例外", + "好在", + "何必", + "何尝", + "何妨", + "何苦", + "何乐而不为", + "何须", + "何止", + "很", + "很多", + "很少", + "轰然", + "后来", + "呼啦", + "忽地", + "忽然", + "互", + "互相", + "哗啦", + "话说", + "还", + "恍然", + "会", + "豁然", + "活", + "伙同", + "或多或少", + "或许", + "基本", + "基本上", + "基于", + "极", + "极大", + "极度", + "极端", + "极力", + "极其", + "极为", + "急匆匆", + "即将", + "即刻", + "即是说", + "几度", + "几番", + "几乎", + "几经", + "既...又", + "继之", + "加上", + "加以", + "间或", + "简而言之", + "简言之", + "简直", + "见", + "将才", + "将近", + "将要", + "交口", + "较比", + "较为", + "接连不断", + "接下来", + "皆可", + "截然", + "截至", + "藉以", + "借此", + "借以", + "届时", + "仅", + "仅仅", + "谨", + "进来", + "进去", + "近", + "近几年来", + "近来", + "近年来", + "尽管如此", + "尽可能", + "尽快", + "尽量", + "尽然", + "尽如人意", + "尽心竭力", + "尽心尽力", + "尽早", + "精光", + "经常", + "竟", + "竟然", + "究竟", + "就此", + "就地", + "就算", + "居然", + "局外", + "举凡", + "据称", + "据此", + "据实", + "据说", + "据我所知", + "据悉", + "具体来说", + "决不", + "决非", + "绝", + "绝不", + "绝顶", + "绝对", + "绝非", + "均", + "喀", + "看", + "看来", + "看起来", + "看上去", + "看样子", + "可好", + "可能", + "恐怕", + "快", + "快要", + "来不及", + "来得及", + "来讲", + "来看", + "拦腰", + "牢牢", + "老", + "老大", + "老老实实", + "老是", + "累次", + "累年", + "理当", + "理该", + "理应", + "历", + "立", + "立地", + "立刻", + "立马", + "立时", + "联袂", + "连连", + "连日", + "连日来", + "连声", + "连袂", + "临到", + "另方面", + "另行", + "另一个", + "路经", + "屡", + "屡次", + "屡次三番", + "屡屡", + "缕缕", + "率尔", + "率然", + "略", + "略加", + "略微", + "略为", + "论说", + "马上", + "蛮", + "满", + "没", + "没有", + "每逢", + "每每", + "每时每刻", + "猛然", + "猛然间", + "莫", + "莫不", + "莫非", + "莫如", + "默默地", + "默然", + "呐", + "那末", + "奈", + "难道", + "难得", + "难怪", + "难说", + "内", + "年复一年", + "凝神", + "偶而", + "偶尔", + "怕", + "砰", + "碰巧", + "譬如", + "偏偏", + "乒", + "平素", + "颇", + "迫于", + "扑通", + "其后", + "其实", + "奇", + "齐", + "起初", + "起来", + "起首", + "起头", + "起先", + "岂", + "岂非", + "岂止", + "迄", + "恰逢", + "恰好", + "恰恰", + "恰巧", + "恰如", + "恰似", + "千", + "千万", + "千万千万", + "切", + "切不可", + "切莫", + "切切", + "切勿", + "窃", + "亲口", + "亲身", + "亲手", + "亲眼", + "亲自", + "顷", + "顷刻", + "顷刻间", + "顷刻之间", + "请勿", + "穷年累月", + "取道", + "去", + "权时", + "全都", + "全力", + "全年", + "全然", + "全身心", + "然", + "人人", + "仍", + "仍旧", + "仍然", + "日复一日", + "日见", + "日渐", + "日益", + "日臻", + "如常", + "如此等等", + "如次", + "如今", + "如期", + "如前所述", + "如上", + "如下", + "汝", + "三番两次", + "三番五次", + "三天两头", + "瑟瑟", + "沙沙", + "上", + "上来", + "上去", + "一个", + "月", + "日", + "\n", + ) +) diff --git a/api/core/rag/datasource/vdb/myscale/myscale_vector.py b/api/core/rag/datasource/vdb/myscale/myscale_vector.py index 17aac25b87..6c62671380 100644 --- a/api/core/rag/datasource/vdb/myscale/myscale_vector.py +++ b/api/core/rag/datasource/vdb/myscale/myscale_vector.py @@ -4,7 +4,7 @@ import uuid from enum import StrEnum from typing import Any -from clickhouse_connect import get_client +from clickhouse_connect import get_client # type: ignore[import-untyped] from pydantic import BaseModel from configs import dify_config diff --git a/api/core/rag/extractor/pdf_extractor.py b/api/core/rag/extractor/pdf_extractor.py index 9abdb31325..02f0efc908 100644 --- a/api/core/rag/extractor/pdf_extractor.py +++ b/api/core/rag/extractor/pdf_extractor.py @@ -35,7 +35,7 @@ class PdfExtractor(BaseExtractor): """ # Magic bytes for image format detection: (magic_bytes, extension, mime_type) - IMAGE_FORMATS = [ + IMAGE_FORMATS: tuple[tuple[bytes, str, str], ...] = ( (b"\xff\xd8\xff", "jpg", "image/jpeg"), (b"\x89PNG\r\n\x1a\n", "png", "image/png"), (b"\x00\x00\x00\x0c\x6a\x50\x20\x20\x0d\x0a\x87\x0a", "jp2", "image/jp2"), @@ -45,7 +45,7 @@ class PdfExtractor(BaseExtractor): (b"MM\x00*", "tiff", "image/tiff"), (b"II+\x00", "tiff", "image/tiff"), (b"MM\x00+", "tiff", "image/tiff"), - ] + ) MAX_MAGIC_LEN = max(len(m) for m, _, _ in IMAGE_FORMATS) def __init__(self, file_path: str, tenant_id: str, user_id: str, file_cache_key: str | None = None): diff --git a/api/core/trigger/constants.py b/api/core/trigger/constants.py index 192faa2d3e..4047e9bc88 100644 --- a/api/core/trigger/constants.py +++ b/api/core/trigger/constants.py @@ -5,11 +5,11 @@ TRIGGER_SCHEDULE_NODE_TYPE: Final[str] = "trigger-schedule" TRIGGER_PLUGIN_NODE_TYPE: Final[str] = "trigger-plugin" TRIGGER_NODE_TYPES: Final[frozenset[str]] = frozenset( - { + ( TRIGGER_WEBHOOK_NODE_TYPE, TRIGGER_SCHEDULE_NODE_TYPE, TRIGGER_PLUGIN_NODE_TYPE, - } + ) ) diff --git a/api/core/workflow/nodes/trigger_webhook/entities.py b/api/core/workflow/nodes/trigger_webhook/entities.py index 4d5ad72154..a30f877e4b 100644 --- a/api/core/workflow/nodes/trigger_webhook/entities.py +++ b/api/core/workflow/nodes/trigger_webhook/entities.py @@ -8,24 +8,20 @@ from pydantic import BaseModel, Field, field_validator from core.trigger.constants import TRIGGER_WEBHOOK_NODE_TYPE -_WEBHOOK_HEADER_ALLOWED_TYPES = frozenset( - { - SegmentType.STRING, - } -) +_WEBHOOK_HEADER_ALLOWED_TYPES: frozenset[SegmentType] = frozenset((SegmentType.STRING,)) -_WEBHOOK_QUERY_PARAMETER_ALLOWED_TYPES = frozenset( - { +_WEBHOOK_QUERY_PARAMETER_ALLOWED_TYPES: frozenset[SegmentType] = frozenset( + ( SegmentType.STRING, SegmentType.NUMBER, SegmentType.BOOLEAN, - } + ) ) _WEBHOOK_PARAMETER_ALLOWED_TYPES = _WEBHOOK_HEADER_ALLOWED_TYPES | _WEBHOOK_QUERY_PARAMETER_ALLOWED_TYPES -_WEBHOOK_BODY_ALLOWED_TYPES = frozenset( - { +_WEBHOOK_BODY_ALLOWED_TYPES: frozenset[SegmentType] = frozenset( + ( SegmentType.STRING, SegmentType.NUMBER, SegmentType.BOOLEAN, @@ -35,7 +31,7 @@ _WEBHOOK_BODY_ALLOWED_TYPES = frozenset( SegmentType.ARRAY_BOOLEAN, SegmentType.ARRAY_OBJECT, SegmentType.FILE, - } + ) ) diff --git a/api/libs/collection_utils.py b/api/libs/collection_utils.py index f97308ca44..7054fe401e 100644 --- a/api/libs/collection_utils.py +++ b/api/libs/collection_utils.py @@ -1,9 +1,12 @@ -def convert_to_lower_and_upper_set(inputs: list[str] | set[str]) -> set[str]: +from collections.abc import Collection + + +def convert_to_lower_and_upper_set(inputs: Collection[str]) -> set[str]: """ - Convert a list or set of strings to a set containing both lower and upper case versions of each string. + Convert a collection of strings to a set containing both lower and upper case versions of each string. Args: - inputs (list[str] | set[str]): A list or set of strings to be converted. + inputs (Collection[str]): A collection of strings to be converted. Returns: set[str]: A set containing both lower and upper case versions of each string. diff --git a/api/models/workflow.py b/api/models/workflow.py index f8868cb73c..1063016370 100644 --- a/api/models/workflow.py +++ b/api/models/workflow.py @@ -1386,7 +1386,7 @@ class ConversationVariable(TypeBase): # Only `sys.query` and `sys.files` could be modified. -_EDITABLE_SYSTEM_VARIABLE = frozenset(["query", "files"]) +_EDITABLE_SYSTEM_VARIABLE = frozenset(("query", "files")) class WorkflowDraftVariable(Base): diff --git a/api/services/workflow_draft_variable_service.py b/api/services/workflow_draft_variable_service.py index 98e338a2d4..9ed60bf86b 100644 --- a/api/services/workflow_draft_variable_service.py +++ b/api/services/workflow_draft_variable_service.py @@ -800,8 +800,8 @@ class DraftVariableSaver: # technical variables from being exposed in the draft environment, particularly those # that aren't meant to be directly edited or viewed by users. _EXCLUDE_VARIABLE_NAMES_MAPPING: dict[NodeType, frozenset[str]] = { - BuiltinNodeTypes.LLM: frozenset(["finish_reason"]), - BuiltinNodeTypes.LOOP: frozenset(["loop_round"]), + BuiltinNodeTypes.LLM: frozenset(("finish_reason",)), + BuiltinNodeTypes.LOOP: frozenset(("loop_round",)), } # Database session used for persisting draft variables. diff --git a/api/tests/unit_tests/core/datasource/test_file_upload.py b/api/tests/unit_tests/core/datasource/test_file_upload.py index 63b86e64fc..c6d6dd5808 100644 --- a/api/tests/unit_tests/core/datasource/test_file_upload.py +++ b/api/tests/unit_tests/core/datasource/test_file_upload.py @@ -1249,9 +1249,9 @@ class TestFileConstants: """ def test_image_extensions_set_properties(self): - """Test that IMAGE_EXTENSIONS set has expected properties.""" - # Assert - Should be a set - assert isinstance(IMAGE_EXTENSIONS, set) + """Test that IMAGE_EXTENSIONS frozenset has expected properties.""" + # Assert - Should be immutable + assert isinstance(IMAGE_EXTENSIONS, frozenset) # Should not be empty assert len(IMAGE_EXTENSIONS) > 0 # Should contain common image formats @@ -1260,9 +1260,9 @@ class TestFileConstants: assert ext in IMAGE_EXTENSIONS or ext.upper() in IMAGE_EXTENSIONS def test_video_extensions_set_properties(self): - """Test that VIDEO_EXTENSIONS set has expected properties.""" - # Assert - Should be a set - assert isinstance(VIDEO_EXTENSIONS, set) + """Test that VIDEO_EXTENSIONS frozenset has expected properties.""" + # Assert - Should be immutable + assert isinstance(VIDEO_EXTENSIONS, frozenset) # Should not be empty assert len(VIDEO_EXTENSIONS) > 0 # Should contain common video formats @@ -1271,9 +1271,9 @@ class TestFileConstants: assert ext in VIDEO_EXTENSIONS or ext.upper() in VIDEO_EXTENSIONS def test_audio_extensions_set_properties(self): - """Test that AUDIO_EXTENSIONS set has expected properties.""" - # Assert - Should be a set - assert isinstance(AUDIO_EXTENSIONS, set) + """Test that AUDIO_EXTENSIONS frozenset has expected properties.""" + # Assert - Should be immutable + assert isinstance(AUDIO_EXTENSIONS, frozenset) # Should not be empty assert len(AUDIO_EXTENSIONS) > 0 # Should contain common audio formats @@ -1282,9 +1282,9 @@ class TestFileConstants: assert ext in AUDIO_EXTENSIONS or ext.upper() in AUDIO_EXTENSIONS def test_document_extensions_set_properties(self): - """Test that DOCUMENT_EXTENSIONS set has expected properties.""" - # Assert - Should be a set - assert isinstance(DOCUMENT_EXTENSIONS, set) + """Test that DOCUMENT_EXTENSIONS frozenset has expected properties.""" + # Assert - Should be immutable + assert isinstance(DOCUMENT_EXTENSIONS, frozenset) # Should not be empty assert len(DOCUMENT_EXTENSIONS) > 0 # Should contain common document formats diff --git a/api/tests/unit_tests/core/rag/datasource/keyword/jieba/test_stopwords.py b/api/tests/unit_tests/core/rag/datasource/keyword/jieba/test_stopwords.py index 1b1541ddd6..4375d854ba 100644 --- a/api/tests/unit_tests/core/rag/datasource/keyword/jieba/test_stopwords.py +++ b/api/tests/unit_tests/core/rag/datasource/keyword/jieba/test_stopwords.py @@ -2,5 +2,6 @@ from core.rag.datasource.keyword.jieba.stopwords import STOPWORDS def test_stopwords_loaded(): + assert isinstance(STOPWORDS, frozenset) assert "during" in STOPWORDS assert "the" in STOPWORDS diff --git a/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py b/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py index a215e9d350..7841bf05ad 100644 --- a/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py @@ -1,4 +1,5 @@ import base64 +import logging import uuid from collections.abc import Sequence from unittest import mock @@ -1261,6 +1262,10 @@ def test_llm_node_image_file_to_markdown(llm_node: LLMNode): class TestSaveMultimodalOutputAndConvertResultToMarkdown: + class _UnknownItem: + def __str__(self) -> str: + return "" + def test_str_content(self, llm_node_for_multimodal): llm_node, mock_file_saver = llm_node_for_multimodal gen = llm_node._save_multimodal_output_and_convert_result_to_markdown( @@ -1330,18 +1335,23 @@ class TestSaveMultimodalOutputAndConvertResultToMarkdown: def test_unknown_content_type(self, llm_node_for_multimodal): llm_node, mock_file_saver = llm_node_for_multimodal gen = llm_node._save_multimodal_output_and_convert_result_to_markdown( - contents=frozenset(["hello world"]), file_saver=mock_file_saver, file_outputs=[] + contents=frozenset(("hello world",)), file_saver=mock_file_saver, file_outputs=[] ) assert list(gen) == ["hello world"] mock_file_saver.save_binary_string.assert_not_called() mock_file_saver.save_remote_url.assert_not_called() - def test_unknown_item_type(self, llm_node_for_multimodal): + def test_unknown_item_type(self, llm_node_for_multimodal, caplog): llm_node, mock_file_saver = llm_node_for_multimodal - gen = llm_node._save_multimodal_output_and_convert_result_to_markdown( - contents=[frozenset(["hello world"])], file_saver=mock_file_saver, file_outputs=[] - ) - assert list(gen) == ["frozenset({'hello world'})"] + unknown_item = self._UnknownItem() + + with caplog.at_level(logging.WARNING, logger="graphon.nodes.llm.node"): + gen = llm_node._save_multimodal_output_and_convert_result_to_markdown( + contents=[unknown_item], file_saver=mock_file_saver, file_outputs=[] + ) + assert list(gen) == [str(unknown_item)] + + assert "unknown item type encountered" in caplog.text mock_file_saver.save_binary_string.assert_not_called() mock_file_saver.save_remote_url.assert_not_called() diff --git a/api/tests/unit_tests/factories/test_variable_factory.py b/api/tests/unit_tests/factories/test_variable_factory.py index 8d573b1154..a06c42507d 100644 --- a/api/tests/unit_tests/factories/test_variable_factory.py +++ b/api/tests/unit_tests/factories/test_variable_factory.py @@ -837,7 +837,7 @@ class TestBuildSegmentValueErrors: self.ValueErrorTestCase( name="frozenset_type", description="frozenset (unsupported type)", - test_value=frozenset([1, 2, 3]), + test_value=frozenset((1, 2, 3)), ), self.ValueErrorTestCase( name="memoryview_type", diff --git a/dev/pytest/pytest_config_tests.py b/dev/pytest/pytest_config_tests.py index 1ae115f85c..d56cceff5e 100644 --- a/dev/pytest/pytest_config_tests.py +++ b/dev/pytest/pytest_config_tests.py @@ -3,89 +3,93 @@ from pathlib import Path import yaml # type: ignore from dotenv import dotenv_values -BASE_API_AND_DOCKER_CONFIG_SET_DIFF = { - "APP_MAX_EXECUTION_TIME", - "BATCH_UPLOAD_LIMIT", - "CELERY_BEAT_SCHEDULER_TIME", - "CODE_EXECUTION_API_KEY", - "HTTP_REQUEST_MAX_CONNECT_TIMEOUT", - "HTTP_REQUEST_MAX_READ_TIMEOUT", - "HTTP_REQUEST_MAX_WRITE_TIMEOUT", - "INNER_API_KEY", - "INNER_API_KEY_FOR_PLUGIN", - "KEYWORD_DATA_SOURCE_TYPE", - "LOGIN_LOCKOUT_DURATION", - "LOG_FORMAT", - "OCI_ACCESS_KEY", - "OCI_BUCKET_NAME", - "OCI_ENDPOINT", - "OCI_REGION", - "OCI_SECRET_KEY", - "PLUGIN_DAEMON_KEY", - "PLUGIN_DAEMON_URL", - "PLUGIN_REMOTE_INSTALL_HOST", - "PLUGIN_REMOTE_INSTALL_PORT", - "REDIS_DB", - "RESEND_API_URL", - "RESPECT_XFORWARD_HEADERS_ENABLED", - "SENTRY_DSN", - "SSRF_DEFAULT_CONNECT_TIME_OUT", - "SSRF_DEFAULT_MAX_RETRIES", - "SSRF_DEFAULT_READ_TIME_OUT", - "SSRF_DEFAULT_TIME_OUT", - "SSRF_DEFAULT_WRITE_TIME_OUT", - "UPSTASH_VECTOR_TOKEN", - "UPSTASH_VECTOR_URL", - "USING_UGC_INDEX", - "WEAVIATE_BATCH_SIZE", -} +BASE_API_AND_DOCKER_CONFIG_SET_DIFF: frozenset[str] = frozenset( + ( + "APP_MAX_EXECUTION_TIME", + "BATCH_UPLOAD_LIMIT", + "CELERY_BEAT_SCHEDULER_TIME", + "CODE_EXECUTION_API_KEY", + "HTTP_REQUEST_MAX_CONNECT_TIMEOUT", + "HTTP_REQUEST_MAX_READ_TIMEOUT", + "HTTP_REQUEST_MAX_WRITE_TIMEOUT", + "INNER_API_KEY", + "INNER_API_KEY_FOR_PLUGIN", + "KEYWORD_DATA_SOURCE_TYPE", + "LOGIN_LOCKOUT_DURATION", + "LOG_FORMAT", + "OCI_ACCESS_KEY", + "OCI_BUCKET_NAME", + "OCI_ENDPOINT", + "OCI_REGION", + "OCI_SECRET_KEY", + "PLUGIN_DAEMON_KEY", + "PLUGIN_DAEMON_URL", + "PLUGIN_REMOTE_INSTALL_HOST", + "PLUGIN_REMOTE_INSTALL_PORT", + "REDIS_DB", + "RESEND_API_URL", + "RESPECT_XFORWARD_HEADERS_ENABLED", + "SENTRY_DSN", + "SSRF_DEFAULT_CONNECT_TIME_OUT", + "SSRF_DEFAULT_MAX_RETRIES", + "SSRF_DEFAULT_READ_TIME_OUT", + "SSRF_DEFAULT_TIME_OUT", + "SSRF_DEFAULT_WRITE_TIME_OUT", + "UPSTASH_VECTOR_TOKEN", + "UPSTASH_VECTOR_URL", + "USING_UGC_INDEX", + "WEAVIATE_BATCH_SIZE", + ) +) -BASE_API_AND_DOCKER_COMPOSE_CONFIG_SET_DIFF = { - "BATCH_UPLOAD_LIMIT", - "CELERY_BEAT_SCHEDULER_TIME", - "HTTP_REQUEST_MAX_CONNECT_TIMEOUT", - "HTTP_REQUEST_MAX_READ_TIMEOUT", - "HTTP_REQUEST_MAX_WRITE_TIMEOUT", - "INNER_API_KEY", - "INNER_API_KEY_FOR_PLUGIN", - "KEYWORD_DATA_SOURCE_TYPE", - "LOGIN_LOCKOUT_DURATION", - "LOG_FORMAT", - "OPENDAL_FS_ROOT", - "OPENDAL_S3_ACCESS_KEY_ID", - "OPENDAL_S3_BUCKET", - "OPENDAL_S3_ENDPOINT", - "OPENDAL_S3_REGION", - "OPENDAL_S3_ROOT", - "OPENDAL_S3_SECRET_ACCESS_KEY", - "OPENDAL_S3_SERVER_SIDE_ENCRYPTION", - "PGVECTOR_MAX_CONNECTION", - "PGVECTOR_MIN_CONNECTION", - "PGVECTO_RS_DATABASE", - "PGVECTO_RS_HOST", - "PGVECTO_RS_PASSWORD", - "PGVECTO_RS_PORT", - "PGVECTO_RS_USER", - "PLUGIN_DAEMON_KEY", - "PLUGIN_DAEMON_URL", - "PLUGIN_REMOTE_INSTALL_HOST", - "PLUGIN_REMOTE_INSTALL_PORT", - "RESPECT_XFORWARD_HEADERS_ENABLED", - "SCARF_NO_ANALYTICS", - "SSRF_DEFAULT_CONNECT_TIME_OUT", - "SSRF_DEFAULT_MAX_RETRIES", - "SSRF_DEFAULT_READ_TIME_OUT", - "SSRF_DEFAULT_TIME_OUT", - "SSRF_DEFAULT_WRITE_TIME_OUT", - "STORAGE_OPENDAL_SCHEME", - "SUPABASE_API_KEY", - "SUPABASE_BUCKET_NAME", - "SUPABASE_URL", - "USING_UGC_INDEX", - "VIKINGDB_CONNECTION_TIMEOUT", - "VIKINGDB_SOCKET_TIMEOUT", - "WEAVIATE_BATCH_SIZE", -} +BASE_API_AND_DOCKER_COMPOSE_CONFIG_SET_DIFF: frozenset[str] = frozenset( + ( + "BATCH_UPLOAD_LIMIT", + "CELERY_BEAT_SCHEDULER_TIME", + "HTTP_REQUEST_MAX_CONNECT_TIMEOUT", + "HTTP_REQUEST_MAX_READ_TIMEOUT", + "HTTP_REQUEST_MAX_WRITE_TIMEOUT", + "INNER_API_KEY", + "INNER_API_KEY_FOR_PLUGIN", + "KEYWORD_DATA_SOURCE_TYPE", + "LOGIN_LOCKOUT_DURATION", + "LOG_FORMAT", + "OPENDAL_FS_ROOT", + "OPENDAL_S3_ACCESS_KEY_ID", + "OPENDAL_S3_BUCKET", + "OPENDAL_S3_ENDPOINT", + "OPENDAL_S3_REGION", + "OPENDAL_S3_ROOT", + "OPENDAL_S3_SECRET_ACCESS_KEY", + "OPENDAL_S3_SERVER_SIDE_ENCRYPTION", + "PGVECTOR_MAX_CONNECTION", + "PGVECTOR_MIN_CONNECTION", + "PGVECTO_RS_DATABASE", + "PGVECTO_RS_HOST", + "PGVECTO_RS_PASSWORD", + "PGVECTO_RS_PORT", + "PGVECTO_RS_USER", + "PLUGIN_DAEMON_KEY", + "PLUGIN_DAEMON_URL", + "PLUGIN_REMOTE_INSTALL_HOST", + "PLUGIN_REMOTE_INSTALL_PORT", + "RESPECT_XFORWARD_HEADERS_ENABLED", + "SCARF_NO_ANALYTICS", + "SSRF_DEFAULT_CONNECT_TIME_OUT", + "SSRF_DEFAULT_MAX_RETRIES", + "SSRF_DEFAULT_READ_TIME_OUT", + "SSRF_DEFAULT_TIME_OUT", + "SSRF_DEFAULT_WRITE_TIME_OUT", + "STORAGE_OPENDAL_SCHEME", + "SUPABASE_API_KEY", + "SUPABASE_BUCKET_NAME", + "SUPABASE_URL", + "USING_UGC_INDEX", + "VIKINGDB_CONNECTION_TIMEOUT", + "VIKINGDB_SOCKET_TIMEOUT", + "WEAVIATE_BATCH_SIZE", + ) +) API_CONFIG_SET = set(dotenv_values(Path("api") / Path(".env.example")).keys()) DOCKER_CONFIG_SET = set(dotenv_values(Path("docker") / Path(".env.example")).keys()) From b54a0dc1e4acaa100ae7d8271d2db57614129e03 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:41:20 +0800 Subject: [PATCH 041/199] fix(web): localize error boundary copy (#34332) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .../error-boundary/__tests__/index.spec.tsx | 14 +++++ .../components/base/error-boundary/index.tsx | 52 ++++++++++++++----- .../__tests__/index.spec.tsx | 12 ++++- web/i18n/en-US/common.json | 9 ++++ 4 files changed, 71 insertions(+), 16 deletions(-) diff --git a/web/app/components/base/error-boundary/__tests__/index.spec.tsx b/web/app/components/base/error-boundary/__tests__/index.spec.tsx index 8c34026175..b9838130f7 100644 --- a/web/app/components/base/error-boundary/__tests__/index.spec.tsx +++ b/web/app/components/base/error-boundary/__tests__/index.spec.tsx @@ -1,6 +1,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { createReactI18nextMock } from '@/test/i18n-mock' import ErrorBoundary, { ErrorFallback, useAsyncError, useErrorHandler, withErrorBoundary } from '../index' const mockConfig = vi.hoisted(() => ({ @@ -13,6 +14,19 @@ vi.mock('@/config', () => ({ }, })) +vi.mock('react-i18next', () => createReactI18nextMock({ + 'error': 'Error', + 'errorBoundary.componentStack': 'Component Stack:', + 'errorBoundary.details': 'Error Details (Development Only)', + 'errorBoundary.errorCount': 'This error has occurred {{count}} times', + 'errorBoundary.fallbackTitle': 'Oops! Something went wrong', + 'errorBoundary.message': 'An unexpected error occurred while rendering this component.', + 'errorBoundary.reloadPage': 'Reload Page', + 'errorBoundary.title': 'Something went wrong', + 'errorBoundary.tryAgain': 'Try Again', + 'errorBoundary.tryAgainCompact': 'Try again', +})) + type ThrowOnRenderProps = { message?: string shouldThrow: boolean diff --git a/web/app/components/base/error-boundary/index.tsx b/web/app/components/base/error-boundary/index.tsx index 9cb4b70cf5..3e7bc03ed6 100644 --- a/web/app/components/base/error-boundary/index.tsx +++ b/web/app/components/base/error-boundary/index.tsx @@ -3,6 +3,7 @@ import type { ErrorInfo, ReactNode } from 'react' import { RiAlertLine, RiBugLine } from '@remixicon/react' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import { IS_DEV } from '@/config' import { cn } from '@/utils/classnames' @@ -29,9 +30,21 @@ type ErrorBoundaryProps = { customMessage?: string } +type ErrorBoundaryCopy = { + componentStack: string + details: string + error: string + formatErrorCount: (count: number) => string + message: string + reload: string + title: string + tryAgain: string +} + // Internal class component for error catching class ErrorBoundaryInner extends React.Component< ErrorBoundaryProps & { + copy: ErrorBoundaryCopy resetErrorBoundary: () => void onResetKeysChange: (prevResetKeys?: Array) => void }, @@ -96,6 +109,7 @@ class ErrorBoundaryInner extends React.Component< enableRecovery = true, customTitle, customMessage, + copy, resetErrorBoundary, } = this.props @@ -118,12 +132,12 @@ class ErrorBoundaryInner extends React.Component<

- {customTitle || 'Something went wrong'} + {customTitle || copy.title}

- {customMessage || 'An unexpected error occurred while rendering this component.'} + {customMessage || copy.message}

{showDetails && errorInfo && ( @@ -131,19 +145,19 @@ class ErrorBoundaryInner extends React.Component< - Error Details (Development Only) + {copy.details}
- Error: + {copy.error}
                     {error.toString()}
                   
{errorInfo && (
- Component Stack: + {copy.componentStack}
                       {errorInfo.componentStack}
                     
@@ -151,11 +165,7 @@ class ErrorBoundaryInner extends React.Component< )} {errorCount > 1 && (
- This error has occurred - {' '} - {errorCount} - {' '} - times + {copy.formatErrorCount(errorCount)}
)}
@@ -169,14 +179,14 @@ class ErrorBoundaryInner extends React.Component< size="small" onClick={resetErrorBoundary} > - Try Again + {copy.tryAgain}
)} @@ -190,9 +200,20 @@ class ErrorBoundaryInner extends React.Component< // Main functional component wrapper const ErrorBoundary: React.FC = (props) => { + const { t } = useTranslation() const [errorBoundaryKey, setErrorBoundaryKey] = useState(0) const resetKeysRef = useRef(props.resetKeys) const prevResetKeysRef = useRef | undefined>(undefined) + const copy = { + componentStack: t('errorBoundary.componentStack', { ns: 'common' }), + details: t('errorBoundary.details', { ns: 'common' }), + error: `${t('error', { ns: 'common' })}:`, + formatErrorCount: (count: number) => t('errorBoundary.errorCount', { ns: 'common', count }), + message: t('errorBoundary.message', { ns: 'common' }), + reload: t('errorBoundary.reloadPage', { ns: 'common' }), + title: t('errorBoundary.title', { ns: 'common' }), + tryAgain: t('errorBoundary.tryAgain', { ns: 'common' }), + } const resetErrorBoundary = useCallback(() => { setErrorBoundaryKey(prev => prev + 1) @@ -211,6 +232,7 @@ const ErrorBoundary: React.FC = (props) => { return ( void }> = ({ error, resetErrorBoundaryAction }) => { + const { t } = useTranslation() + return (
-

Oops! Something went wrong

+

{t('errorBoundary.fallbackTitle', { ns: 'common' })}

{error.message}

) diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/index.spec.tsx index 5c7ebfc57a..d41dfaa7d0 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/index.spec.tsx @@ -3,9 +3,17 @@ import type { TriggerSubscription } from '@/app/components/workflow/block-select import { fireEvent, render, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types' +import { createReactI18nextMock } from '@/test/i18n-mock' import { SubscriptionList } from '../index' import { SubscriptionListMode } from '../types' +vi.mock('react-i18next', () => createReactI18nextMock({ + 'errorBoundary.title': 'Something went wrong', + 'errorBoundary.message': 'An unexpected error occurred while rendering this component.', + 'errorBoundary.tryAgain': 'Try Again', + 'errorBoundary.reloadPage': 'Reload Page', +})) + const mockRefetch = vi.fn() let mockSubscriptionListError: Error | null = null let mockSubscriptionListState: { @@ -209,12 +217,12 @@ describe('SubscriptionList', () => { }) describe('Edge Cases', () => { - it('should render error boundary fallback when an error occurs', () => { + it('should render error boundary fallback when an error occurs', async () => { mockSubscriptionListError = new Error('boom') render() - expect(screen.getByText('Something went wrong')).toBeInTheDocument() + expect(await screen.findByText('Something went wrong')).toBeInTheDocument() }) }) }) diff --git a/web/i18n/en-US/common.json b/web/i18n/en-US/common.json index 36301ed72b..c21aa25eae 100644 --- a/web/i18n/en-US/common.json +++ b/web/i18n/en-US/common.json @@ -162,6 +162,15 @@ "environment.development": "DEVELOPMENT", "environment.testing": "TESTING", "error": "Error", + "errorBoundary.componentStack": "Component Stack:", + "errorBoundary.details": "Error Details (Development Only)", + "errorBoundary.errorCount": "This error has occurred {{count}} times", + "errorBoundary.fallbackTitle": "Oops! Something went wrong", + "errorBoundary.message": "An unexpected error occurred while rendering this component.", + "errorBoundary.reloadPage": "Reload Page", + "errorBoundary.title": "Something went wrong", + "errorBoundary.tryAgain": "Try Again", + "errorBoundary.tryAgainCompact": "Try again", "errorMsg.fieldRequired": "{{field}} is required", "errorMsg.urlError": "url should start with http:// or https://", "feedback.content": "Feedback Content", From fbd2d31624646d6e83121b03270fd45c461dc340 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:41:30 +0800 Subject: [PATCH 042/199] refactor(nodejs-sdk): replace axios with fetch transport (#34325) --- pnpm-lock.yaml | 215 +------ pnpm-workspace.yaml | 4 +- sdks/nodejs-client/eslint.config.js | 4 +- sdks/nodejs-client/package.json | 8 +- .../src/client/{base.test.js => base.test.ts} | 10 +- sdks/nodejs-client/src/client/base.ts | 47 +- .../src/client/{chat.test.js => chat.test.ts} | 10 +- sdks/nodejs-client/src/client/chat.ts | 62 +- ...{completion.test.js => completion.test.ts} | 0 sdks/nodejs-client/src/client/completion.ts | 31 +- ...ge-base.test.js => knowledge-base.test.ts} | 21 +- .../src/client/knowledge-base.ts | 27 +- ...{validation.test.js => validation.test.ts} | 7 +- sdks/nodejs-client/src/client/validation.ts | 5 +- .../{workflow.test.js => workflow.test.ts} | 1 - sdks/nodejs-client/src/client/workflow.ts | 20 +- .../{workspace.test.js => workspace.test.ts} | 0 ...{dify-error.test.js => dify-error.test.ts} | 0 sdks/nodejs-client/src/http/client.test.js | 304 ---------- sdks/nodejs-client/src/http/client.test.ts | 527 ++++++++++++++++ sdks/nodejs-client/src/http/client.ts | 569 ++++++++++++------ .../{form-data.test.js => form-data.test.ts} | 10 +- sdks/nodejs-client/src/http/form-data.ts | 20 +- .../src/http/{retry.test.js => retry.test.ts} | 2 +- .../src/http/{sse.test.js => sse.test.ts} | 29 +- sdks/nodejs-client/src/http/sse.ts | 40 +- sdks/nodejs-client/src/index.test.js | 227 ------- sdks/nodejs-client/src/index.test.ts | 240 ++++++++ .../nodejs-client/src/internal/type-guards.ts | 9 + sdks/nodejs-client/src/types/annotation.ts | 3 +- sdks/nodejs-client/src/types/chat.ts | 23 +- sdks/nodejs-client/src/types/common.ts | 24 +- sdks/nodejs-client/src/types/completion.ts | 17 +- .../nodejs-client/src/types/knowledge-base.ts | 33 +- sdks/nodejs-client/src/types/workflow.ts | 17 +- sdks/nodejs-client/src/types/workspace.ts | 4 +- .../tests/http.integration.test.ts | 137 +++++ sdks/nodejs-client/tests/test-utils.js | 30 - sdks/nodejs-client/tests/test-utils.ts | 48 ++ sdks/nodejs-client/tsconfig.json | 4 +- sdks/nodejs-client/vitest.config.ts | 2 +- 41 files changed, 1673 insertions(+), 1118 deletions(-) rename sdks/nodejs-client/src/client/{base.test.js => base.test.ts} (96%) rename sdks/nodejs-client/src/client/{chat.test.js => chat.test.ts} (97%) rename sdks/nodejs-client/src/client/{completion.test.js => completion.test.ts} (100%) rename sdks/nodejs-client/src/client/{knowledge-base.test.js => knowledge-base.test.ts} (92%) rename sdks/nodejs-client/src/client/{validation.test.js => validation.test.ts} (93%) rename sdks/nodejs-client/src/client/{workflow.test.js => workflow.test.ts} (97%) rename sdks/nodejs-client/src/client/{workspace.test.js => workspace.test.ts} (100%) rename sdks/nodejs-client/src/errors/{dify-error.test.js => dify-error.test.ts} (100%) delete mode 100644 sdks/nodejs-client/src/http/client.test.js create mode 100644 sdks/nodejs-client/src/http/client.test.ts rename sdks/nodejs-client/src/http/{form-data.test.js => form-data.test.ts} (73%) rename sdks/nodejs-client/src/http/{retry.test.js => retry.test.ts} (94%) rename sdks/nodejs-client/src/http/{sse.test.js => sse.test.ts} (73%) delete mode 100644 sdks/nodejs-client/src/index.test.js create mode 100644 sdks/nodejs-client/src/index.test.ts create mode 100644 sdks/nodejs-client/src/internal/type-guards.ts create mode 100644 sdks/nodejs-client/tests/http.integration.test.ts delete mode 100644 sdks/nodejs-client/tests/test-utils.js create mode 100644 sdks/nodejs-client/tests/test-utils.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6c234d8ad..eb45ea0ef8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -235,8 +235,8 @@ catalogs: specifier: 0.5.21 version: 0.5.21 '@vitest/coverage-v8': - specifier: 4.1.2 - version: 4.1.2 + specifier: 4.1.1 + version: 4.1.1 abcjs: specifier: 6.6.2 version: 6.6.2 @@ -570,7 +570,6 @@ overrides: array.prototype.flatmap: npm:@nolyfill/array.prototype.flatmap@^1.0.44 array.prototype.tosorted: npm:@nolyfill/array.prototype.tosorted@^1.0.44 assert: npm:@nolyfill/assert@^1.0.26 - axios: 1.14.0 brace-expansion@<2.0.2: 2.0.2 canvas: ^3.2.2 devalue@<5.3.2: 5.3.2 @@ -648,10 +647,6 @@ importers: version: 0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) sdks/nodejs-client: - dependencies: - axios: - specifier: 1.14.0 - version: 1.14.0 devDependencies: '@eslint/js': specifier: 'catalog:' @@ -667,7 +662,7 @@ importers: version: 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.2(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)) + version: 4.1.1(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)) eslint: specifier: 'catalog:' version: 10.1.0(jiti@2.6.1) @@ -1124,7 +1119,7 @@ importers: version: 0.5.21(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(react@19.2.4) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.2(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) + version: 4.1.1(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) agentation: specifier: 'catalog:' version: 3.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -2405,10 +2400,6 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nolyfill/hasown@1.0.44': - resolution: {integrity: sha512-GA/21lkTr2PAQuT6jGnhLuBD5IFd/AEhBXJ/tf33+/bVxPxg+5ejKx9jGQGnyV/P0eSmdup5E+s8b2HL6lOrwQ==} - engines: {node: '>=12.4.0'} - '@nolyfill/is-core-module@1.0.39': resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} @@ -4440,11 +4431,11 @@ packages: react-server-dom-webpack: optional: true - '@vitest/coverage-v8@4.1.2': - resolution: {integrity: sha512-sPK//PHO+kAkScb8XITeB1bf7fsk85Km7+rt4eeuRR3VS1/crD47cmV5wicisJmjNdfeokTZwjMk4Mj2d58Mgg==} + '@vitest/coverage-v8@4.1.1': + resolution: {integrity: sha512-nZ4RWwGCoGOQRMmU/Q9wlUY540RVRxJZ9lxFsFfy0QV7Zmo5VVBhB6Sl9Xa0KIp2iIs3zWfPlo9LcY1iqbpzCw==} peerDependencies: - '@vitest/browser': 4.1.2 - vitest: 4.1.2 + '@vitest/browser': 4.1.1 + vitest: 4.1.1 peerDependenciesMeta: '@vitest/browser': optional: true @@ -4471,8 +4462,8 @@ packages: '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - '@vitest/pretty-format@4.1.2': - resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} + '@vitest/pretty-format@4.1.1': + resolution: {integrity: sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==} '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} @@ -4480,8 +4471,8 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@vitest/utils@4.1.2': - resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} + '@vitest/utils@4.1.1': + resolution: {integrity: sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==} '@voidzero-dev/vite-plus-core@0.1.14': resolution: {integrity: sha512-CCWzdkfW0fo0cQNlIsYp5fOuH2IwKuPZEb2UY2Z8gXcp5pG74A82H2Pthj0heAuvYTAnfT7kEC6zM+RbiBgQbg==} @@ -4841,9 +4832,6 @@ packages: async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - autoprefixer@10.4.27: resolution: {integrity: sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==} engines: {node: ^10 || ^12 || >=14} @@ -4851,9 +4839,6 @@ packages: peerDependencies: postcss: ^8.1.0 - axios@1.14.0: - resolution: {integrity: sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==} - bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -4951,10 +4936,6 @@ packages: resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} engines: {node: '>=20.19.0'} - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -5126,10 +5107,6 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - comma-separated-tokens@1.0.8: resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} @@ -5464,10 +5441,6 @@ packages: delaunator@5.1.0: resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -5533,10 +5506,6 @@ packages: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - echarts-for-react@3.0.6: resolution: {integrity: sha512-4zqLgTGWS3JvkQDXjzkR1k1CHRdpd6by0988TWMJgnvDytegWLbeP/VNZmMa+0VJx2eD7Y632bi2JquXDgiGJg==} peerDependencies: @@ -5613,28 +5582,12 @@ packages: error-stack-parser@2.1.4: resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} es-module-lexer@2.0.0: resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - es-toolkit@1.45.1: resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} @@ -6115,19 +6068,6 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - follow-redirects@1.15.11: - resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} - engines: {node: '>= 6'} - format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} @@ -6164,9 +6104,6 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - functional-red-black-tree@1.0.1: resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} @@ -6181,18 +6118,10 @@ packages: resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} engines: {node: '>=18'} - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - get-stream@5.2.0: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} @@ -6249,10 +6178,6 @@ packages: peerDependencies: csstype: ^3.0.10 - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -6271,14 +6196,6 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - hast-util-from-dom@5.0.1: resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==} @@ -6920,10 +6837,6 @@ packages: engines: {node: '>= 20'} hasBin: true - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - mdast-util-directive@3.1.0: resolution: {integrity: sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==} @@ -7651,10 +7564,6 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - proxy-from-env@2.1.0: - resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} - engines: {node: '>=10'} - pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} @@ -10497,8 +10406,6 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@nolyfill/hasown@1.0.44': {} - '@nolyfill/is-core-module@1.0.39': {} '@nolyfill/safer-buffer@1.0.44': {} @@ -12354,10 +12261,10 @@ snapshots: optionalDependencies: react-server-dom-webpack: 19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) - '@vitest/coverage-v8@4.1.2(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))': + '@vitest/coverage-v8@4.1.1(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.2 + '@vitest/utils': 4.1.1 ast-v8-to-istanbul: 1.0.0 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 @@ -12368,10 +12275,10 @@ snapshots: tinyrainbow: 3.1.0 vitest: '@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(@voidzero-dev/vite-plus-core@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)' - '@vitest/coverage-v8@4.1.2(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3))': + '@vitest/coverage-v8@4.1.1(@voidzero-dev/vite-plus-test@0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3))': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.2 + '@vitest/utils': 4.1.1 ast-v8-to-istanbul: 1.0.0 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 @@ -12406,7 +12313,7 @@ snapshots: dependencies: tinyrainbow: 2.0.0 - '@vitest/pretty-format@4.1.2': + '@vitest/pretty-format@4.1.1': dependencies: tinyrainbow: 3.1.0 @@ -12420,9 +12327,9 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@vitest/utils@4.1.2': + '@vitest/utils@4.1.1': dependencies: - '@vitest/pretty-format': 4.1.2 + '@vitest/pretty-format': 4.1.1 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -12816,8 +12723,6 @@ snapshots: async@3.2.6: {} - asynckit@0.4.0: {} - autoprefixer@10.4.27(postcss@8.5.8): dependencies: browserslist: 4.28.1 @@ -12827,14 +12732,6 @@ snapshots: postcss: 8.5.8 postcss-value-parser: 4.2.0 - axios@1.14.0: - dependencies: - follow-redirects: 1.15.11 - form-data: 4.0.5 - proxy-from-env: 2.1.0 - transitivePeerDependencies: - - debug - bail@2.0.2: {} balanced-match@1.0.2: {} @@ -12914,11 +12811,6 @@ snapshots: cac@7.0.0: {} - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - callsites@3.1.0: {} camelcase-css@2.0.1: {} @@ -13108,10 +13000,6 @@ snapshots: colorette@2.0.20: {} - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - comma-separated-tokens@1.0.8: {} comma-separated-tokens@2.0.3: {} @@ -13441,8 +13329,6 @@ snapshots: dependencies: robust-predicates: 3.0.3 - delayed-stream@1.0.0: {} - dequal@2.0.3: {} destr@2.0.5: {} @@ -13499,12 +13385,6 @@ snapshots: dotenv@16.6.1: {} - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - echarts-for-react@3.0.6(echarts@6.0.0)(react@19.2.4): dependencies: echarts: 6.0.0 @@ -13571,25 +13451,10 @@ snapshots: dependencies: stackframe: 1.3.4 - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - es-module-lexer@1.7.0: {} es-module-lexer@2.0.0: {} - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: '@nolyfill/hasown@1.0.44' - es-toolkit@1.45.1: {} esast-util-from-estree@2.0.0: @@ -14344,16 +14209,6 @@ snapshots: flatted@3.4.2: {} - follow-redirects@1.15.11: {} - - form-data@4.0.5: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: '@nolyfill/hasown@1.0.44' - mime-types: 2.1.35 - format@0.2.2: {} formatly@0.3.0: @@ -14380,8 +14235,6 @@ snapshots: fsevents@2.3.3: optional: true - function-bind@1.1.2: {} - functional-red-black-tree@1.0.1: {} fzf@0.5.2: {} @@ -14390,26 +14243,8 @@ snapshots: get-east-asian-width@1.5.0: {} - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: '@nolyfill/hasown@1.0.44' - math-intrinsics: 1.1.0 - get-nonce@1.0.1: {} - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - get-stream@5.2.0: dependencies: pump: 3.0.4 @@ -14457,8 +14292,6 @@ snapshots: dependencies: csstype: 3.2.3 - gopd@1.2.0: {} - graceful-fs@4.2.11: {} hachure-fill@0.5.2: {} @@ -14481,12 +14314,6 @@ snapshots: has-flag@4.0.0: {} - has-symbols@1.1.0: {} - - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - hast-util-from-dom@5.0.1: dependencies: '@types/hast': 3.0.4 @@ -15127,8 +14954,6 @@ snapshots: marked@17.0.5: {} - math-intrinsics@1.1.0: {} - mdast-util-directive@3.1.0: dependencies: '@types/mdast': 4.0.4 @@ -16267,8 +16092,6 @@ snapshots: property-information@7.1.0: {} - proxy-from-env@2.1.0: {} - pump@3.0.4: dependencies: end-of-stream: 1.4.5 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ae53a57832..b11cca6642 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -22,7 +22,6 @@ overrides: array.prototype.flatmap: npm:@nolyfill/array.prototype.flatmap@^1.0.44 array.prototype.tosorted: npm:@nolyfill/array.prototype.tosorted@^1.0.44 assert: npm:@nolyfill/assert@^1.0.26 - axios: 1.14.0 brace-expansion@<2.0.2: 2.0.2 canvas: ^3.2.2 devalue@<5.3.2: 5.3.2 @@ -147,12 +146,11 @@ catalog: "@typescript/native-preview": 7.0.0-dev.20260329.1 "@vitejs/plugin-react": 6.0.1 "@vitejs/plugin-rsc": 0.5.21 - "@vitest/coverage-v8": 4.1.2 + "@vitest/coverage-v8": 4.1.1 abcjs: 6.6.2 agentation: 3.0.2 ahooks: 3.9.7 autoprefixer: 10.4.27 - axios: 1.14.0 class-variance-authority: 0.7.1 clsx: 2.1.1 cmdk: 1.1.1 diff --git a/sdks/nodejs-client/eslint.config.js b/sdks/nodejs-client/eslint.config.js index 9e659f5d28..21ac872f2a 100644 --- a/sdks/nodejs-client/eslint.config.js +++ b/sdks/nodejs-client/eslint.config.js @@ -12,11 +12,11 @@ const typeCheckedRules = export default [ { - ignores: ["dist", "node_modules", "scripts", "tests", "**/*.test.*", "**/*.spec.*"], + ignores: ["dist", "node_modules", "scripts"], }, js.configs.recommended, { - files: ["src/**/*.ts"], + files: ["src/**/*.ts", "tests/**/*.ts"], languageOptions: { parser: tsParser, ecmaVersion: "latest", diff --git a/sdks/nodejs-client/package.json b/sdks/nodejs-client/package.json index 63fa6799b1..d487c3abb3 100644 --- a/sdks/nodejs-client/package.json +++ b/sdks/nodejs-client/package.json @@ -1,6 +1,6 @@ { "name": "dify-client", - "version": "3.0.0", + "version": "3.1.0", "description": "This is the Node.js SDK for the Dify.AI API, which allows you to easily integrate Dify.AI into your Node.js applications.", "type": "module", "main": "./dist/index.js", @@ -15,7 +15,8 @@ "node": ">=18.0.0" }, "files": [ - "dist", + "dist/index.js", + "dist/index.d.ts", "README.md", "LICENSE" ], @@ -53,9 +54,6 @@ "publish:check": "./scripts/publish.sh --dry-run", "publish:npm": "./scripts/publish.sh" }, - "dependencies": { - "axios": "catalog:" - }, "devDependencies": { "@eslint/js": "catalog:", "@types/node": "catalog:", diff --git a/sdks/nodejs-client/src/client/base.test.js b/sdks/nodejs-client/src/client/base.test.ts similarity index 96% rename from sdks/nodejs-client/src/client/base.test.js rename to sdks/nodejs-client/src/client/base.test.ts index 5e1b21d0f1..868c476432 100644 --- a/sdks/nodejs-client/src/client/base.test.js +++ b/sdks/nodejs-client/src/client/base.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { DifyClient } from "./base"; import { ValidationError } from "../errors/dify-error"; +import { DifyClient } from "./base"; import { createHttpClientWithSpies } from "../../tests/test-utils"; describe("DifyClient base", () => { @@ -103,7 +103,7 @@ describe("DifyClient base", () => { }); }); - it("filePreview uses arraybuffer response", async () => { + it("filePreview uses bytes response", async () => { const { client, request } = createHttpClientWithSpies(); const dify = new DifyClient(client); @@ -113,7 +113,7 @@ describe("DifyClient base", () => { method: "GET", path: "/files/file/preview", query: { user: "user", as_attachment: "true" }, - responseType: "arraybuffer", + responseType: "bytes", }); }); @@ -162,11 +162,11 @@ describe("DifyClient base", () => { streaming: false, voice: "voice", }, - responseType: "arraybuffer", + responseType: "bytes", }); }); - it("textToAudio requires text or message id", async () => { + it("textToAudio requires text or message id", () => { const { client } = createHttpClientWithSpies(); const dify = new DifyClient(client); diff --git a/sdks/nodejs-client/src/client/base.ts b/sdks/nodejs-client/src/client/base.ts index 0fa535a488..f02b88be3a 100644 --- a/sdks/nodejs-client/src/client/base.ts +++ b/sdks/nodejs-client/src/client/base.ts @@ -2,14 +2,18 @@ import type { BinaryStream, DifyClientConfig, DifyResponse, + JsonObject, MessageFeedbackRequest, QueryParams, RequestMethod, + SuccessResponse, TextToAudioRequest, } from "../types/common"; +import type { HttpRequestBody } from "../http/client"; import { HttpClient } from "../http/client"; import { ensureNonEmptyString, ensureRating } from "./validation"; import { FileUploadError, ValidationError } from "../errors/dify-error"; +import type { SdkFormData } from "../http/form-data"; import { isFormData } from "../http/form-data"; const toConfig = ( @@ -25,13 +29,8 @@ const toConfig = ( return init; }; -const appendUserToFormData = (form: unknown, user: string): void => { - if (!isFormData(form)) { - throw new FileUploadError("FormData is required for file uploads"); - } - if (typeof form.append === "function") { - form.append("user", user); - } +const appendUserToFormData = (form: SdkFormData, user: string): void => { + form.append("user", user); }; export class DifyClient { @@ -57,7 +56,7 @@ export class DifyClient { sendRequest( method: RequestMethod, endpoint: string, - data: unknown = null, + data: HttpRequestBody = null, params: QueryParams | null = null, stream = false, headerParams: Record = {} @@ -72,14 +71,14 @@ export class DifyClient { }); } - getRoot(): Promise> { + getRoot(): Promise> { return this.http.request({ method: "GET", path: "/", }); } - getApplicationParameters(user?: string): Promise> { + getApplicationParameters(user?: string): Promise> { if (user) { ensureNonEmptyString(user, "user"); } @@ -90,11 +89,11 @@ export class DifyClient { }); } - async getParameters(user?: string): Promise> { + async getParameters(user?: string): Promise> { return this.getApplicationParameters(user); } - getMeta(user?: string): Promise> { + getMeta(user?: string): Promise> { if (user) { ensureNonEmptyString(user, "user"); } @@ -107,21 +106,21 @@ export class DifyClient { messageFeedback( request: MessageFeedbackRequest - ): Promise>>; + ): Promise>; messageFeedback( messageId: string, rating: "like" | "dislike" | null, user: string, content?: string - ): Promise>>; + ): Promise>; messageFeedback( messageIdOrRequest: string | MessageFeedbackRequest, rating?: "like" | "dislike" | null, user?: string, content?: string - ): Promise>> { + ): Promise> { let messageId: string; - const payload: Record = {}; + const payload: JsonObject = {}; if (typeof messageIdOrRequest === "string") { messageId = messageIdOrRequest; @@ -157,7 +156,7 @@ export class DifyClient { }); } - getInfo(user?: string): Promise> { + getInfo(user?: string): Promise> { if (user) { ensureNonEmptyString(user, "user"); } @@ -168,7 +167,7 @@ export class DifyClient { }); } - getSite(user?: string): Promise> { + getSite(user?: string): Promise> { if (user) { ensureNonEmptyString(user, "user"); } @@ -179,7 +178,7 @@ export class DifyClient { }); } - fileUpload(form: unknown, user: string): Promise> { + fileUpload(form: unknown, user: string): Promise> { if (!isFormData(form)) { throw new FileUploadError("FormData is required for file uploads"); } @@ -199,18 +198,18 @@ export class DifyClient { ): Promise> { ensureNonEmptyString(fileId, "fileId"); ensureNonEmptyString(user, "user"); - return this.http.request({ + return this.http.request({ method: "GET", path: `/files/${fileId}/preview`, query: { user, as_attachment: asAttachment ? "true" : undefined, }, - responseType: "arraybuffer", + responseType: "bytes", }); } - audioToText(form: unknown, user: string): Promise> { + audioToText(form: unknown, user: string): Promise> { if (!isFormData(form)) { throw new FileUploadError("FormData is required for audio uploads"); } @@ -274,11 +273,11 @@ export class DifyClient { }); } - return this.http.request({ + return this.http.request({ method: "POST", path: "/text-to-audio", data: payload, - responseType: "arraybuffer", + responseType: "bytes", }); } } diff --git a/sdks/nodejs-client/src/client/chat.test.js b/sdks/nodejs-client/src/client/chat.test.ts similarity index 97% rename from sdks/nodejs-client/src/client/chat.test.js rename to sdks/nodejs-client/src/client/chat.test.ts index a97c9d4a5c..712ad64fd1 100644 --- a/sdks/nodejs-client/src/client/chat.test.js +++ b/sdks/nodejs-client/src/client/chat.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { ChatClient } from "./chat"; import { ValidationError } from "../errors/dify-error"; +import { ChatClient } from "./chat"; import { createHttpClientWithSpies } from "../../tests/test-utils"; describe("ChatClient", () => { @@ -156,13 +156,13 @@ describe("ChatClient", () => { }); }); - it("requires name when autoGenerate is false", async () => { + it("requires name when autoGenerate is false", () => { const { client } = createHttpClientWithSpies(); const chat = new ChatClient(client); - expect(() => - chat.renameConversation("conv", "", "user", false) - ).toThrow(ValidationError); + expect(() => chat.renameConversation("conv", "", "user", false)).toThrow( + ValidationError + ); }); it("deletes conversations", async () => { diff --git a/sdks/nodejs-client/src/client/chat.ts b/sdks/nodejs-client/src/client/chat.ts index 745c999552..9c232e5117 100644 --- a/sdks/nodejs-client/src/client/chat.ts +++ b/sdks/nodejs-client/src/client/chat.ts @@ -1,5 +1,9 @@ import { DifyClient } from "./base"; -import type { ChatMessageRequest, ChatMessageResponse } from "../types/chat"; +import type { + ChatMessageRequest, + ChatMessageResponse, + ConversationSortBy, +} from "../types/chat"; import type { AnnotationCreateRequest, AnnotationListOptions, @@ -9,7 +13,11 @@ import type { import type { DifyResponse, DifyStream, + JsonObject, + JsonValue, QueryParams, + SuccessResponse, + SuggestedQuestionsResponse, } from "../types/common"; import { ensureNonEmptyString, @@ -22,20 +30,20 @@ export class ChatClient extends DifyClient { request: ChatMessageRequest ): Promise | DifyStream>; createChatMessage( - inputs: Record, + inputs: JsonObject, query: string, user: string, stream?: boolean, conversationId?: string | null, - files?: Array> | null + files?: ChatMessageRequest["files"] ): Promise | DifyStream>; createChatMessage( - inputOrRequest: ChatMessageRequest | Record, + inputOrRequest: ChatMessageRequest | JsonObject, query?: string, user?: string, stream = false, conversationId?: string | null, - files?: Array> | null + files?: ChatMessageRequest["files"] ): Promise | DifyStream> { let payload: ChatMessageRequest; let shouldStream = stream; @@ -46,8 +54,8 @@ export class ChatClient extends DifyClient { } else { ensureNonEmptyString(query, "query"); ensureNonEmptyString(user, "user"); - payload = { - inputs: inputOrRequest as Record, + payload = { + inputs: inputOrRequest, query, user, response_mode: stream ? "streaming" : "blocking", @@ -79,10 +87,10 @@ export class ChatClient extends DifyClient { stopChatMessage( taskId: string, user: string - ): Promise> { + ): Promise> { ensureNonEmptyString(taskId, "taskId"); ensureNonEmptyString(user, "user"); - return this.http.request({ + return this.http.request({ method: "POST", path: `/chat-messages/${taskId}/stop`, data: { user }, @@ -92,17 +100,17 @@ export class ChatClient extends DifyClient { stopMessage( taskId: string, user: string - ): Promise> { + ): Promise> { return this.stopChatMessage(taskId, user); } getSuggested( messageId: string, user: string - ): Promise> { + ): Promise> { ensureNonEmptyString(messageId, "messageId"); ensureNonEmptyString(user, "user"); - return this.http.request({ + return this.http.request({ method: "GET", path: `/messages/${messageId}/suggested`, query: { user }, @@ -114,7 +122,7 @@ export class ChatClient extends DifyClient { getAppFeedbacks( page?: number, limit?: number - ): Promise>> { + ): Promise> { ensureOptionalInt(page, "page"); ensureOptionalInt(limit, "limit"); return this.http.request({ @@ -131,8 +139,8 @@ export class ChatClient extends DifyClient { user: string, lastId?: string | null, limit?: number | null, - sortByOrPinned?: string | boolean | null - ): Promise>> { + sortBy?: ConversationSortBy | null + ): Promise> { ensureNonEmptyString(user, "user"); ensureOptionalString(lastId, "lastId"); ensureOptionalInt(limit, "limit"); @@ -144,10 +152,8 @@ export class ChatClient extends DifyClient { if (limit) { params.limit = limit; } - if (typeof sortByOrPinned === "string") { - params.sort_by = sortByOrPinned; - } else if (typeof sortByOrPinned === "boolean") { - params.pinned = sortByOrPinned; + if (sortBy) { + params.sort_by = sortBy; } return this.http.request({ @@ -162,7 +168,7 @@ export class ChatClient extends DifyClient { conversationId: string, firstId?: string | null, limit?: number | null - ): Promise>> { + ): Promise> { ensureNonEmptyString(user, "user"); ensureNonEmptyString(conversationId, "conversationId"); ensureOptionalString(firstId, "firstId"); @@ -189,18 +195,18 @@ export class ChatClient extends DifyClient { name: string, user: string, autoGenerate?: boolean - ): Promise>>; + ): Promise>; renameConversation( conversationId: string, user: string, options?: { name?: string | null; autoGenerate?: boolean } - ): Promise>>; + ): Promise>; renameConversation( conversationId: string, nameOrUser: string, userOrOptions?: string | { name?: string | null; autoGenerate?: boolean }, autoGenerate?: boolean - ): Promise>> { + ): Promise> { ensureNonEmptyString(conversationId, "conversationId"); let name: string | null | undefined; @@ -222,7 +228,7 @@ export class ChatClient extends DifyClient { ensureNonEmptyString(name, "name"); } - const payload: Record = { + const payload: JsonObject = { user, auto_generate: resolvedAutoGenerate, }; @@ -240,7 +246,7 @@ export class ChatClient extends DifyClient { deleteConversation( conversationId: string, user: string - ): Promise>> { + ): Promise> { ensureNonEmptyString(conversationId, "conversationId"); ensureNonEmptyString(user, "user"); return this.http.request({ @@ -256,7 +262,7 @@ export class ChatClient extends DifyClient { lastId?: string | null, limit?: number | null, variableName?: string | null - ): Promise>> { + ): Promise> { ensureNonEmptyString(conversationId, "conversationId"); ensureNonEmptyString(user, "user"); ensureOptionalString(lastId, "lastId"); @@ -279,8 +285,8 @@ export class ChatClient extends DifyClient { conversationId: string, variableId: string, user: string, - value: unknown - ): Promise>> { + value: JsonValue + ): Promise> { ensureNonEmptyString(conversationId, "conversationId"); ensureNonEmptyString(variableId, "variableId"); ensureNonEmptyString(user, "user"); diff --git a/sdks/nodejs-client/src/client/completion.test.js b/sdks/nodejs-client/src/client/completion.test.ts similarity index 100% rename from sdks/nodejs-client/src/client/completion.test.js rename to sdks/nodejs-client/src/client/completion.test.ts diff --git a/sdks/nodejs-client/src/client/completion.ts b/sdks/nodejs-client/src/client/completion.ts index 9e39898e8b..f4e7121776 100644 --- a/sdks/nodejs-client/src/client/completion.ts +++ b/sdks/nodejs-client/src/client/completion.ts @@ -1,6 +1,11 @@ import { DifyClient } from "./base"; import type { CompletionRequest, CompletionResponse } from "../types/completion"; -import type { DifyResponse, DifyStream } from "../types/common"; +import type { + DifyResponse, + DifyStream, + JsonObject, + SuccessResponse, +} from "../types/common"; import { ensureNonEmptyString } from "./validation"; const warned = new Set(); @@ -17,16 +22,16 @@ export class CompletionClient extends DifyClient { request: CompletionRequest ): Promise | DifyStream>; createCompletionMessage( - inputs: Record, + inputs: JsonObject, user: string, stream?: boolean, - files?: Array> | null + files?: CompletionRequest["files"] ): Promise | DifyStream>; createCompletionMessage( - inputOrRequest: CompletionRequest | Record, + inputOrRequest: CompletionRequest | JsonObject, user?: string, stream = false, - files?: Array> | null + files?: CompletionRequest["files"] ): Promise | DifyStream> { let payload: CompletionRequest; let shouldStream = stream; @@ -37,7 +42,7 @@ export class CompletionClient extends DifyClient { } else { ensureNonEmptyString(user, "user"); payload = { - inputs: inputOrRequest as Record, + inputs: inputOrRequest, user, files, response_mode: stream ? "streaming" : "blocking", @@ -64,10 +69,10 @@ export class CompletionClient extends DifyClient { stopCompletionMessage( taskId: string, user: string - ): Promise> { + ): Promise> { ensureNonEmptyString(taskId, "taskId"); ensureNonEmptyString(user, "user"); - return this.http.request({ + return this.http.request({ method: "POST", path: `/completion-messages/${taskId}/stop`, data: { user }, @@ -77,15 +82,15 @@ export class CompletionClient extends DifyClient { stop( taskId: string, user: string - ): Promise> { + ): Promise> { return this.stopCompletionMessage(taskId, user); } runWorkflow( - inputs: Record, + inputs: JsonObject, user: string, stream = false - ): Promise> | DifyStream>> { + ): Promise | DifyStream> { warnOnce( "CompletionClient.runWorkflow is deprecated. Use WorkflowClient.run instead." ); @@ -96,13 +101,13 @@ export class CompletionClient extends DifyClient { response_mode: stream ? "streaming" : "blocking", }; if (stream) { - return this.http.requestStream>({ + return this.http.requestStream({ method: "POST", path: "/workflows/run", data: payload, }); } - return this.http.request>({ + return this.http.request({ method: "POST", path: "/workflows/run", data: payload, diff --git a/sdks/nodejs-client/src/client/knowledge-base.test.js b/sdks/nodejs-client/src/client/knowledge-base.test.ts similarity index 92% rename from sdks/nodejs-client/src/client/knowledge-base.test.js rename to sdks/nodejs-client/src/client/knowledge-base.test.ts index 4381b39e56..113a9db24b 100644 --- a/sdks/nodejs-client/src/client/knowledge-base.test.js +++ b/sdks/nodejs-client/src/client/knowledge-base.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { FileUploadError, ValidationError } from "../errors/dify-error"; import { KnowledgeBaseClient } from "./knowledge-base"; import { createHttpClientWithSpies } from "../../tests/test-utils"; @@ -174,7 +175,6 @@ describe("KnowledgeBaseClient", () => { it("handles pipeline operations", async () => { const { client, request, requestStream } = createHttpClientWithSpies(); const kb = new KnowledgeBaseClient(client); - const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); const form = { append: vi.fn(), getHeaders: () => ({}) }; await kb.listDatasourcePlugins("ds", { isPublished: true }); @@ -201,7 +201,6 @@ describe("KnowledgeBaseClient", () => { }); await kb.uploadPipelineFile(form); - expect(warn).toHaveBeenCalled(); expect(request).toHaveBeenCalledWith({ method: "GET", path: "/datasets/ds/pipeline/datasource-plugins", @@ -246,4 +245,22 @@ describe("KnowledgeBaseClient", () => { data: form, }); }); + + it("validates form-data and optional array filters", async () => { + const { client } = createHttpClientWithSpies(); + const kb = new KnowledgeBaseClient(client); + + await expect(kb.createDocumentByFile("ds", {})).rejects.toBeInstanceOf( + FileUploadError + ); + await expect( + kb.listSegments("ds", "doc", { status: ["ok", 1] as unknown as string[] }) + ).rejects.toBeInstanceOf(ValidationError); + await expect( + kb.hitTesting("ds", { + query: "q", + attachment_ids: ["att-1", 2] as unknown as string[], + }) + ).rejects.toBeInstanceOf(ValidationError); + }); }); diff --git a/sdks/nodejs-client/src/client/knowledge-base.ts b/sdks/nodejs-client/src/client/knowledge-base.ts index 7a0e39898b..9871c098e9 100644 --- a/sdks/nodejs-client/src/client/knowledge-base.ts +++ b/sdks/nodejs-client/src/client/knowledge-base.ts @@ -38,22 +38,17 @@ import { ensureStringArray, } from "./validation"; import { FileUploadError, ValidationError } from "../errors/dify-error"; +import type { SdkFormData } from "../http/form-data"; import { isFormData } from "../http/form-data"; -const warned = new Set(); -const warnOnce = (message: string): void => { - if (warned.has(message)) { - return; - } - warned.add(message); - console.warn(message); -}; - -const ensureFormData = (form: unknown, context: string): void => { +function ensureFormData( + form: unknown, + context: string +): asserts form is SdkFormData { if (!isFormData(form)) { throw new FileUploadError(`${context} requires FormData`); } -}; +} const ensureNonEmptyArray = (value: unknown, name: string): void => { if (!Array.isArray(value) || value.length === 0) { @@ -61,12 +56,6 @@ const ensureNonEmptyArray = (value: unknown, name: string): void => { } }; -const warnPipelineRoutes = (): void => { - warnOnce( - "RAG pipeline endpoints may be unavailable unless the service API registers dataset/rag_pipeline routes." - ); -}; - export class KnowledgeBaseClient extends DifyClient { async listDatasets( options?: DatasetListOptions @@ -641,7 +630,6 @@ export class KnowledgeBaseClient extends DifyClient { datasetId: string, options?: DatasourcePluginListOptions ): Promise> { - warnPipelineRoutes(); ensureNonEmptyString(datasetId, "datasetId"); ensureOptionalBoolean(options?.isPublished, "isPublished"); return this.http.request({ @@ -658,7 +646,6 @@ export class KnowledgeBaseClient extends DifyClient { nodeId: string, request: DatasourceNodeRunRequest ): Promise> { - warnPipelineRoutes(); ensureNonEmptyString(datasetId, "datasetId"); ensureNonEmptyString(nodeId, "nodeId"); ensureNonEmptyString(request.datasource_type, "datasource_type"); @@ -673,7 +660,6 @@ export class KnowledgeBaseClient extends DifyClient { datasetId: string, request: PipelineRunRequest ): Promise | DifyStream> { - warnPipelineRoutes(); ensureNonEmptyString(datasetId, "datasetId"); ensureNonEmptyString(request.datasource_type, "datasource_type"); ensureNonEmptyString(request.start_node_id, "start_node_id"); @@ -695,7 +681,6 @@ export class KnowledgeBaseClient extends DifyClient { async uploadPipelineFile( form: unknown ): Promise> { - warnPipelineRoutes(); ensureFormData(form, "uploadPipelineFile"); return this.http.request({ method: "POST", diff --git a/sdks/nodejs-client/src/client/validation.test.js b/sdks/nodejs-client/src/client/validation.test.ts similarity index 93% rename from sdks/nodejs-client/src/client/validation.test.js rename to sdks/nodejs-client/src/client/validation.test.ts index 65bfa471a6..384dd46309 100644 --- a/sdks/nodejs-client/src/client/validation.test.js +++ b/sdks/nodejs-client/src/client/validation.test.ts @@ -10,7 +10,7 @@ import { validateParams, } from "./validation"; -const makeLongString = (length) => "a".repeat(length); +const makeLongString = (length: number) => "a".repeat(length); describe("validation utilities", () => { it("ensureNonEmptyString throws on empty or whitespace", () => { @@ -19,9 +19,7 @@ describe("validation utilities", () => { }); it("ensureNonEmptyString throws on overly long strings", () => { - expect(() => - ensureNonEmptyString(makeLongString(10001), "name") - ).toThrow(); + expect(() => ensureNonEmptyString(makeLongString(10001), "name")).toThrow(); }); it("ensureOptionalString ignores undefined and validates when set", () => { @@ -73,7 +71,6 @@ describe("validation utilities", () => { expect(() => validateParams({ rating: "bad" })).toThrow(); expect(() => validateParams({ page: 1.1 })).toThrow(); expect(() => validateParams({ files: "bad" })).toThrow(); - // Empty strings are allowed for optional params (e.g., keyword: "" means no filter) expect(() => validateParams({ keyword: "" })).not.toThrow(); expect(() => validateParams({ name: makeLongString(10001) })).toThrow(); expect(() => diff --git a/sdks/nodejs-client/src/client/validation.ts b/sdks/nodejs-client/src/client/validation.ts index 6aeec36bdc..0fe747a8f9 100644 --- a/sdks/nodejs-client/src/client/validation.ts +++ b/sdks/nodejs-client/src/client/validation.ts @@ -1,4 +1,5 @@ import { ValidationError } from "../errors/dify-error"; +import { isRecord } from "../internal/type-guards"; const MAX_STRING_LENGTH = 10000; const MAX_LIST_LENGTH = 1000; @@ -109,8 +110,8 @@ export function validateParams(params: Record): void { `Parameter '${key}' exceeds maximum size of ${MAX_LIST_LENGTH} items` ); } - } else if (typeof value === "object") { - if (Object.keys(value as Record).length > MAX_DICT_LENGTH) { + } else if (isRecord(value)) { + if (Object.keys(value).length > MAX_DICT_LENGTH) { throw new ValidationError( `Parameter '${key}' exceeds maximum size of ${MAX_DICT_LENGTH} items` ); diff --git a/sdks/nodejs-client/src/client/workflow.test.js b/sdks/nodejs-client/src/client/workflow.test.ts similarity index 97% rename from sdks/nodejs-client/src/client/workflow.test.js rename to sdks/nodejs-client/src/client/workflow.test.ts index 79c419b55a..281540304e 100644 --- a/sdks/nodejs-client/src/client/workflow.test.js +++ b/sdks/nodejs-client/src/client/workflow.test.ts @@ -90,7 +90,6 @@ describe("WorkflowClient", () => { const { client, request } = createHttpClientWithSpies(); const workflow = new WorkflowClient(client); - // Use createdByEndUserSessionId to filter by user session (backend API parameter) await workflow.getLogs({ keyword: "k", status: "succeeded", diff --git a/sdks/nodejs-client/src/client/workflow.ts b/sdks/nodejs-client/src/client/workflow.ts index ae4d5861fa..6e073b12d2 100644 --- a/sdks/nodejs-client/src/client/workflow.ts +++ b/sdks/nodejs-client/src/client/workflow.ts @@ -1,6 +1,12 @@ import { DifyClient } from "./base"; import type { WorkflowRunRequest, WorkflowRunResponse } from "../types/workflow"; -import type { DifyResponse, DifyStream, QueryParams } from "../types/common"; +import type { + DifyResponse, + DifyStream, + JsonObject, + QueryParams, + SuccessResponse, +} from "../types/common"; import { ensureNonEmptyString, ensureOptionalInt, @@ -12,12 +18,12 @@ export class WorkflowClient extends DifyClient { request: WorkflowRunRequest ): Promise | DifyStream>; run( - inputs: Record, + inputs: JsonObject, user: string, stream?: boolean ): Promise | DifyStream>; run( - inputOrRequest: WorkflowRunRequest | Record, + inputOrRequest: WorkflowRunRequest | JsonObject, user?: string, stream = false ): Promise | DifyStream> { @@ -30,7 +36,7 @@ export class WorkflowClient extends DifyClient { } else { ensureNonEmptyString(user, "user"); payload = { - inputs: inputOrRequest as Record, + inputs: inputOrRequest, user, response_mode: stream ? "streaming" : "blocking", }; @@ -84,10 +90,10 @@ export class WorkflowClient extends DifyClient { stop( taskId: string, user: string - ): Promise> { + ): Promise> { ensureNonEmptyString(taskId, "taskId"); ensureNonEmptyString(user, "user"); - return this.http.request({ + return this.http.request({ method: "POST", path: `/workflows/tasks/${taskId}/stop`, data: { user }, @@ -111,7 +117,7 @@ export class WorkflowClient extends DifyClient { limit?: number; startTime?: string; endTime?: string; - }): Promise>> { + }): Promise> { if (options?.keyword) { ensureOptionalString(options.keyword, "keyword"); } diff --git a/sdks/nodejs-client/src/client/workspace.test.js b/sdks/nodejs-client/src/client/workspace.test.ts similarity index 100% rename from sdks/nodejs-client/src/client/workspace.test.js rename to sdks/nodejs-client/src/client/workspace.test.ts diff --git a/sdks/nodejs-client/src/errors/dify-error.test.js b/sdks/nodejs-client/src/errors/dify-error.test.ts similarity index 100% rename from sdks/nodejs-client/src/errors/dify-error.test.js rename to sdks/nodejs-client/src/errors/dify-error.test.ts diff --git a/sdks/nodejs-client/src/http/client.test.js b/sdks/nodejs-client/src/http/client.test.js deleted file mode 100644 index 05892547ed..0000000000 --- a/sdks/nodejs-client/src/http/client.test.js +++ /dev/null @@ -1,304 +0,0 @@ -import axios from "axios"; -import { Readable } from "node:stream"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { - APIError, - AuthenticationError, - FileUploadError, - NetworkError, - RateLimitError, - TimeoutError, - ValidationError, -} from "../errors/dify-error"; -import { HttpClient } from "./client"; - -describe("HttpClient", () => { - beforeEach(() => { - vi.restoreAllMocks(); - }); - it("builds requests with auth headers and JSON content type", async () => { - const mockRequest = vi.fn().mockResolvedValue({ - status: 200, - data: { ok: true }, - headers: { "x-request-id": "req" }, - }); - vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); - - const client = new HttpClient({ apiKey: "test" }); - const response = await client.request({ - method: "POST", - path: "/chat-messages", - data: { user: "u" }, - }); - - expect(response.requestId).toBe("req"); - const config = mockRequest.mock.calls[0][0]; - expect(config.headers.Authorization).toBe("Bearer test"); - expect(config.headers["Content-Type"]).toBe("application/json"); - expect(config.responseType).toBe("json"); - }); - - it("serializes array query params", async () => { - const mockRequest = vi.fn().mockResolvedValue({ - status: 200, - data: "ok", - headers: {}, - }); - vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); - - const client = new HttpClient({ apiKey: "test" }); - await client.requestRaw({ - method: "GET", - path: "/datasets", - query: { tag_ids: ["a", "b"], limit: 2 }, - }); - - const config = mockRequest.mock.calls[0][0]; - const queryString = config.paramsSerializer.serialize({ - tag_ids: ["a", "b"], - limit: 2, - }); - expect(queryString).toBe("tag_ids=a&tag_ids=b&limit=2"); - }); - - it("returns SSE stream helpers", async () => { - const mockRequest = vi.fn().mockResolvedValue({ - status: 200, - data: Readable.from(["data: {\"text\":\"hi\"}\n\n"]), - headers: { "x-request-id": "req" }, - }); - vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); - - const client = new HttpClient({ apiKey: "test" }); - const stream = await client.requestStream({ - method: "POST", - path: "/chat-messages", - data: { user: "u" }, - }); - - expect(stream.status).toBe(200); - expect(stream.requestId).toBe("req"); - await expect(stream.toText()).resolves.toBe("hi"); - }); - - it("returns binary stream helpers", async () => { - const mockRequest = vi.fn().mockResolvedValue({ - status: 200, - data: Readable.from(["chunk"]), - headers: { "x-request-id": "req" }, - }); - vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); - - const client = new HttpClient({ apiKey: "test" }); - const stream = await client.requestBinaryStream({ - method: "POST", - path: "/text-to-audio", - data: { user: "u", text: "hi" }, - }); - - expect(stream.status).toBe(200); - expect(stream.requestId).toBe("req"); - }); - - it("respects form-data headers", async () => { - const mockRequest = vi.fn().mockResolvedValue({ - status: 200, - data: "ok", - headers: {}, - }); - vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); - - const client = new HttpClient({ apiKey: "test" }); - const form = { - append: () => {}, - getHeaders: () => ({ "content-type": "multipart/form-data; boundary=abc" }), - }; - - await client.requestRaw({ - method: "POST", - path: "/files/upload", - data: form, - }); - - const config = mockRequest.mock.calls[0][0]; - expect(config.headers["content-type"]).toBe( - "multipart/form-data; boundary=abc" - ); - expect(config.headers["Content-Type"]).toBeUndefined(); - }); - - it("maps 401 and 429 errors", async () => { - const mockRequest = vi.fn(); - vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); - const client = new HttpClient({ apiKey: "test", maxRetries: 0 }); - - mockRequest.mockRejectedValueOnce({ - isAxiosError: true, - response: { - status: 401, - data: { message: "unauthorized" }, - headers: {}, - }, - }); - await expect( - client.requestRaw({ method: "GET", path: "/meta" }) - ).rejects.toBeInstanceOf(AuthenticationError); - - mockRequest.mockRejectedValueOnce({ - isAxiosError: true, - response: { - status: 429, - data: { message: "rate" }, - headers: { "retry-after": "2" }, - }, - }); - const error = await client - .requestRaw({ method: "GET", path: "/meta" }) - .catch((err) => err); - expect(error).toBeInstanceOf(RateLimitError); - expect(error.retryAfter).toBe(2); - }); - - it("maps validation and upload errors", async () => { - const mockRequest = vi.fn(); - vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); - const client = new HttpClient({ apiKey: "test", maxRetries: 0 }); - - mockRequest.mockRejectedValueOnce({ - isAxiosError: true, - response: { - status: 422, - data: { message: "invalid" }, - headers: {}, - }, - }); - await expect( - client.requestRaw({ method: "POST", path: "/chat-messages", data: { user: "u" } }) - ).rejects.toBeInstanceOf(ValidationError); - - mockRequest.mockRejectedValueOnce({ - isAxiosError: true, - config: { url: "/files/upload" }, - response: { - status: 400, - data: { message: "bad upload" }, - headers: {}, - }, - }); - await expect( - client.requestRaw({ method: "POST", path: "/files/upload", data: { user: "u" } }) - ).rejects.toBeInstanceOf(FileUploadError); - }); - - it("maps timeout and network errors", async () => { - const mockRequest = vi.fn(); - vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); - const client = new HttpClient({ apiKey: "test", maxRetries: 0 }); - - mockRequest.mockRejectedValueOnce({ - isAxiosError: true, - code: "ECONNABORTED", - message: "timeout", - }); - await expect( - client.requestRaw({ method: "GET", path: "/meta" }) - ).rejects.toBeInstanceOf(TimeoutError); - - mockRequest.mockRejectedValueOnce({ - isAxiosError: true, - message: "network", - }); - await expect( - client.requestRaw({ method: "GET", path: "/meta" }) - ).rejects.toBeInstanceOf(NetworkError); - }); - - it("retries on timeout errors", async () => { - const mockRequest = vi.fn(); - vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); - const client = new HttpClient({ apiKey: "test", maxRetries: 1, retryDelay: 0 }); - - mockRequest - .mockRejectedValueOnce({ - isAxiosError: true, - code: "ECONNABORTED", - message: "timeout", - }) - .mockResolvedValueOnce({ status: 200, data: "ok", headers: {} }); - - await client.requestRaw({ method: "GET", path: "/meta" }); - expect(mockRequest).toHaveBeenCalledTimes(2); - }); - - it("validates query parameters before request", async () => { - const mockRequest = vi.fn(); - vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); - const client = new HttpClient({ apiKey: "test" }); - - await expect( - client.requestRaw({ method: "GET", path: "/meta", query: { user: 1 } }) - ).rejects.toBeInstanceOf(ValidationError); - expect(mockRequest).not.toHaveBeenCalled(); - }); - - it("returns APIError for other http failures", async () => { - const mockRequest = vi.fn(); - vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); - const client = new HttpClient({ apiKey: "test", maxRetries: 0 }); - - mockRequest.mockRejectedValueOnce({ - isAxiosError: true, - response: { status: 500, data: { message: "server" }, headers: {} }, - }); - - await expect( - client.requestRaw({ method: "GET", path: "/meta" }) - ).rejects.toBeInstanceOf(APIError); - }); - - it("logs requests and responses when enableLogging is true", async () => { - const mockRequest = vi.fn().mockResolvedValue({ - status: 200, - data: { ok: true }, - headers: {}, - }); - vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); - const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => {}); - - const client = new HttpClient({ apiKey: "test", enableLogging: true }); - await client.requestRaw({ method: "GET", path: "/meta" }); - - expect(consoleInfo).toHaveBeenCalledWith( - expect.stringContaining("dify-client-node response 200 GET") - ); - consoleInfo.mockRestore(); - }); - - it("logs retry attempts when enableLogging is true", async () => { - const mockRequest = vi.fn(); - vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); - const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => {}); - - const client = new HttpClient({ - apiKey: "test", - maxRetries: 1, - retryDelay: 0, - enableLogging: true, - }); - - mockRequest - .mockRejectedValueOnce({ - isAxiosError: true, - code: "ECONNABORTED", - message: "timeout", - }) - .mockResolvedValueOnce({ status: 200, data: "ok", headers: {} }); - - await client.requestRaw({ method: "GET", path: "/meta" }); - - expect(consoleInfo).toHaveBeenCalledWith( - expect.stringContaining("dify-client-node retry") - ); - consoleInfo.mockRestore(); - }); -}); diff --git a/sdks/nodejs-client/src/http/client.test.ts b/sdks/nodejs-client/src/http/client.test.ts new file mode 100644 index 0000000000..af859801c6 --- /dev/null +++ b/sdks/nodejs-client/src/http/client.test.ts @@ -0,0 +1,527 @@ +import { Readable, Stream } from "node:stream"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { + APIError, + AuthenticationError, + FileUploadError, + NetworkError, + RateLimitError, + TimeoutError, + ValidationError, +} from "../errors/dify-error"; +import { HttpClient } from "./client"; + +const stubFetch = (): ReturnType => { + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + return fetchMock; +}; + +const getFetchCall = ( + fetchMock: ReturnType, + index = 0 +): [string, RequestInit | undefined] => { + const call = fetchMock.mock.calls[index]; + if (!call) { + throw new Error(`Missing fetch call at index ${index}`); + } + return call as [string, RequestInit | undefined]; +}; + +const toHeaderRecord = (headers: HeadersInit | undefined): Record => + Object.fromEntries(new Headers(headers).entries()); + +const jsonResponse = ( + body: unknown, + init: ResponseInit = {} +): Response => + new Response(JSON.stringify(body), { + ...init, + headers: { + "content-type": "application/json", + ...(init.headers ?? {}), + }, + }); + +const textResponse = (body: string, init: ResponseInit = {}): Response => + new Response(body, { + ...init, + headers: { + ...(init.headers ?? {}), + }, + }); + +describe("HttpClient", () => { + beforeEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it("builds requests with auth headers and JSON content type", async () => { + const fetchMock = stubFetch(); + fetchMock.mockResolvedValueOnce( + jsonResponse({ ok: true }, { status: 200, headers: { "x-request-id": "req" } }) + ); + + const client = new HttpClient({ apiKey: "test" }); + const response = await client.request({ + method: "POST", + path: "/chat-messages", + data: { user: "u" }, + }); + + expect(response.requestId).toBe("req"); + expect(fetchMock).toHaveBeenCalledTimes(1); + const [url, init] = getFetchCall(fetchMock); + expect(url).toBe("https://api.dify.ai/v1/chat-messages"); + expect(toHeaderRecord(init?.headers)).toMatchObject({ + authorization: "Bearer test", + "content-type": "application/json", + "user-agent": "dify-client-node", + }); + expect(init?.body).toBe(JSON.stringify({ user: "u" })); + }); + + it("serializes array query params", async () => { + const fetchMock = stubFetch(); + fetchMock.mockResolvedValueOnce(jsonResponse("ok", { status: 200 })); + + const client = new HttpClient({ apiKey: "test" }); + await client.requestRaw({ + method: "GET", + path: "/datasets", + query: { tag_ids: ["a", "b"], limit: 2 }, + }); + + const [url] = getFetchCall(fetchMock); + expect(new URL(url).searchParams.toString()).toBe( + "tag_ids=a&tag_ids=b&limit=2" + ); + }); + + it("returns SSE stream helpers", async () => { + const fetchMock = stubFetch(); + fetchMock.mockResolvedValueOnce( + new Response('data: {"text":"hi"}\n\n', { + status: 200, + headers: { "x-request-id": "req" }, + }) + ); + + const client = new HttpClient({ apiKey: "test" }); + const stream = await client.requestStream({ + method: "POST", + path: "/chat-messages", + data: { user: "u" }, + }); + + expect(stream.status).toBe(200); + expect(stream.requestId).toBe("req"); + await expect(stream.toText()).resolves.toBe("hi"); + }); + + it("returns binary stream helpers", async () => { + const fetchMock = stubFetch(); + fetchMock.mockResolvedValueOnce( + new Response("chunk", { + status: 200, + headers: { "x-request-id": "req" }, + }) + ); + + const client = new HttpClient({ apiKey: "test" }); + const stream = await client.requestBinaryStream({ + method: "POST", + path: "/text-to-audio", + data: { user: "u", text: "hi" }, + }); + + expect(stream.status).toBe(200); + expect(stream.requestId).toBe("req"); + expect(stream.data).toBeInstanceOf(Readable); + }); + + it("respects form-data headers", async () => { + const fetchMock = stubFetch(); + fetchMock.mockResolvedValueOnce(jsonResponse("ok", { status: 200 })); + + const client = new HttpClient({ apiKey: "test" }); + const form = new FormData(); + form.append("file", new Blob(["abc"]), "file.txt"); + + await client.requestRaw({ + method: "POST", + path: "/files/upload", + data: form, + }); + + const [, init] = getFetchCall(fetchMock); + expect(toHeaderRecord(init?.headers)).toMatchObject({ + authorization: "Bearer test", + }); + expect(toHeaderRecord(init?.headers)["content-type"]).toBeUndefined(); + }); + + it("sends legacy form-data as a readable request body", async () => { + const fetchMock = stubFetch(); + fetchMock.mockResolvedValueOnce(jsonResponse("ok", { status: 200 })); + + const client = new HttpClient({ apiKey: "test" }); + const legacyForm = Object.assign(Readable.from(["chunk"]), { + append: vi.fn(), + getHeaders: () => ({ + "content-type": "multipart/form-data; boundary=test", + }), + }); + + await client.requestRaw({ + method: "POST", + path: "/files/upload", + data: legacyForm, + }); + + const [, init] = getFetchCall(fetchMock); + expect(toHeaderRecord(init?.headers)).toMatchObject({ + authorization: "Bearer test", + "content-type": "multipart/form-data; boundary=test", + }); + expect((init as RequestInit & { duplex?: string } | undefined)?.duplex).toBe( + "half" + ); + expect(init?.body).not.toBe(legacyForm); + }); + + it("rejects legacy form-data objects that are not readable streams", async () => { + const fetchMock = stubFetch(); + const client = new HttpClient({ apiKey: "test" }); + const legacyForm = { + append: vi.fn(), + getHeaders: () => ({ + "content-type": "multipart/form-data; boundary=test", + }), + }; + + await expect( + client.requestRaw({ + method: "POST", + path: "/files/upload", + data: legacyForm, + }) + ).rejects.toBeInstanceOf(FileUploadError); + + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("accepts legacy pipeable streams that are not Readable instances", async () => { + const fetchMock = stubFetch(); + fetchMock.mockResolvedValueOnce(jsonResponse("ok", { status: 200 })); + const client = new HttpClient({ apiKey: "test" }); + + const legacyStream = new Stream() as Stream & + NodeJS.ReadableStream & { + append: ReturnType; + getHeaders: () => Record; + }; + legacyStream.readable = true; + legacyStream.pause = () => legacyStream; + legacyStream.resume = () => legacyStream; + legacyStream.append = vi.fn(); + legacyStream.getHeaders = () => ({ + "content-type": "multipart/form-data; boundary=test", + }); + queueMicrotask(() => { + legacyStream.emit("data", Buffer.from("chunk")); + legacyStream.emit("end"); + }); + + await client.requestRaw({ + method: "POST", + path: "/files/upload", + data: legacyStream as unknown as FormData, + }); + + const [, init] = getFetchCall(fetchMock); + expect((init as RequestInit & { duplex?: string } | undefined)?.duplex).toBe( + "half" + ); + }); + + it("returns buffers for byte responses", async () => { + const fetchMock = stubFetch(); + fetchMock.mockResolvedValueOnce( + new Response(Uint8Array.from([1, 2, 3]), { + status: 200, + headers: { "content-type": "application/octet-stream" }, + }) + ); + + const client = new HttpClient({ apiKey: "test" }); + const response = await client.request({ + method: "GET", + path: "/files/file-1/preview", + responseType: "bytes", + }); + + expect(Buffer.isBuffer(response.data)).toBe(true); + expect(Array.from(response.data.values())).toEqual([1, 2, 3]); + }); + + it("keeps arraybuffer as a backward-compatible binary alias", async () => { + const fetchMock = stubFetch(); + fetchMock.mockResolvedValueOnce( + new Response(Uint8Array.from([4, 5, 6]), { + status: 200, + headers: { "content-type": "application/octet-stream" }, + }) + ); + + const client = new HttpClient({ apiKey: "test" }); + const response = await client.request({ + method: "GET", + path: "/files/file-1/preview", + responseType: "arraybuffer", + }); + + expect(Buffer.isBuffer(response.data)).toBe(true); + expect(Array.from(response.data.values())).toEqual([4, 5, 6]); + }); + + it("returns null for empty no-content responses", async () => { + const fetchMock = stubFetch(); + fetchMock.mockResolvedValueOnce(new Response(null, { status: 204 })); + + const client = new HttpClient({ apiKey: "test" }); + const response = await client.requestRaw({ + method: "GET", + path: "/meta", + }); + + expect(response.data).toBeNull(); + }); + + it("maps 401 and 429 errors", async () => { + const fetchMock = stubFetch(); + fetchMock + .mockResolvedValueOnce( + jsonResponse({ message: "unauthorized" }, { status: 401 }) + ) + .mockResolvedValueOnce( + jsonResponse({ message: "rate" }, { status: 429, headers: { "retry-after": "2" } }) + ); + const client = new HttpClient({ apiKey: "test", maxRetries: 0 }); + + await expect( + client.requestRaw({ method: "GET", path: "/meta" }) + ).rejects.toBeInstanceOf(AuthenticationError); + + const error = await client + .requestRaw({ method: "GET", path: "/meta" }) + .catch((err: unknown) => err); + expect(error).toBeInstanceOf(RateLimitError); + expect((error as RateLimitError).retryAfter).toBe(2); + }); + + it("maps validation and upload errors", async () => { + const fetchMock = stubFetch(); + fetchMock + .mockResolvedValueOnce(jsonResponse({ message: "invalid" }, { status: 422 })) + .mockResolvedValueOnce(jsonResponse({ message: "bad upload" }, { status: 400 })); + const client = new HttpClient({ apiKey: "test", maxRetries: 0 }); + + await expect( + client.requestRaw({ method: "POST", path: "/chat-messages", data: { user: "u" } }) + ).rejects.toBeInstanceOf(ValidationError); + + await expect( + client.requestRaw({ method: "POST", path: "/files/upload", data: { user: "u" } }) + ).rejects.toBeInstanceOf(FileUploadError); + }); + + it("maps timeout and network errors", async () => { + const fetchMock = stubFetch(); + fetchMock + .mockRejectedValueOnce(Object.assign(new Error("timeout"), { name: "AbortError" })) + .mockRejectedValueOnce(new Error("network")); + const client = new HttpClient({ apiKey: "test", maxRetries: 0 }); + + await expect( + client.requestRaw({ method: "GET", path: "/meta" }) + ).rejects.toBeInstanceOf(TimeoutError); + + await expect( + client.requestRaw({ method: "GET", path: "/meta" }) + ).rejects.toBeInstanceOf(NetworkError); + }); + + it("maps unknown transport failures to NetworkError", async () => { + const fetchMock = stubFetch(); + fetchMock.mockRejectedValueOnce("boom"); + const client = new HttpClient({ apiKey: "test", maxRetries: 0 }); + + await expect( + client.requestRaw({ method: "GET", path: "/meta" }) + ).rejects.toMatchObject({ + name: "NetworkError", + message: "Unexpected network error", + }); + }); + + it("retries on timeout errors", async () => { + const fetchMock = stubFetch(); + fetchMock + .mockRejectedValueOnce(Object.assign(new Error("timeout"), { name: "AbortError" })) + .mockResolvedValueOnce(jsonResponse("ok", { status: 200 })); + const client = new HttpClient({ apiKey: "test", maxRetries: 1, retryDelay: 0 }); + + await client.requestRaw({ method: "GET", path: "/meta" }); + expect(fetchMock).toHaveBeenCalledTimes(2); + }); + + it("does not retry non-replayable readable request bodies", async () => { + const fetchMock = stubFetch(); + fetchMock.mockRejectedValueOnce(new Error("network")); + const client = new HttpClient({ apiKey: "test", maxRetries: 2, retryDelay: 0 }); + + await expect( + client.requestRaw({ + method: "POST", + path: "/chat-messages", + data: Readable.from(["chunk"]), + }) + ).rejects.toBeInstanceOf(NetworkError); + + expect(fetchMock).toHaveBeenCalledTimes(1); + const [, init] = getFetchCall(fetchMock); + expect((init as RequestInit & { duplex?: string } | undefined)?.duplex).toBe( + "half" + ); + }); + + it("validates query parameters before request", async () => { + const fetchMock = stubFetch(); + const client = new HttpClient({ apiKey: "test" }); + + await expect( + client.requestRaw({ method: "GET", path: "/meta", query: { user: 1 } }) + ).rejects.toBeInstanceOf(ValidationError); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("returns APIError for other http failures", async () => { + const fetchMock = stubFetch(); + fetchMock.mockResolvedValueOnce(jsonResponse({ message: "server" }, { status: 500 })); + const client = new HttpClient({ apiKey: "test", maxRetries: 0 }); + + await expect( + client.requestRaw({ method: "GET", path: "/meta" }) + ).rejects.toBeInstanceOf(APIError); + }); + + it("uses plain text bodies when json parsing is not possible", async () => { + const fetchMock = stubFetch(); + fetchMock.mockResolvedValueOnce( + textResponse("plain text", { + status: 200, + headers: { "content-type": "text/plain" }, + }) + ); + const client = new HttpClient({ apiKey: "test" }); + + const response = await client.requestRaw({ + method: "GET", + path: "/info", + }); + + expect(response.data).toBe("plain text"); + }); + + it("keeps invalid json error bodies as API errors", async () => { + const fetchMock = stubFetch(); + fetchMock.mockResolvedValueOnce( + textResponse("{invalid", { + status: 500, + headers: { "content-type": "application/json", "x-request-id": "req-500" }, + }) + ); + const client = new HttpClient({ apiKey: "test", maxRetries: 0 }); + + await expect( + client.requestRaw({ method: "GET", path: "/meta" }) + ).rejects.toMatchObject({ + name: "APIError", + statusCode: 500, + requestId: "req-500", + responseBody: "{invalid", + }); + }); + + it("sends raw string bodies without additional json encoding", async () => { + const fetchMock = stubFetch(); + fetchMock.mockResolvedValueOnce(jsonResponse("ok", { status: 200 })); + const client = new HttpClient({ apiKey: "test" }); + + await client.requestRaw({ + method: "POST", + path: "/meta", + data: '{"pre":"serialized"}', + headers: { "Content-Type": "application/custom+json" }, + }); + + const [, init] = getFetchCall(fetchMock); + expect(init?.body).toBe('{"pre":"serialized"}'); + expect(toHeaderRecord(init?.headers)).toMatchObject({ + "content-type": "application/custom+json", + }); + }); + + it("preserves explicit user-agent headers", async () => { + const fetchMock = stubFetch(); + fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }, { status: 200 })); + const client = new HttpClient({ apiKey: "test" }); + + await client.requestRaw({ + method: "GET", + path: "/meta", + headers: { "User-Agent": "custom-agent" }, + }); + + const [, init] = getFetchCall(fetchMock); + expect(toHeaderRecord(init?.headers)).toMatchObject({ + "user-agent": "custom-agent", + }); + }); + + it("logs requests and responses when enableLogging is true", async () => { + const fetchMock = stubFetch(); + fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }, { status: 200 })); + const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => {}); + + const client = new HttpClient({ apiKey: "test", enableLogging: true }); + await client.requestRaw({ method: "GET", path: "/meta" }); + + expect(consoleInfo).toHaveBeenCalledWith( + expect.stringContaining("dify-client-node response 200 GET") + ); + }); + + it("logs retry attempts when enableLogging is true", async () => { + const fetchMock = stubFetch(); + fetchMock + .mockRejectedValueOnce(Object.assign(new Error("timeout"), { name: "AbortError" })) + .mockResolvedValueOnce(jsonResponse("ok", { status: 200 })); + const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => {}); + + const client = new HttpClient({ + apiKey: "test", + maxRetries: 1, + retryDelay: 0, + enableLogging: true, + }); + + await client.requestRaw({ method: "GET", path: "/meta" }); + + expect(consoleInfo).toHaveBeenCalledWith( + expect.stringContaining("dify-client-node retry") + ); + }); +}); diff --git a/sdks/nodejs-client/src/http/client.ts b/sdks/nodejs-client/src/http/client.ts index 44b63c9903..c233d9807d 100644 --- a/sdks/nodejs-client/src/http/client.ts +++ b/sdks/nodejs-client/src/http/client.ts @@ -1,11 +1,4 @@ -import axios from "axios"; -import type { - AxiosError, - AxiosInstance, - AxiosRequestConfig, - AxiosResponse, -} from "axios"; -import type { Readable } from "node:stream"; +import { Readable } from "node:stream"; import { DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, @@ -13,36 +6,69 @@ import { DEFAULT_TIMEOUT_SECONDS, } from "../types/common"; import type { + BinaryStream, DifyClientConfig, DifyResponse, + DifyStream, Headers, + JsonValue, QueryParams, RequestMethod, } from "../types/common"; -import type { DifyError } from "../errors/dify-error"; import { APIError, AuthenticationError, + DifyError, FileUploadError, NetworkError, RateLimitError, TimeoutError, ValidationError, } from "../errors/dify-error"; +import type { SdkFormData } from "./form-data"; import { getFormDataHeaders, isFormData } from "./form-data"; import { createBinaryStream, createSseStream } from "./sse"; import { getRetryDelayMs, shouldRetry, sleep } from "./retry"; import { validateParams } from "../client/validation"; +import { hasStringProperty, isRecord } from "../internal/type-guards"; const DEFAULT_USER_AGENT = "dify-client-node"; -export type RequestOptions = { +export type HttpResponseType = "json" | "bytes" | "stream" | "arraybuffer"; + +export type HttpRequestBody = + | JsonValue + | Readable + | SdkFormData + | URLSearchParams + | ArrayBuffer + | ArrayBufferView + | Blob + | string + | null; + +export type ResponseDataFor = + TResponseType extends "stream" + ? Readable + : TResponseType extends "bytes" | "arraybuffer" + ? Buffer + : JsonValue | string | null; + +export type RawHttpResponse = { + data: TData; + status: number; + headers: Headers; + requestId?: string; + url: string; +}; + +export type RequestOptions = { method: RequestMethod; path: string; query?: QueryParams; - data?: unknown; + data?: HttpRequestBody; headers?: Headers; - responseType?: AxiosRequestConfig["responseType"]; + responseType?: TResponseType; }; export type HttpClientSettings = Required< @@ -51,6 +77,23 @@ export type HttpClientSettings = Required< apiKey: string; }; +type FetchRequestInit = RequestInit & { + duplex?: "half"; +}; + +type PreparedRequestBody = { + body?: BodyInit | null; + headers: Headers; + duplex?: "half"; + replayable: boolean; +}; + +type TimeoutContext = { + cleanup: () => void; + reason: Error; + signal: AbortSignal; +}; + const normalizeSettings = (config: DifyClientConfig): HttpClientSettings => ({ apiKey: config.apiKey, baseUrl: config.baseUrl ?? DEFAULT_BASE_URL, @@ -60,19 +103,10 @@ const normalizeSettings = (config: DifyClientConfig): HttpClientSettings => ({ enableLogging: config.enableLogging ?? false, }); -const normalizeHeaders = (headers: AxiosResponse["headers"]): Headers => { +const normalizeHeaders = (headers: globalThis.Headers): Headers => { const result: Headers = {}; - if (!headers) { - return result; - } - Object.entries(headers).forEach(([key, value]) => { - if (Array.isArray(value)) { - result[key.toLowerCase()] = value.join(", "); - } else if (typeof value === "string") { - result[key.toLowerCase()] = value; - } else if (typeof value === "number") { - result[key.toLowerCase()] = value.toString(); - } + headers.forEach((value, key) => { + result[key.toLowerCase()] = value; }); return result; }; @@ -80,9 +114,18 @@ const normalizeHeaders = (headers: AxiosResponse["headers"]): Headers => { const resolveRequestId = (headers: Headers): string | undefined => headers["x-request-id"] ?? headers["x-requestid"]; -const buildRequestUrl = (baseUrl: string, path: string): string => { +const buildRequestUrl = ( + baseUrl: string, + path: string, + query?: QueryParams +): string => { const trimmed = baseUrl.replace(/\/+$/, ""); - return `${trimmed}${path}`; + const url = new URL(`${trimmed}${path}`); + const queryString = buildQueryString(query); + if (queryString) { + url.search = queryString; + } + return url.toString(); }; const buildQueryString = (params?: QueryParams): string => { @@ -121,24 +164,53 @@ const parseRetryAfterSeconds = (headerValue?: string): number | undefined => { return undefined; }; -const isReadableStream = (value: unknown): value is Readable => { +const isPipeableStream = (value: unknown): value is { pipe: (destination: unknown) => unknown } => { if (!value || typeof value !== "object") { return false; } return typeof (value as { pipe?: unknown }).pipe === "function"; }; -const isUploadLikeRequest = (config?: AxiosRequestConfig): boolean => { - const url = (config?.url ?? "").toLowerCase(); - if (!url) { - return false; +const toNodeReadable = (value: unknown): Readable | null => { + if (value instanceof Readable) { + return value; } + if (!isPipeableStream(value)) { + return null; + } + const readable = new Readable({ + read() {}, + }); + return readable.wrap(value as NodeJS.ReadableStream); +}; + +const isBinaryBody = ( + value: unknown +): value is ArrayBuffer | ArrayBufferView | Blob => { + if (value instanceof Blob) { + return true; + } + if (value instanceof ArrayBuffer) { + return true; + } + return ArrayBuffer.isView(value); +}; + +const isJsonBody = (value: unknown): value is Exclude => + value === null || + typeof value === "boolean" || + typeof value === "number" || + Array.isArray(value) || + isRecord(value); + +const isUploadLikeRequest = (path: string): boolean => { + const normalizedPath = path.toLowerCase(); return ( - url.includes("upload") || - url.includes("/files/") || - url.includes("audio-to-text") || - url.includes("create_by_file") || - url.includes("update_by_file") + normalizedPath.includes("upload") || + normalizedPath.includes("/files/") || + normalizedPath.includes("audio-to-text") || + normalizedPath.includes("create_by_file") || + normalizedPath.includes("update_by_file") ); }; @@ -146,88 +218,242 @@ const resolveErrorMessage = (status: number, responseBody: unknown): string => { if (typeof responseBody === "string" && responseBody.trim().length > 0) { return responseBody; } - if ( - responseBody && - typeof responseBody === "object" && - "message" in responseBody - ) { - const message = (responseBody as Record).message; - if (typeof message === "string" && message.trim().length > 0) { + if (hasStringProperty(responseBody, "message")) { + const message = responseBody.message.trim(); + if (message.length > 0) { return message; } } return `Request failed with status code ${status}`; }; -const mapAxiosError = (error: unknown): DifyError => { - if (axios.isAxiosError(error)) { - const axiosError = error as AxiosError; - if (axiosError.response) { - const status = axiosError.response.status; - const headers = normalizeHeaders(axiosError.response.headers); - const requestId = resolveRequestId(headers); - const responseBody = axiosError.response.data; - const message = resolveErrorMessage(status, responseBody); - - if (status === 401) { - return new AuthenticationError(message, { - statusCode: status, - responseBody, - requestId, - }); - } - if (status === 429) { - const retryAfter = parseRetryAfterSeconds(headers["retry-after"]); - return new RateLimitError(message, { - statusCode: status, - responseBody, - requestId, - retryAfter, - }); - } - if (status === 422) { - return new ValidationError(message, { - statusCode: status, - responseBody, - requestId, - }); - } - if (status === 400) { - if (isUploadLikeRequest(axiosError.config)) { - return new FileUploadError(message, { - statusCode: status, - responseBody, - requestId, - }); - } - } - return new APIError(message, { - statusCode: status, - responseBody, - requestId, - }); - } - if (axiosError.code === "ECONNABORTED") { - return new TimeoutError("Request timed out", { cause: axiosError }); - } - return new NetworkError(axiosError.message, { cause: axiosError }); +const parseJsonLikeText = ( + value: string, + contentType?: string | null +): JsonValue | string | null => { + if (value.length === 0) { + return null; } + const shouldParseJson = + contentType?.includes("application/json") === true || + contentType?.includes("+json") === true; + if (!shouldParseJson) { + try { + return JSON.parse(value) as JsonValue; + } catch { + return value; + } + } + return JSON.parse(value) as JsonValue; +}; + +const prepareRequestBody = ( + method: RequestMethod, + data: HttpRequestBody | undefined +): PreparedRequestBody => { + if (method === "GET" || data === undefined) { + return { + body: undefined, + headers: {}, + replayable: true, + }; + } + + if (isFormData(data)) { + if ("getHeaders" in data && typeof data.getHeaders === "function") { + const readable = toNodeReadable(data); + if (!readable) { + throw new FileUploadError( + "Legacy FormData must be a readable stream when used with fetch" + ); + } + return { + body: Readable.toWeb(readable) as BodyInit, + headers: getFormDataHeaders(data), + duplex: "half", + replayable: false, + }; + } + return { + body: data as BodyInit, + headers: getFormDataHeaders(data), + replayable: true, + }; + } + + if (typeof data === "string") { + return { + body: data, + headers: {}, + replayable: true, + }; + } + + const readable = toNodeReadable(data); + if (readable) { + return { + body: Readable.toWeb(readable) as BodyInit, + headers: {}, + duplex: "half", + replayable: false, + }; + } + + if (data instanceof URLSearchParams || isBinaryBody(data)) { + const body = + ArrayBuffer.isView(data) && !(data instanceof Uint8Array) + ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) + : data; + return { + body: body as BodyInit, + headers: {}, + replayable: true, + }; + } + + if (isJsonBody(data)) { + return { + body: JSON.stringify(data), + headers: { + "Content-Type": "application/json", + }, + replayable: true, + }; + } + + throw new ValidationError("Unsupported request body type"); +}; + +const createTimeoutContext = (timeoutMs: number): TimeoutContext => { + const controller = new AbortController(); + const reason = new Error("Request timed out"); + const timer = setTimeout(() => { + controller.abort(reason); + }, timeoutMs); + return { + signal: controller.signal, + reason, + cleanup: () => { + clearTimeout(timer); + }, + }; +}; + +const parseResponseBody = async ( + response: Response, + responseType: TResponseType +): Promise> => { + if (responseType === "stream") { + if (!response.body) { + throw new NetworkError("Response body is empty"); + } + return Readable.fromWeb( + response.body as unknown as Parameters[0] + ) as ResponseDataFor; + } + + if (responseType === "bytes" || responseType === "arraybuffer") { + const bytes = Buffer.from(await response.arrayBuffer()); + return bytes as ResponseDataFor; + } + + if (response.status === 204 || response.status === 205 || response.status === 304) { + return null as ResponseDataFor; + } + + const text = await response.text(); + try { + return parseJsonLikeText( + text, + response.headers.get("content-type") + ) as ResponseDataFor; + } catch (error) { + if (!response.ok && error instanceof SyntaxError) { + return text as ResponseDataFor; + } + throw error; + } +}; + +const mapHttpError = ( + response: RawHttpResponse, + path: string +): DifyError => { + const status = response.status; + const responseBody = response.data; + const message = resolveErrorMessage(status, responseBody); + + if (status === 401) { + return new AuthenticationError(message, { + statusCode: status, + responseBody, + requestId: response.requestId, + }); + } + + if (status === 429) { + const retryAfter = parseRetryAfterSeconds(response.headers["retry-after"]); + return new RateLimitError(message, { + statusCode: status, + responseBody, + requestId: response.requestId, + retryAfter, + }); + } + + if (status === 422) { + return new ValidationError(message, { + statusCode: status, + responseBody, + requestId: response.requestId, + }); + } + + if (status === 400 && isUploadLikeRequest(path)) { + return new FileUploadError(message, { + statusCode: status, + responseBody, + requestId: response.requestId, + }); + } + + return new APIError(message, { + statusCode: status, + responseBody, + requestId: response.requestId, + }); +}; + +const mapTransportError = ( + error: unknown, + timeoutContext: TimeoutContext +): DifyError => { + if (error instanceof DifyError) { + return error; + } + + if ( + timeoutContext.signal.aborted && + timeoutContext.signal.reason === timeoutContext.reason + ) { + return new TimeoutError("Request timed out", { cause: error }); + } + if (error instanceof Error) { + if (error.name === "AbortError" || error.name === "TimeoutError") { + return new TimeoutError("Request timed out", { cause: error }); + } return new NetworkError(error.message, { cause: error }); } + return new NetworkError("Unexpected network error", { cause: error }); }; export class HttpClient { - private axios: AxiosInstance; private settings: HttpClientSettings; constructor(config: DifyClientConfig) { this.settings = normalizeSettings(config); - this.axios = axios.create({ - baseURL: this.settings.baseUrl, - timeout: this.settings.timeout * 1000, - }); } updateApiKey(apiKey: string): void { @@ -238,118 +464,123 @@ export class HttpClient { return { ...this.settings }; } - async request(options: RequestOptions): Promise> { + async request< + T, + TResponseType extends HttpResponseType = "json", + >(options: RequestOptions): Promise> { const response = await this.requestRaw(options); - const headers = normalizeHeaders(response.headers); return { data: response.data as T, status: response.status, - headers, - requestId: resolveRequestId(headers), + headers: response.headers, + requestId: response.requestId, }; } - async requestStream(options: RequestOptions) { + async requestStream(options: RequestOptions): Promise> { const response = await this.requestRaw({ ...options, responseType: "stream", }); - const headers = normalizeHeaders(response.headers); - return createSseStream(response.data as Readable, { + return createSseStream(response.data, { status: response.status, - headers, - requestId: resolveRequestId(headers), + headers: response.headers, + requestId: response.requestId, }); } - async requestBinaryStream(options: RequestOptions) { + async requestBinaryStream(options: RequestOptions): Promise { const response = await this.requestRaw({ ...options, responseType: "stream", }); - const headers = normalizeHeaders(response.headers); - return createBinaryStream(response.data as Readable, { + return createBinaryStream(response.data, { status: response.status, - headers, - requestId: resolveRequestId(headers), + headers: response.headers, + requestId: response.requestId, }); } - async requestRaw(options: RequestOptions): Promise { - const { method, path, query, data, headers, responseType } = options; - const { apiKey, enableLogging, maxRetries, retryDelay, timeout } = - this.settings; + async requestRaw( + options: RequestOptions + ): Promise>> { + const responseType = options.responseType ?? "json"; + const { method, path, query, data, headers } = options; + const { apiKey, enableLogging, maxRetries, retryDelay, timeout } = this.settings; if (query) { validateParams(query as Record); } - if ( - data && - typeof data === "object" && - !Array.isArray(data) && - !isFormData(data) && - !isReadableStream(data) - ) { - validateParams(data as Record); + + if (isRecord(data) && !Array.isArray(data) && !isFormData(data) && !isPipeableStream(data)) { + validateParams(data); } - const requestHeaders: Headers = { - Authorization: `Bearer ${apiKey}`, - ...headers, - }; - if ( - typeof process !== "undefined" && - !!process.versions?.node && - !requestHeaders["User-Agent"] && - !requestHeaders["user-agent"] - ) { - requestHeaders["User-Agent"] = DEFAULT_USER_AGENT; - } - - if (isFormData(data)) { - Object.assign(requestHeaders, getFormDataHeaders(data)); - } else if (data && method !== "GET") { - requestHeaders["Content-Type"] = "application/json"; - } - - const url = buildRequestUrl(this.settings.baseUrl, path); + const url = buildRequestUrl(this.settings.baseUrl, path, query); if (enableLogging) { console.info(`dify-client-node request ${method} ${url}`); } - const axiosConfig: AxiosRequestConfig = { - method, - url: path, - params: query, - paramsSerializer: { - serialize: (params) => buildQueryString(params as QueryParams), - }, - headers: requestHeaders, - responseType: responseType ?? "json", - timeout: timeout * 1000, - }; - - if (method !== "GET" && data !== undefined) { - axiosConfig.data = data; - } - let attempt = 0; - // `attempt` is a zero-based retry counter - // Total attempts = 1 (initial) + maxRetries - // e.g., maxRetries=3 means: attempt 0 (initial), then retries at 1, 2, 3 while (true) { + const preparedBody = prepareRequestBody(method, data); + const requestHeaders: Headers = { + Authorization: `Bearer ${apiKey}`, + ...preparedBody.headers, + ...headers, + }; + + if ( + typeof process !== "undefined" && + !!process.versions?.node && + !requestHeaders["User-Agent"] && + !requestHeaders["user-agent"] + ) { + requestHeaders["User-Agent"] = DEFAULT_USER_AGENT; + } + + const timeoutContext = createTimeoutContext(timeout * 1000); + const requestInit: FetchRequestInit = { + method, + headers: requestHeaders, + body: preparedBody.body, + signal: timeoutContext.signal, + }; + + if (preparedBody.duplex) { + requestInit.duplex = preparedBody.duplex; + } + try { - const response = await this.axios.request(axiosConfig); + const fetchResponse = await fetch(url, requestInit); + const responseHeaders = normalizeHeaders(fetchResponse.headers); + const parsedBody = + (await parseResponseBody(fetchResponse, responseType)) as ResponseDataFor; + const response: RawHttpResponse> = { + data: parsedBody, + status: fetchResponse.status, + headers: responseHeaders, + requestId: resolveRequestId(responseHeaders), + url, + }; + + if (!fetchResponse.ok) { + throw mapHttpError(response, path); + } + if (enableLogging) { console.info( `dify-client-node response ${response.status} ${method} ${url}` ); } + return response; } catch (error) { - const mapped = mapAxiosError(error); - if (!shouldRetry(mapped, attempt, maxRetries)) { + const mapped = mapTransportError(error, timeoutContext); + const shouldRetryRequest = + preparedBody.replayable && shouldRetry(mapped, attempt, maxRetries); + if (!shouldRetryRequest) { throw mapped; } const retryAfterSeconds = @@ -362,6 +593,8 @@ export class HttpClient { } attempt += 1; await sleep(delay); + } finally { + timeoutContext.cleanup(); } } } diff --git a/sdks/nodejs-client/src/http/form-data.test.js b/sdks/nodejs-client/src/http/form-data.test.ts similarity index 73% rename from sdks/nodejs-client/src/http/form-data.test.js rename to sdks/nodejs-client/src/http/form-data.test.ts index 2938e41435..922f220c69 100644 --- a/sdks/nodejs-client/src/http/form-data.test.js +++ b/sdks/nodejs-client/src/http/form-data.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { getFormDataHeaders, isFormData } from "./form-data"; describe("form-data helpers", () => { @@ -11,9 +11,15 @@ describe("form-data helpers", () => { expect(isFormData({})).toBe(false); }); + it("detects native FormData", () => { + const form = new FormData(); + form.append("field", "value"); + expect(isFormData(form)).toBe(true); + }); + it("returns headers from form-data", () => { const formLike = { - append: () => {}, + append: vi.fn(), getHeaders: () => ({ "content-type": "multipart/form-data" }), }; expect(getFormDataHeaders(formLike)).toEqual({ diff --git a/sdks/nodejs-client/src/http/form-data.ts b/sdks/nodejs-client/src/http/form-data.ts index 2efa23e54e..6091b7cfdd 100644 --- a/sdks/nodejs-client/src/http/form-data.ts +++ b/sdks/nodejs-client/src/http/form-data.ts @@ -1,19 +1,25 @@ import type { Headers } from "../types/common"; -export type FormDataLike = { - append: (...args: unknown[]) => void; - getHeaders?: () => Headers; +type FormDataAppendValue = Blob | string; + +export type WebFormData = FormData; + +export type LegacyNodeFormData = { + append: (name: string, value: FormDataAppendValue, fileName?: string) => void; + getHeaders: () => Headers; constructor?: { name?: string }; }; -export const isFormData = (value: unknown): value is FormDataLike => { +export type SdkFormData = WebFormData | LegacyNodeFormData; + +export const isFormData = (value: unknown): value is SdkFormData => { if (!value || typeof value !== "object") { return false; } if (typeof FormData !== "undefined" && value instanceof FormData) { return true; } - const candidate = value as FormDataLike; + const candidate = value as Partial; if (typeof candidate.append !== "function") { return false; } @@ -23,8 +29,8 @@ export const isFormData = (value: unknown): value is FormDataLike => { return candidate.constructor?.name === "FormData"; }; -export const getFormDataHeaders = (form: FormDataLike): Headers => { - if (typeof form.getHeaders === "function") { +export const getFormDataHeaders = (form: SdkFormData): Headers => { + if ("getHeaders" in form && typeof form.getHeaders === "function") { return form.getHeaders(); } return {}; diff --git a/sdks/nodejs-client/src/http/retry.test.js b/sdks/nodejs-client/src/http/retry.test.ts similarity index 94% rename from sdks/nodejs-client/src/http/retry.test.js rename to sdks/nodejs-client/src/http/retry.test.ts index fc017f631b..f53f7428b7 100644 --- a/sdks/nodejs-client/src/http/retry.test.js +++ b/sdks/nodejs-client/src/http/retry.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { getRetryDelayMs, shouldRetry } from "./retry"; import { NetworkError, RateLimitError, TimeoutError } from "../errors/dify-error"; -const withMockedRandom = (value, fn) => { +const withMockedRandom = (value: number, fn: () => void): void => { const original = Math.random; Math.random = () => value; try { diff --git a/sdks/nodejs-client/src/http/sse.test.js b/sdks/nodejs-client/src/http/sse.test.ts similarity index 73% rename from sdks/nodejs-client/src/http/sse.test.js rename to sdks/nodejs-client/src/http/sse.test.ts index fff85fd29b..70cd11007d 100644 --- a/sdks/nodejs-client/src/http/sse.test.js +++ b/sdks/nodejs-client/src/http/sse.test.ts @@ -6,10 +6,10 @@ describe("sse parsing", () => { it("parses event and data lines", async () => { const stream = Readable.from([ "event: message\n", - "data: {\"answer\":\"hi\"}\n", + 'data: {"answer":"hi"}\n', "\n", ]); - const events = []; + const events: Array<{ event?: string; data: unknown; raw: string }> = []; for await (const event of parseSseStream(stream)) { events.push(event); } @@ -20,7 +20,7 @@ describe("sse parsing", () => { it("handles multi-line data payloads", async () => { const stream = Readable.from(["data: line1\n", "data: line2\n", "\n"]); - const events = []; + const events: Array<{ event?: string; data: unknown; raw: string }> = []; for await (const event of parseSseStream(stream)) { events.push(event); } @@ -28,10 +28,28 @@ describe("sse parsing", () => { expect(events[0].data).toBe("line1\nline2"); }); + it("ignores comments and flushes the last event without a trailing separator", async () => { + const stream = Readable.from([ + Buffer.from(": keep-alive\n"), + Uint8Array.from(Buffer.from('event: message\ndata: {"delta":"hi"}\n')), + ]); + const events: Array<{ event?: string; data: unknown; raw: string }> = []; + for await (const event of parseSseStream(stream)) { + events.push(event); + } + expect(events).toEqual([ + { + event: "message", + data: { delta: "hi" }, + raw: '{"delta":"hi"}', + }, + ]); + }); + it("createSseStream exposes toText", async () => { const stream = Readable.from([ - "data: {\"answer\":\"hello\"}\n\n", - "data: {\"delta\":\" world\"}\n\n", + 'data: {"answer":"hello"}\n\n', + 'data: {"delta":" world"}\n\n', ]); const sseStream = createSseStream(stream, { status: 200, @@ -72,5 +90,6 @@ describe("sse parsing", () => { }); expect(binary.status).toBe(200); expect(binary.headers["content-type"]).toBe("audio/mpeg"); + expect(binary.toReadable()).toBe(stream); }); }); diff --git a/sdks/nodejs-client/src/http/sse.ts b/sdks/nodejs-client/src/http/sse.ts index ed5a17fe39..75a2544f71 100644 --- a/sdks/nodejs-client/src/http/sse.ts +++ b/sdks/nodejs-client/src/http/sse.ts @@ -1,12 +1,29 @@ import type { Readable } from "node:stream"; import { StringDecoder } from "node:string_decoder"; -import type { BinaryStream, DifyStream, Headers, StreamEvent } from "../types/common"; +import type { + BinaryStream, + DifyStream, + Headers, + JsonValue, + StreamEvent, +} from "../types/common"; +import { isRecord } from "../internal/type-guards"; + +const toBufferChunk = (chunk: unknown): Buffer => { + if (Buffer.isBuffer(chunk)) { + return chunk; + } + if (chunk instanceof Uint8Array) { + return Buffer.from(chunk); + } + return Buffer.from(String(chunk)); +}; const readLines = async function* (stream: Readable): AsyncIterable { const decoder = new StringDecoder("utf8"); let buffered = ""; for await (const chunk of stream) { - buffered += decoder.write(chunk as Buffer); + buffered += decoder.write(toBufferChunk(chunk)); let index = buffered.indexOf("\n"); while (index >= 0) { let line = buffered.slice(0, index); @@ -24,12 +41,12 @@ const readLines = async function* (stream: Readable): AsyncIterable { } }; -const parseMaybeJson = (value: string): unknown => { +const parseMaybeJson = (value: string): JsonValue | string | null => { if (!value) { return null; } try { - return JSON.parse(value); + return JSON.parse(value) as JsonValue; } catch { return value; } @@ -81,18 +98,17 @@ const extractTextFromEvent = (data: unknown): string => { if (typeof data === "string") { return data; } - if (!data || typeof data !== "object") { + if (!isRecord(data)) { return ""; } - const record = data as Record; - if (typeof record.answer === "string") { - return record.answer; + if (typeof data.answer === "string") { + return data.answer; } - if (typeof record.text === "string") { - return record.text; + if (typeof data.text === "string") { + return data.text; } - if (typeof record.delta === "string") { - return record.delta; + if (typeof data.delta === "string") { + return data.delta; } return ""; }; diff --git a/sdks/nodejs-client/src/index.test.js b/sdks/nodejs-client/src/index.test.js deleted file mode 100644 index 289f4d9b1b..0000000000 --- a/sdks/nodejs-client/src/index.test.js +++ /dev/null @@ -1,227 +0,0 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { ChatClient, DifyClient, WorkflowClient, BASE_URL, routes } from "./index"; -import axios from "axios"; - -const mockRequest = vi.fn(); - -const setupAxiosMock = () => { - vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); -}; - -beforeEach(() => { - vi.restoreAllMocks(); - mockRequest.mockReset(); - setupAxiosMock(); -}); - -describe("Client", () => { - it("should create a client", () => { - new DifyClient("test"); - - expect(axios.create).toHaveBeenCalledWith({ - baseURL: BASE_URL, - timeout: 60000, - }); - }); - - it("should update the api key", () => { - const difyClient = new DifyClient("test"); - difyClient.updateApiKey("test2"); - - expect(difyClient.getHttpClient().getSettings().apiKey).toBe("test2"); - }); -}); - -describe("Send Requests", () => { - it("should make a successful request to the application parameter", async () => { - const difyClient = new DifyClient("test"); - const method = "GET"; - const endpoint = routes.application.url(); - mockRequest.mockResolvedValue({ - status: 200, - data: "response", - headers: {}, - }); - - await difyClient.sendRequest(method, endpoint); - - const requestConfig = mockRequest.mock.calls[0][0]; - expect(requestConfig).toMatchObject({ - method, - url: endpoint, - params: undefined, - responseType: "json", - timeout: 60000, - }); - expect(requestConfig.headers.Authorization).toBe("Bearer test"); - }); - - it("uses the getMeta route configuration", async () => { - const difyClient = new DifyClient("test"); - mockRequest.mockResolvedValue({ status: 200, data: "ok", headers: {} }); - - await difyClient.getMeta("end-user"); - - expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({ - method: routes.getMeta.method, - url: routes.getMeta.url(), - params: { user: "end-user" }, - headers: expect.objectContaining({ - Authorization: "Bearer test", - }), - responseType: "json", - timeout: 60000, - })); - }); -}); - -describe("File uploads", () => { - const OriginalFormData = globalThis.FormData; - - beforeAll(() => { - globalThis.FormData = class FormDataMock { - append() {} - - getHeaders() { - return { - "content-type": "multipart/form-data; boundary=test", - }; - } - }; - }); - - afterAll(() => { - globalThis.FormData = OriginalFormData; - }); - - it("does not override multipart boundary headers for FormData", async () => { - const difyClient = new DifyClient("test"); - const form = new globalThis.FormData(); - mockRequest.mockResolvedValue({ status: 200, data: "ok", headers: {} }); - - await difyClient.fileUpload(form, "end-user"); - - expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({ - method: routes.fileUpload.method, - url: routes.fileUpload.url(), - params: undefined, - headers: expect.objectContaining({ - Authorization: "Bearer test", - "content-type": "multipart/form-data; boundary=test", - }), - responseType: "json", - timeout: 60000, - data: form, - })); - }); -}); - -describe("Workflow client", () => { - it("uses tasks stop path for workflow stop", async () => { - const workflowClient = new WorkflowClient("test"); - mockRequest.mockResolvedValue({ status: 200, data: "stopped", headers: {} }); - - await workflowClient.stop("task-1", "end-user"); - - expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({ - method: routes.stopWorkflow.method, - url: routes.stopWorkflow.url("task-1"), - params: undefined, - headers: expect.objectContaining({ - Authorization: "Bearer test", - "Content-Type": "application/json", - }), - responseType: "json", - timeout: 60000, - data: { user: "end-user" }, - })); - }); - - it("maps workflow log filters to service api params", async () => { - const workflowClient = new WorkflowClient("test"); - mockRequest.mockResolvedValue({ status: 200, data: "ok", headers: {} }); - - await workflowClient.getLogs({ - createdAtAfter: "2024-01-01T00:00:00Z", - createdAtBefore: "2024-01-02T00:00:00Z", - createdByEndUserSessionId: "sess-1", - createdByAccount: "acc-1", - page: 2, - limit: 10, - }); - - expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({ - method: "GET", - url: "/workflows/logs", - params: { - created_at__after: "2024-01-01T00:00:00Z", - created_at__before: "2024-01-02T00:00:00Z", - created_by_end_user_session_id: "sess-1", - created_by_account: "acc-1", - page: 2, - limit: 10, - }, - headers: expect.objectContaining({ - Authorization: "Bearer test", - }), - responseType: "json", - timeout: 60000, - })); - }); -}); - -describe("Chat client", () => { - it("places user in query for suggested messages", async () => { - const chatClient = new ChatClient("test"); - mockRequest.mockResolvedValue({ status: 200, data: "ok", headers: {} }); - - await chatClient.getSuggested("msg-1", "end-user"); - - expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({ - method: routes.getSuggested.method, - url: routes.getSuggested.url("msg-1"), - params: { user: "end-user" }, - headers: expect.objectContaining({ - Authorization: "Bearer test", - }), - responseType: "json", - timeout: 60000, - })); - }); - - it("uses last_id when listing conversations", async () => { - const chatClient = new ChatClient("test"); - mockRequest.mockResolvedValue({ status: 200, data: "ok", headers: {} }); - - await chatClient.getConversations("end-user", "last-1", 10); - - expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({ - method: routes.getConversations.method, - url: routes.getConversations.url(), - params: { user: "end-user", last_id: "last-1", limit: 10 }, - headers: expect.objectContaining({ - Authorization: "Bearer test", - }), - responseType: "json", - timeout: 60000, - })); - }); - - it("lists app feedbacks without user params", async () => { - const chatClient = new ChatClient("test"); - mockRequest.mockResolvedValue({ status: 200, data: "ok", headers: {} }); - - await chatClient.getAppFeedbacks(1, 20); - - expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({ - method: "GET", - url: "/app/feedbacks", - params: { page: 1, limit: 20 }, - headers: expect.objectContaining({ - Authorization: "Bearer test", - }), - responseType: "json", - timeout: 60000, - })); - }); -}); diff --git a/sdks/nodejs-client/src/index.test.ts b/sdks/nodejs-client/src/index.test.ts new file mode 100644 index 0000000000..d194680379 --- /dev/null +++ b/sdks/nodejs-client/src/index.test.ts @@ -0,0 +1,240 @@ +import { Readable } from "node:stream"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { BASE_URL, ChatClient, DifyClient, WorkflowClient, routes } from "./index"; + +const stubFetch = (): ReturnType => { + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + return fetchMock; +}; + +const jsonResponse = (body: unknown, init: ResponseInit = {}): Response => + new Response(JSON.stringify(body), { + status: 200, + ...init, + headers: { + "content-type": "application/json", + ...(init.headers ?? {}), + }, + }); + +describe("Client", () => { + beforeEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it("creates a client with default settings", () => { + const difyClient = new DifyClient("test"); + + expect(difyClient.getHttpClient().getSettings()).toMatchObject({ + apiKey: "test", + baseUrl: BASE_URL, + timeout: 60, + }); + }); + + it("updates the api key", () => { + const difyClient = new DifyClient("test"); + difyClient.updateApiKey("test2"); + + expect(difyClient.getHttpClient().getSettings().apiKey).toBe("test2"); + }); +}); + +describe("Send Requests", () => { + beforeEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it("makes a successful request to the application parameter route", async () => { + const fetchMock = stubFetch(); + const difyClient = new DifyClient("test"); + const method = "GET"; + const endpoint = routes.application.url(); + + fetchMock.mockResolvedValueOnce(jsonResponse("response")); + + const response = await difyClient.sendRequest(method, endpoint); + + expect(response).toMatchObject({ + status: 200, + data: "response", + headers: { + "content-type": "application/json", + }, + }); + const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; + expect(url).toBe(`${BASE_URL}${endpoint}`); + expect(init.method).toBe(method); + expect(init.headers).toMatchObject({ + Authorization: "Bearer test", + "User-Agent": "dify-client-node", + }); + }); + + it("uses the getMeta route configuration", async () => { + const fetchMock = stubFetch(); + const difyClient = new DifyClient("test"); + fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true })); + + await difyClient.getMeta("end-user"); + + const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; + expect(url).toBe(`${BASE_URL}${routes.getMeta.url()}?user=end-user`); + expect(init.method).toBe(routes.getMeta.method); + expect(init.headers).toMatchObject({ + Authorization: "Bearer test", + }); + }); +}); + +describe("File uploads", () => { + const OriginalFormData = globalThis.FormData; + + beforeAll(() => { + globalThis.FormData = class FormDataMock extends Readable { + constructor() { + super(); + } + + _read() {} + + append() {} + + getHeaders() { + return { + "content-type": "multipart/form-data; boundary=test", + }; + } + } as unknown as typeof FormData; + }); + + afterAll(() => { + globalThis.FormData = OriginalFormData; + }); + + beforeEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it("does not override multipart boundary headers for legacy FormData", async () => { + const fetchMock = stubFetch(); + const difyClient = new DifyClient("test"); + const form = new globalThis.FormData(); + fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true })); + + await difyClient.fileUpload(form, "end-user"); + + const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; + expect(url).toBe(`${BASE_URL}${routes.fileUpload.url()}`); + expect(init.method).toBe(routes.fileUpload.method); + expect(init.headers).toMatchObject({ + Authorization: "Bearer test", + "content-type": "multipart/form-data; boundary=test", + }); + expect(init.body).not.toBe(form); + expect((init as RequestInit & { duplex?: string }).duplex).toBe("half"); + }); +}); + +describe("Workflow client", () => { + beforeEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it("uses tasks stop path for workflow stop", async () => { + const fetchMock = stubFetch(); + const workflowClient = new WorkflowClient("test"); + fetchMock.mockResolvedValueOnce(jsonResponse({ result: "success" })); + + await workflowClient.stop("task-1", "end-user"); + + const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; + expect(url).toBe(`${BASE_URL}${routes.stopWorkflow.url("task-1")}`); + expect(init.method).toBe(routes.stopWorkflow.method); + expect(init.headers).toMatchObject({ + Authorization: "Bearer test", + "Content-Type": "application/json", + }); + expect(init.body).toBe(JSON.stringify({ user: "end-user" })); + }); + + it("maps workflow log filters to service api params", async () => { + const fetchMock = stubFetch(); + const workflowClient = new WorkflowClient("test"); + fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true })); + + await workflowClient.getLogs({ + createdAtAfter: "2024-01-01T00:00:00Z", + createdAtBefore: "2024-01-02T00:00:00Z", + createdByEndUserSessionId: "sess-1", + createdByAccount: "acc-1", + page: 2, + limit: 10, + }); + + const [url] = fetchMock.mock.calls[0] as [string, RequestInit]; + const parsedUrl = new URL(url); + expect(parsedUrl.origin + parsedUrl.pathname).toBe(`${BASE_URL}/workflows/logs`); + expect(parsedUrl.searchParams.get("created_at__before")).toBe( + "2024-01-02T00:00:00Z" + ); + expect(parsedUrl.searchParams.get("created_at__after")).toBe( + "2024-01-01T00:00:00Z" + ); + expect(parsedUrl.searchParams.get("created_by_end_user_session_id")).toBe( + "sess-1" + ); + expect(parsedUrl.searchParams.get("created_by_account")).toBe("acc-1"); + expect(parsedUrl.searchParams.get("page")).toBe("2"); + expect(parsedUrl.searchParams.get("limit")).toBe("10"); + }); +}); + +describe("Chat client", () => { + beforeEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it("places user in query for suggested messages", async () => { + const fetchMock = stubFetch(); + const chatClient = new ChatClient("test"); + fetchMock.mockResolvedValueOnce(jsonResponse({ result: "success", data: [] })); + + await chatClient.getSuggested("msg-1", "end-user"); + + const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; + expect(url).toBe(`${BASE_URL}${routes.getSuggested.url("msg-1")}?user=end-user`); + expect(init.method).toBe(routes.getSuggested.method); + expect(init.headers).toMatchObject({ + Authorization: "Bearer test", + }); + }); + + it("uses last_id when listing conversations", async () => { + const fetchMock = stubFetch(); + const chatClient = new ChatClient("test"); + fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true })); + + await chatClient.getConversations("end-user", "last-1", 10); + + const [url] = fetchMock.mock.calls[0] as [string, RequestInit]; + expect(url).toBe(`${BASE_URL}${routes.getConversations.url()}?user=end-user&last_id=last-1&limit=10`); + }); + + it("lists app feedbacks without user params", async () => { + const fetchMock = stubFetch(); + const chatClient = new ChatClient("test"); + fetchMock.mockResolvedValueOnce(jsonResponse({ data: [] })); + + await chatClient.getAppFeedbacks(1, 20); + + const [url] = fetchMock.mock.calls[0] as [string, RequestInit]; + expect(url).toBe(`${BASE_URL}/app/feedbacks?page=1&limit=20`); + }); +}); diff --git a/sdks/nodejs-client/src/internal/type-guards.ts b/sdks/nodejs-client/src/internal/type-guards.ts new file mode 100644 index 0000000000..3d74df00fb --- /dev/null +++ b/sdks/nodejs-client/src/internal/type-guards.ts @@ -0,0 +1,9 @@ +export const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null; + +export const hasStringProperty = < + TKey extends string, +>( + value: unknown, + key: TKey +): value is Record => isRecord(value) && typeof value[key] === "string"; diff --git a/sdks/nodejs-client/src/types/annotation.ts b/sdks/nodejs-client/src/types/annotation.ts index dcbd644dab..eda48e565c 100644 --- a/sdks/nodejs-client/src/types/annotation.ts +++ b/sdks/nodejs-client/src/types/annotation.ts @@ -15,4 +15,5 @@ export type AnnotationListOptions = { keyword?: string; }; -export type AnnotationResponse = Record; +export type AnnotationResponse = JsonObject; +import type { JsonObject } from "./common"; diff --git a/sdks/nodejs-client/src/types/chat.ts b/sdks/nodejs-client/src/types/chat.ts index 5b627f6cf6..0e714c83f9 100644 --- a/sdks/nodejs-client/src/types/chat.ts +++ b/sdks/nodejs-client/src/types/chat.ts @@ -1,17 +1,28 @@ -import type { StreamEvent } from "./common"; +import type { + DifyRequestFile, + JsonObject, + ResponseMode, + StreamEvent, +} from "./common"; export type ChatMessageRequest = { - inputs?: Record; + inputs?: JsonObject; query: string; user: string; - response_mode?: "blocking" | "streaming"; - files?: Array> | null; + response_mode?: ResponseMode; + files?: DifyRequestFile[] | null; conversation_id?: string; auto_generate_name?: boolean; workflow_id?: string; retriever_from?: "app" | "dataset"; }; -export type ChatMessageResponse = Record; +export type ChatMessageResponse = JsonObject; -export type ChatStreamEvent = StreamEvent>; +export type ChatStreamEvent = StreamEvent; + +export type ConversationSortBy = + | "created_at" + | "-created_at" + | "updated_at" + | "-updated_at"; diff --git a/sdks/nodejs-client/src/types/common.ts b/sdks/nodejs-client/src/types/common.ts index 00b0fcc756..60b1f8adf5 100644 --- a/sdks/nodejs-client/src/types/common.ts +++ b/sdks/nodejs-client/src/types/common.ts @@ -1,9 +1,18 @@ +import type { Readable } from "node:stream"; + export const DEFAULT_BASE_URL = "https://api.dify.ai/v1"; export const DEFAULT_TIMEOUT_SECONDS = 60; export const DEFAULT_MAX_RETRIES = 3; export const DEFAULT_RETRY_DELAY_SECONDS = 1; export type RequestMethod = "GET" | "POST" | "PATCH" | "PUT" | "DELETE"; +export type ResponseMode = "blocking" | "streaming"; +export type JsonPrimitive = string | number | boolean | null; +export type JsonValue = JsonPrimitive | JsonObject | JsonArray; +export type JsonObject = { + [key: string]: JsonValue; +}; +export type JsonArray = JsonValue[]; export type QueryParamValue = | string @@ -15,6 +24,13 @@ export type QueryParamValue = export type QueryParams = Record; export type Headers = Record; +export type DifyRequestFile = JsonObject; +export type SuccessResponse = { + result: "success"; +}; +export type SuggestedQuestionsResponse = SuccessResponse & { + data: string[]; +}; export type DifyClientConfig = { apiKey: string; @@ -54,18 +70,18 @@ export type StreamEvent = { }; export type DifyStream = AsyncIterable> & { - data: NodeJS.ReadableStream; + data: Readable; status: number; headers: Headers; requestId?: string; toText(): Promise; - toReadable(): NodeJS.ReadableStream; + toReadable(): Readable; }; export type BinaryStream = { - data: NodeJS.ReadableStream; + data: Readable; status: number; headers: Headers; requestId?: string; - toReadable(): NodeJS.ReadableStream; + toReadable(): Readable; }; diff --git a/sdks/nodejs-client/src/types/completion.ts b/sdks/nodejs-client/src/types/completion.ts index 4074137c5d..99b1757b66 100644 --- a/sdks/nodejs-client/src/types/completion.ts +++ b/sdks/nodejs-client/src/types/completion.ts @@ -1,13 +1,18 @@ -import type { StreamEvent } from "./common"; +import type { + DifyRequestFile, + JsonObject, + ResponseMode, + StreamEvent, +} from "./common"; export type CompletionRequest = { - inputs?: Record; - response_mode?: "blocking" | "streaming"; + inputs?: JsonObject; + response_mode?: ResponseMode; user: string; - files?: Array> | null; + files?: DifyRequestFile[] | null; retriever_from?: "app" | "dataset"; }; -export type CompletionResponse = Record; +export type CompletionResponse = JsonObject; -export type CompletionStreamEvent = StreamEvent>; +export type CompletionStreamEvent = StreamEvent; diff --git a/sdks/nodejs-client/src/types/knowledge-base.ts b/sdks/nodejs-client/src/types/knowledge-base.ts index a4ddef50ea..3180148ce7 100644 --- a/sdks/nodejs-client/src/types/knowledge-base.ts +++ b/sdks/nodejs-client/src/types/knowledge-base.ts @@ -14,7 +14,7 @@ export type DatasetCreateRequest = { external_knowledge_api_id?: string | null; provider?: string; external_knowledge_id?: string | null; - retrieval_model?: Record | null; + retrieval_model?: JsonObject | null; embedding_model?: string | null; embedding_model_provider?: string | null; }; @@ -26,9 +26,9 @@ export type DatasetUpdateRequest = { permission?: string | null; embedding_model?: string | null; embedding_model_provider?: string | null; - retrieval_model?: Record | null; + retrieval_model?: JsonObject | null; partial_member_list?: Array> | null; - external_retrieval_model?: Record | null; + external_retrieval_model?: JsonObject | null; external_knowledge_id?: string | null; external_knowledge_api_id?: string | null; }; @@ -61,12 +61,12 @@ export type DatasetTagUnbindingRequest = { export type DocumentTextCreateRequest = { name: string; text: string; - process_rule?: Record | null; + process_rule?: JsonObject | null; original_document_id?: string | null; doc_form?: string; doc_language?: string; indexing_technique?: string | null; - retrieval_model?: Record | null; + retrieval_model?: JsonObject | null; embedding_model?: string | null; embedding_model_provider?: string | null; }; @@ -74,10 +74,10 @@ export type DocumentTextCreateRequest = { export type DocumentTextUpdateRequest = { name?: string | null; text?: string | null; - process_rule?: Record | null; + process_rule?: JsonObject | null; doc_form?: string; doc_language?: string; - retrieval_model?: Record | null; + retrieval_model?: JsonObject | null; }; export type DocumentListOptions = { @@ -92,7 +92,7 @@ export type DocumentGetOptions = { }; export type SegmentCreateRequest = { - segments: Array>; + segments: JsonObject[]; }; export type SegmentUpdateRequest = { @@ -155,8 +155,8 @@ export type MetadataOperationRequest = { export type HitTestingRequest = { query?: string | null; - retrieval_model?: Record | null; - external_retrieval_model?: Record | null; + retrieval_model?: JsonObject | null; + external_retrieval_model?: JsonObject | null; attachment_ids?: string[] | null; }; @@ -165,20 +165,21 @@ export type DatasourcePluginListOptions = { }; export type DatasourceNodeRunRequest = { - inputs: Record; + inputs: JsonObject; datasource_type: string; credential_id?: string | null; is_published: boolean; }; export type PipelineRunRequest = { - inputs: Record; + inputs: JsonObject; datasource_type: string; - datasource_info_list: Array>; + datasource_info_list: JsonObject[]; start_node_id: string; is_published: boolean; - response_mode: "streaming" | "blocking"; + response_mode: ResponseMode; }; -export type KnowledgeBaseResponse = Record; -export type PipelineStreamEvent = Record; +export type KnowledgeBaseResponse = JsonObject; +export type PipelineStreamEvent = JsonObject; +import type { JsonObject, ResponseMode } from "./common"; diff --git a/sdks/nodejs-client/src/types/workflow.ts b/sdks/nodejs-client/src/types/workflow.ts index 2b507c7352..9ddedce1c2 100644 --- a/sdks/nodejs-client/src/types/workflow.ts +++ b/sdks/nodejs-client/src/types/workflow.ts @@ -1,12 +1,17 @@ -import type { StreamEvent } from "./common"; +import type { + DifyRequestFile, + JsonObject, + ResponseMode, + StreamEvent, +} from "./common"; export type WorkflowRunRequest = { - inputs?: Record; + inputs?: JsonObject; user: string; - response_mode?: "blocking" | "streaming"; - files?: Array> | null; + response_mode?: ResponseMode; + files?: DifyRequestFile[] | null; }; -export type WorkflowRunResponse = Record; +export type WorkflowRunResponse = JsonObject; -export type WorkflowStreamEvent = StreamEvent>; +export type WorkflowStreamEvent = StreamEvent; diff --git a/sdks/nodejs-client/src/types/workspace.ts b/sdks/nodejs-client/src/types/workspace.ts index 0ab6743063..5bb07ad373 100644 --- a/sdks/nodejs-client/src/types/workspace.ts +++ b/sdks/nodejs-client/src/types/workspace.ts @@ -1,2 +1,4 @@ +import type { JsonObject } from "./common"; + export type WorkspaceModelType = string; -export type WorkspaceModelsResponse = Record; +export type WorkspaceModelsResponse = JsonObject; diff --git a/sdks/nodejs-client/tests/http.integration.test.ts b/sdks/nodejs-client/tests/http.integration.test.ts new file mode 100644 index 0000000000..e73b192a67 --- /dev/null +++ b/sdks/nodejs-client/tests/http.integration.test.ts @@ -0,0 +1,137 @@ +import { createServer } from "node:http"; +import { Readable } from "node:stream"; +import type { AddressInfo } from "node:net"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { HttpClient } from "../src/http/client"; + +const readBody = async (stream: NodeJS.ReadableStream): Promise => { + const chunks: Buffer[] = []; + for await (const chunk of stream) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + return Buffer.concat(chunks); +}; + +describe("HttpClient integration", () => { + const requests: Array<{ + url: string; + method: string; + headers: Record; + body: Buffer; + }> = []; + + const server = createServer((req, res) => { + void (async () => { + const body = await readBody(req); + requests.push({ + url: req.url ?? "", + method: req.method ?? "", + headers: req.headers, + body, + }); + + if (req.url?.startsWith("/json")) { + res.writeHead(200, { "content-type": "application/json", "x-request-id": "req-json" }); + res.end(JSON.stringify({ ok: true })); + return; + } + + if (req.url === "/stream") { + res.writeHead(200, { "content-type": "text/event-stream" }); + res.end('data: {"answer":"hello"}\n\ndata: {"delta":" world"}\n\n'); + return; + } + + if (req.url === "/bytes") { + res.writeHead(200, { "content-type": "application/octet-stream" }); + res.end(Buffer.from([1, 2, 3, 4])); + return; + } + + if (req.url === "/upload-stream") { + res.writeHead(200, { "content-type": "application/json" }); + res.end(JSON.stringify({ received: body.toString("utf8") })); + return; + } + + res.writeHead(404, { "content-type": "application/json" }); + res.end(JSON.stringify({ message: "not found" })); + })(); + }); + + let client: HttpClient; + + beforeAll(async () => { + await new Promise((resolve) => { + server.listen(0, "127.0.0.1", () => resolve()); + }); + const address = server.address() as AddressInfo; + client = new HttpClient({ + apiKey: "test-key", + baseUrl: `http://127.0.0.1:${address.port}`, + maxRetries: 0, + retryDelay: 0, + }); + }); + + afterAll(async () => { + await new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); + }); + + it("uses real fetch for query serialization and json bodies", async () => { + const response = await client.request({ + method: "POST", + path: "/json", + query: { tag_ids: ["a", "b"], limit: 2 }, + data: { user: "u" }, + }); + + expect(response.requestId).toBe("req-json"); + expect(response.data).toEqual({ ok: true }); + expect(requests.at(-1)).toMatchObject({ + url: "/json?tag_ids=a&tag_ids=b&limit=2", + method: "POST", + }); + expect(requests.at(-1)?.headers.authorization).toBe("Bearer test-key"); + expect(requests.at(-1)?.headers["content-type"]).toBe("application/json"); + expect(requests.at(-1)?.body.toString("utf8")).toBe(JSON.stringify({ user: "u" })); + }); + + it("supports streaming request bodies with duplex fetch", async () => { + const response = await client.request<{ received: string }>({ + method: "POST", + path: "/upload-stream", + data: Readable.from(["hello ", "world"]), + }); + + expect(response.data).toEqual({ received: "hello world" }); + expect(requests.at(-1)?.body.toString("utf8")).toBe("hello world"); + }); + + it("parses real sse responses into text", async () => { + const stream = await client.requestStream({ + method: "GET", + path: "/stream", + }); + + await expect(stream.toText()).resolves.toBe("hello world"); + }); + + it("parses real byte responses into buffers", async () => { + const response = await client.request({ + method: "GET", + path: "/bytes", + responseType: "bytes", + }); + + expect(Array.from(response.data.values())).toEqual([1, 2, 3, 4]); + }); +}); diff --git a/sdks/nodejs-client/tests/test-utils.js b/sdks/nodejs-client/tests/test-utils.js deleted file mode 100644 index 0d42514e9a..0000000000 --- a/sdks/nodejs-client/tests/test-utils.js +++ /dev/null @@ -1,30 +0,0 @@ -import axios from "axios"; -import { vi } from "vitest"; -import { HttpClient } from "../src/http/client"; - -export const createHttpClient = (configOverrides = {}) => { - const mockRequest = vi.fn(); - vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); - const client = new HttpClient({ apiKey: "test", ...configOverrides }); - return { client, mockRequest }; -}; - -export const createHttpClientWithSpies = (configOverrides = {}) => { - const { client, mockRequest } = createHttpClient(configOverrides); - const request = vi - .spyOn(client, "request") - .mockResolvedValue({ data: "ok", status: 200, headers: {} }); - const requestStream = vi - .spyOn(client, "requestStream") - .mockResolvedValue({ data: null }); - const requestBinaryStream = vi - .spyOn(client, "requestBinaryStream") - .mockResolvedValue({ data: null }); - return { - client, - mockRequest, - request, - requestStream, - requestBinaryStream, - }; -}; diff --git a/sdks/nodejs-client/tests/test-utils.ts b/sdks/nodejs-client/tests/test-utils.ts new file mode 100644 index 0000000000..5d45629e31 --- /dev/null +++ b/sdks/nodejs-client/tests/test-utils.ts @@ -0,0 +1,48 @@ +import { vi } from "vitest"; +import { HttpClient } from "../src/http/client"; +import type { DifyClientConfig, DifyResponse } from "../src/types/common"; + +type FetchMock = ReturnType; +type RequestSpy = ReturnType; + +type HttpClientWithFetchMock = { + client: HttpClient; + fetchMock: FetchMock; +}; + +type HttpClientWithSpies = HttpClientWithFetchMock & { + request: RequestSpy; + requestStream: RequestSpy; + requestBinaryStream: RequestSpy; +}; + +export const createHttpClient = ( + configOverrides: Partial = {} +): HttpClientWithFetchMock => { + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + const client = new HttpClient({ apiKey: "test", ...configOverrides }); + return { client, fetchMock }; +}; + +export const createHttpClientWithSpies = ( + configOverrides: Partial = {} +): HttpClientWithSpies => { + const { client, fetchMock } = createHttpClient(configOverrides); + const request = vi + .spyOn(client, "request") + .mockResolvedValue({ data: "ok", status: 200, headers: {} } as DifyResponse); + const requestStream = vi + .spyOn(client, "requestStream") + .mockResolvedValue({ data: null, status: 200, headers: {} } as never); + const requestBinaryStream = vi + .spyOn(client, "requestBinaryStream") + .mockResolvedValue({ data: null, status: 200, headers: {} } as never); + return { + client, + fetchMock, + request, + requestStream, + requestBinaryStream, + }; +}; diff --git a/sdks/nodejs-client/tsconfig.json b/sdks/nodejs-client/tsconfig.json index d2da9a2a59..f6fb5e0555 100644 --- a/sdks/nodejs-client/tsconfig.json +++ b/sdks/nodejs-client/tsconfig.json @@ -3,7 +3,7 @@ "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", - "rootDir": "src", + "rootDir": ".", "outDir": "dist", "declaration": true, "declarationMap": true, @@ -13,5 +13,5 @@ "forceConsistentCasingInFileNames": true, "skipLibCheck": true }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts", "tests/**/*.ts"] } diff --git a/sdks/nodejs-client/vitest.config.ts b/sdks/nodejs-client/vitest.config.ts index 5a0a8637a2..c3132e9ecf 100644 --- a/sdks/nodejs-client/vitest.config.ts +++ b/sdks/nodejs-client/vitest.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { environment: "node", - include: ["**/*.test.js"], + include: ["**/*.test.ts"], coverage: { provider: "v8", reporter: ["text", "text-summary"], From 424d34a9c012308ba10817fe3ba990e4bcabb046 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:02:02 +0800 Subject: [PATCH 043/199] fix(ci): structure i18n sync payload and PR flow (#34342) --- .github/workflows/translate-i18n-claude.yml | 283 +++++++++++++++++--- .github/workflows/trigger-i18n-sync.yml | 100 ++++++- 2 files changed, 334 insertions(+), 49 deletions(-) diff --git a/.github/workflows/translate-i18n-claude.yml b/.github/workflows/translate-i18n-claude.yml index f3fbfe60e2..33af4f36fd 100644 --- a/.github/workflows/translate-i18n-claude.yml +++ b/.github/workflows/translate-i18n-claude.yml @@ -67,6 +67,92 @@ jobs: } " web/i18n-config/languages.ts | sed 's/[[:space:]]*$//') + generate_changes_json() { + node <<'NODE' + const { execFileSync } = require('node:child_process') + const fs = require('node:fs') + const path = require('node:path') + + const repoRoot = process.cwd() + const baseSha = process.env.BASE_SHA || '' + const headSha = process.env.HEAD_SHA || '' + const files = (process.env.CHANGED_FILES || '').split(/\s+/).filter(Boolean) + + const englishPath = fileStem => path.join(repoRoot, 'web', 'i18n', 'en-US', `${fileStem}.json`) + + const readCurrentJson = (fileStem) => { + const filePath = englishPath(fileStem) + if (!fs.existsSync(filePath)) + return null + + return JSON.parse(fs.readFileSync(filePath, 'utf8')) + } + + const readBaseJson = (fileStem) => { + if (!baseSha) + return null + + try { + const relativePath = `web/i18n/en-US/${fileStem}.json` + const content = execFileSync('git', ['show', `${baseSha}:${relativePath}`], { encoding: 'utf8' }) + return JSON.parse(content) + } + catch (error) { + return null + } + } + + const compareJson = (beforeValue, afterValue) => JSON.stringify(beforeValue) === JSON.stringify(afterValue) + + const changes = {} + + for (const fileStem of files) { + const currentJson = readCurrentJson(fileStem) + const beforeJson = readBaseJson(fileStem) || {} + const afterJson = currentJson || {} + const added = {} + const updated = {} + const deleted = [] + + for (const [key, value] of Object.entries(afterJson)) { + if (!(key in beforeJson)) { + added[key] = value + continue + } + + if (!compareJson(beforeJson[key], value)) { + updated[key] = { + before: beforeJson[key], + after: value, + } + } + } + + for (const key of Object.keys(beforeJson)) { + if (!(key in afterJson)) + deleted.push(key) + } + + changes[fileStem] = { + fileDeleted: currentJson === null, + added, + updated, + deleted, + } + } + + fs.writeFileSync( + '/tmp/i18n-changes.json', + JSON.stringify({ + baseSha, + headSha, + files, + changes, + }) + ) + NODE + } + if [ "${{ github.event_name }}" = "repository_dispatch" ]; then BASE_SHA="${{ github.event.client_payload.base_sha }}" HEAD_SHA="${{ github.event.client_payload.head_sha }}" @@ -74,12 +160,19 @@ jobs: TARGET_LANGS="$DEFAULT_TARGET_LANGS" SYNC_MODE="${{ github.event.client_payload.sync_mode || 'incremental' }}" - if [ -n "${{ github.event.client_payload.diff_base64 }}" ]; then - printf '%s' '${{ github.event.client_payload.diff_base64 }}' | base64 -d > /tmp/i18n-diff.txt - DIFF_AVAILABLE="true" + if [ -n "${{ github.event.client_payload.changes_base64 }}" ]; then + printf '%s' '${{ github.event.client_payload.changes_base64 }}' | base64 -d > /tmp/i18n-changes.json + CHANGES_AVAILABLE="true" + CHANGES_SOURCE="embedded" + elif [ -n "$BASE_SHA" ] && [ -n "$CHANGED_FILES" ]; then + export BASE_SHA HEAD_SHA CHANGED_FILES + generate_changes_json + CHANGES_AVAILABLE="true" + CHANGES_SOURCE="recomputed" else - : > /tmp/i18n-diff.txt - DIFF_AVAILABLE="false" + printf '%s' '{"baseSha":"","headSha":"","files":[],"changes":{}}' > /tmp/i18n-changes.json + CHANGES_AVAILABLE="false" + CHANGES_SOURCE="unavailable" fi else BASE_SHA="" @@ -106,16 +199,15 @@ jobs: CHANGED_FILES="" fi - if [ "$SYNC_MODE" = "incremental" ] && [ -n "$BASE_SHA" ]; then - git diff "$BASE_SHA" "$HEAD_SHA" -- 'web/i18n/en-US/*.json' > /tmp/i18n-diff.txt 2>/dev/null || : > /tmp/i18n-diff.txt + if [ "$SYNC_MODE" = "incremental" ] && [ -n "$CHANGED_FILES" ]; then + export BASE_SHA HEAD_SHA CHANGED_FILES + generate_changes_json + CHANGES_AVAILABLE="true" + CHANGES_SOURCE="local" else - : > /tmp/i18n-diff.txt - fi - - if [ -s /tmp/i18n-diff.txt ]; then - DIFF_AVAILABLE="true" - else - DIFF_AVAILABLE="false" + printf '%s' '{"baseSha":"","headSha":"","files":[],"changes":{}}' > /tmp/i18n-changes.json + CHANGES_AVAILABLE="false" + CHANGES_SOURCE="unavailable" fi fi @@ -136,7 +228,8 @@ jobs: echo "CHANGED_FILES=$CHANGED_FILES" echo "TARGET_LANGS=$TARGET_LANGS" echo "SYNC_MODE=$SYNC_MODE" - echo "DIFF_AVAILABLE=$DIFF_AVAILABLE" + echo "CHANGES_AVAILABLE=$CHANGES_AVAILABLE" + echo "CHANGES_SOURCE=$CHANGES_SOURCE" echo "FILE_ARGS=$FILE_ARGS" echo "LANG_ARGS=$LANG_ARGS" } >> "$GITHUB_OUTPUT" @@ -155,7 +248,7 @@ jobs: show_full_output: ${{ github.event_name == 'workflow_dispatch' }} prompt: | You are the i18n sync agent for the Dify repository. - Your job is to keep translations synchronized with the English source files under `${{ github.workspace }}/web/i18n/en-US/`, then open a PR with the result. + Your job is to keep translations synchronized with the English source files under `${{ github.workspace }}/web/i18n/en-US/`. Use absolute paths at all times: - Repo root: `${{ github.workspace }}` @@ -170,13 +263,15 @@ jobs: - Head SHA: `${{ steps.context.outputs.HEAD_SHA }}` - Scoped file args: `${{ steps.context.outputs.FILE_ARGS }}` - Scoped language args: `${{ steps.context.outputs.LANG_ARGS }}` - - Full English diff available: `${{ steps.context.outputs.DIFF_AVAILABLE }}` + - Structured change set available: `${{ steps.context.outputs.CHANGES_AVAILABLE }}` + - Structured change set source: `${{ steps.context.outputs.CHANGES_SOURCE }}` + - Structured change set file: `/tmp/i18n-changes.json` Tool rules: - Use Read for repository files. - Use Edit for JSON updates. - - Use Bash only for `git`, `gh`, `pnpm`, and `date`. - - Run Bash commands one by one. Do not combine commands with `&&`, `||`, pipes, or command substitution. + - Use Bash only for `pnpm`. + - Do not use Bash for `git`, `gh`, or branch management. Required execution plan: 1. Resolve target languages. @@ -187,30 +282,25 @@ jobs: - Only process the resolved target languages, never `en-US`. - Do not touch unrelated i18n files. - Do not modify `${{ github.workspace }}/web/i18n/en-US/`. - 3. Detect English changes per file. - - Treat the current English JSON files under `${{ github.workspace }}/web/i18n/en-US/` plus the scoped `i18n:check` result as the primary source of truth. - - Use `/tmp/i18n-diff.txt` only as supporting context to understand what changed between `Base SHA` and `Head SHA`. - - Never rely on diff alone when deciding final keys or values. - - Read the current English JSON file for each file in scope. - - If sync mode is `incremental` and `Base SHA` is not empty, run: - `git -C ${{ github.workspace }} show :web/i18n/en-US/.json` - - If sync mode is `full` or `Base SHA` is empty, skip historical comparison and treat the current English file as the only source of truth for structural sync. - - If the file did not exist at Base SHA, treat all current keys as ADD. - - Compare previous and current English JSON to identify: - - ADD: key only in current - - UPDATE: key exists in both and the English value changed - - DELETE: key only in previous - - If `/tmp/i18n-diff.txt` is available, read it before translating so wording changes are grounded in the full English patch, but resolve any ambiguity by trusting the actual English files and scoped checks. + 3. Resolve source changes. + - If `Structured change set available` is `true`, read `/tmp/i18n-changes.json` and use it as the source of truth for file-level and key-level changes. + - For each file entry: + - `added` contains new English keys that need translations. + - `updated` contains stale keys whose English source changed; re-translate using the `after` value. + - `deleted` contains keys that should be removed from locale files. + - `fileDeleted: true` means the English file no longer exists; remove the matching locale file if present. + - Read the current English JSON file for any file that still exists so wording, placeholders, and surrounding terminology stay accurate. + - If `Structured change set available` is `false`, treat this as a scoped full sync and use the current English files plus scoped checks as the source of truth. 4. Run a scoped pre-check before editing: - `pnpm --dir ${{ github.workspace }}/web run i18n:check ${{ steps.context.outputs.FILE_ARGS }} ${{ steps.context.outputs.LANG_ARGS }}` - Use this command as the source of truth for missing and extra keys inside the current scope. 5. Apply translations. - For every target language and scoped file: + - If `fileDeleted` is `true`, remove the locale file if it exists and skip the rest of that file. - If the locale file does not exist yet, create it with `Write` and then continue with `Edit` as needed. - ADD missing keys. - UPDATE stale translations when the English value changed. - DELETE removed keys. Prefer `pnpm --dir ${{ github.workspace }}/web run i18n:check ${{ steps.context.outputs.FILE_ARGS }} ${{ steps.context.outputs.LANG_ARGS }} --auto-remove` for extra keys so deletions stay in scope. - - For `zh-Hans` and `ja-JP`, if the locale file also changed between Base SHA and Head SHA, preserve manual translations unless they are clearly wrong for the new English value. If in doubt, keep the manual translation. - Preserve placeholders exactly: `{{variable}}`, `${variable}`, HTML tags, component tags, and variable names. - Match the existing terminology and register used by each locale. - Prefer one Edit per file when stable, but prioritize correctness over batching. @@ -218,14 +308,119 @@ jobs: - Run `pnpm --dir ${{ github.workspace }}/web lint:fix --quiet -- ` - Run `pnpm --dir ${{ github.workspace }}/web run i18n:check ${{ steps.context.outputs.FILE_ARGS }} ${{ steps.context.outputs.LANG_ARGS }}` - If verification fails, fix the remaining problems before continuing. - 7. Create a PR only when there are changes in `web/i18n/`. - - Check `git -C ${{ github.workspace }} status --porcelain -- web/i18n/` - - Create branch `chore/i18n-sync-` - - Commit message: `chore(i18n): sync translations with en-US` - - Push the branch and open a PR against `main` - - PR title: `chore(i18n): sync translations with en-US` - - PR body: summarize files, languages, sync mode, and verification commands - 8. If there are no translation changes after verification, do not create a branch, commit, or PR. + 7. Stop after the scoped locale files are updated and verification passes. + - Do not create branches, commits, or pull requests. claude_args: | - --max-turns 80 - --allowedTools "Read,Write,Edit,Bash(git *),Bash(git:*),Bash(gh *),Bash(gh:*),Bash(pnpm *),Bash(pnpm:*),Bash(date *),Bash(date:*),Glob,Grep" + --max-turns 120 + --allowedTools "Read,Write,Edit,Bash(pnpm *),Bash(pnpm:*),Glob,Grep" + + - name: Prepare branch metadata + id: pr_meta + if: steps.context.outputs.CHANGED_FILES != '' + shell: bash + run: | + if [ -z "$(git -C "${{ github.workspace }}" status --porcelain -- web/i18n/)" ]; then + echo "has_changes=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + SCOPE_HASH=$(printf '%s|%s|%s' "${{ steps.context.outputs.CHANGED_FILES }}" "${{ steps.context.outputs.TARGET_LANGS }}" "${{ steps.context.outputs.SYNC_MODE }}" | sha256sum | cut -c1-8) + HEAD_SHORT=$(printf '%s' "${{ steps.context.outputs.HEAD_SHA }}" | cut -c1-12) + BRANCH_NAME="chore/i18n-sync-${HEAD_SHORT}-${SCOPE_HASH}" + + { + echo "has_changes=true" + echo "branch_name=$BRANCH_NAME" + } >> "$GITHUB_OUTPUT" + + - name: Commit translation changes + if: steps.pr_meta.outputs.has_changes == 'true' + shell: bash + run: | + git -C "${{ github.workspace }}" checkout -B "${{ steps.pr_meta.outputs.branch_name }}" + git -C "${{ github.workspace }}" add web/i18n/ + git -C "${{ github.workspace }}" commit -m "chore(i18n): sync translations with en-US" + + - name: Push translation branch + if: steps.pr_meta.outputs.has_changes == 'true' + shell: bash + run: | + if git -C "${{ github.workspace }}" ls-remote --exit-code --heads origin "${{ steps.pr_meta.outputs.branch_name }}" >/dev/null 2>&1; then + git -C "${{ github.workspace }}" push --force-with-lease origin "${{ steps.pr_meta.outputs.branch_name }}" + else + git -C "${{ github.workspace }}" push --set-upstream origin "${{ steps.pr_meta.outputs.branch_name }}" + fi + + - name: Create or update translation PR + if: steps.pr_meta.outputs.has_changes == 'true' + env: + BRANCH_NAME: ${{ steps.pr_meta.outputs.branch_name }} + FILES_IN_SCOPE: ${{ steps.context.outputs.CHANGED_FILES }} + TARGET_LANGS: ${{ steps.context.outputs.TARGET_LANGS }} + SYNC_MODE: ${{ steps.context.outputs.SYNC_MODE }} + CHANGES_SOURCE: ${{ steps.context.outputs.CHANGES_SOURCE }} + BASE_SHA: ${{ steps.context.outputs.BASE_SHA }} + HEAD_SHA: ${{ steps.context.outputs.HEAD_SHA }} + REPO_NAME: ${{ github.repository }} + shell: bash + run: | + PR_BODY_FILE=/tmp/i18n-pr-body.md + LANG_COUNT=$(printf '%s\n' "$TARGET_LANGS" | wc -w | tr -d ' ') + if [ "$LANG_COUNT" = "0" ]; then + LANG_COUNT="0" + fi + export LANG_COUNT + + node <<'NODE' > "$PR_BODY_FILE" + const fs = require('node:fs') + + const changesPath = '/tmp/i18n-changes.json' + const changes = fs.existsSync(changesPath) + ? JSON.parse(fs.readFileSync(changesPath, 'utf8')) + : { changes: {} } + + const filesInScope = (process.env.FILES_IN_SCOPE || '').split(/\s+/).filter(Boolean) + const lines = [ + '## Summary', + '', + `- **Files synced**: \`${process.env.FILES_IN_SCOPE || ''}\``, + `- **Languages updated**: ${process.env.TARGET_LANGS || ''} (${process.env.LANG_COUNT} languages)`, + `- **Sync mode**: ${process.env.SYNC_MODE}${process.env.BASE_SHA ? ` (base: \`${process.env.BASE_SHA.slice(0, 10)}\`, head: \`${process.env.HEAD_SHA.slice(0, 10)}\`)` : ` (head: \`${process.env.HEAD_SHA.slice(0, 10)}\`)`}`, + '', + '### Key changes', + ] + + for (const fileName of filesInScope) { + const fileChange = changes.changes?.[fileName] || { added: {}, updated: {}, deleted: [], fileDeleted: false } + const addedKeys = Object.keys(fileChange.added || {}) + const updatedKeys = Object.keys(fileChange.updated || {}) + const deletedKeys = fileChange.deleted || [] + lines.push(`- \`${fileName}\`: +${addedKeys.length} / ~${updatedKeys.length} / -${deletedKeys.length}${fileChange.fileDeleted ? ' (file deleted in en-US)' : ''}`) + } + + lines.push( + '', + '## Verification', + '', + `- \`pnpm --dir web run i18n:check --file ${process.env.FILES_IN_SCOPE} --lang ${process.env.TARGET_LANGS}\``, + `- \`pnpm --dir web lint:fix --quiet -- \``, + '', + '## Notes', + '', + '- This PR was generated from structured en-US key changes produced by `trigger-i18n-sync.yml`.', + `- Structured change source: ${process.env.CHANGES_SOURCE || 'unknown'}.`, + '- Branch name is deterministic for the head SHA and scope, so reruns update the same PR instead of opening duplicates.', + '', + '🤖 Generated with [Claude Code](https://claude.com/claude-code)' + ) + + process.stdout.write(lines.join('\n')) + NODE + + EXISTING_PR_NUMBER=$(gh pr list --repo "$REPO_NAME" --head "$BRANCH_NAME" --state open --json number --jq '.[0].number') + + if [ -n "$EXISTING_PR_NUMBER" ] && [ "$EXISTING_PR_NUMBER" != "null" ]; then + gh pr edit "$EXISTING_PR_NUMBER" --repo "$REPO_NAME" --title "chore(i18n): sync translations with en-US" --body-file "$PR_BODY_FILE" + else + gh pr create --repo "$REPO_NAME" --head "$BRANCH_NAME" --base main --title "chore(i18n): sync translations with en-US" --body-file "$PR_BODY_FILE" + fi diff --git a/.github/workflows/trigger-i18n-sync.yml b/.github/workflows/trigger-i18n-sync.yml index ee44fbb0c0..a1ca42b26e 100644 --- a/.github/workflows/trigger-i18n-sync.yml +++ b/.github/workflows/trigger-i18n-sync.yml @@ -25,7 +25,7 @@ jobs: with: fetch-depth: 0 - - name: Detect changed files and generate full diff + - name: Detect changed files and build structured change set id: detect shell: bash run: | @@ -37,12 +37,94 @@ jobs: if [ -n "$BASE_SHA" ]; then CHANGED_FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- 'web/i18n/en-US/*.json' 2>/dev/null | sed -n 's@^.*/@@p' | sed 's/\.json$//' | tr '\n' ' ' | sed 's/[[:space:]]*$//') - git diff "$BASE_SHA" "$HEAD_SHA" -- 'web/i18n/en-US/*.json' > /tmp/i18n-diff.txt 2>/dev/null || : > /tmp/i18n-diff.txt else CHANGED_FILES=$(find web/i18n/en-US -maxdepth 1 -type f -name '*.json' -print | sed -n 's@^.*/@@p' | sed 's/\.json$//' | sort | tr '\n' ' ' | sed 's/[[:space:]]*$//') - : > /tmp/i18n-diff.txt fi + export BASE_SHA HEAD_SHA CHANGED_FILES + node <<'NODE' + const { execFileSync } = require('node:child_process') + const fs = require('node:fs') + const path = require('node:path') + + const repoRoot = process.cwd() + const baseSha = process.env.BASE_SHA || '' + const headSha = process.env.HEAD_SHA || '' + const files = (process.env.CHANGED_FILES || '').split(/\s+/).filter(Boolean) + + const englishPath = fileStem => path.join(repoRoot, 'web', 'i18n', 'en-US', `${fileStem}.json`) + + const readCurrentJson = (fileStem) => { + const filePath = englishPath(fileStem) + if (!fs.existsSync(filePath)) + return null + + return JSON.parse(fs.readFileSync(filePath, 'utf8')) + } + + const readBaseJson = (fileStem) => { + if (!baseSha) + return null + + try { + const relativePath = `web/i18n/en-US/${fileStem}.json` + const content = execFileSync('git', ['show', `${baseSha}:${relativePath}`], { encoding: 'utf8' }) + return JSON.parse(content) + } + catch (error) { + return null + } + } + + const compareJson = (beforeValue, afterValue) => JSON.stringify(beforeValue) === JSON.stringify(afterValue) + + const changes = {} + + for (const fileStem of files) { + const beforeJson = readBaseJson(fileStem) || {} + const afterJson = readCurrentJson(fileStem) || {} + const added = {} + const updated = {} + const deleted = [] + + for (const [key, value] of Object.entries(afterJson)) { + if (!(key in beforeJson)) { + added[key] = value + continue + } + + if (!compareJson(beforeJson[key], value)) { + updated[key] = { + before: beforeJson[key], + after: value, + } + } + } + + for (const key of Object.keys(beforeJson)) { + if (!(key in afterJson)) + deleted.push(key) + } + + changes[fileStem] = { + fileDeleted: readCurrentJson(fileStem) === null, + added, + updated, + deleted, + } + } + + fs.writeFileSync( + '/tmp/i18n-changes.json', + JSON.stringify({ + baseSha, + headSha, + files, + changes, + }) + ) + NODE + if [ -n "$CHANGED_FILES" ]; then echo "has_changes=true" >> "$GITHUB_OUTPUT" else @@ -65,7 +147,14 @@ jobs: script: | const fs = require('fs') - const diffBase64 = fs.readFileSync('/tmp/i18n-diff.txt').toString('base64') + const changesJson = fs.readFileSync('/tmp/i18n-changes.json', 'utf8') + const changesBase64 = Buffer.from(changesJson).toString('base64') + const maxEmbeddedChangesChars = 48000 + const changesEmbedded = changesBase64.length <= maxEmbeddedChangesChars + + if (!changesEmbedded) { + console.log(`Structured change set too large to embed safely (${changesBase64.length} chars). Downstream workflow will regenerate it from git history.`) + } await github.rest.repos.createDispatchEvent({ owner: context.repo.owner, @@ -73,7 +162,8 @@ jobs: event_type: 'i18n-sync', client_payload: { changed_files: process.env.CHANGED_FILES, - diff_base64: diffBase64, + changes_base64: changesEmbedded ? changesBase64 : '', + changes_embedded: changesEmbedded, sync_mode: 'incremental', base_sha: process.env.BASE_SHA, head_sha: process.env.HEAD_SHA, From 24111facdd8a209bdbcaf3a10b99be515aa8e391 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:26:22 +0000 Subject: [PATCH 044/199] chore(i18n): sync translations with en-US (#34339) Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> --- web/i18n/ar-TN/workflow.json | 3 +++ web/i18n/de-DE/workflow.json | 3 +++ web/i18n/es-ES/workflow.json | 3 +++ web/i18n/fa-IR/workflow.json | 3 +++ web/i18n/fr-FR/workflow.json | 3 +++ web/i18n/hi-IN/workflow.json | 3 +++ web/i18n/id-ID/workflow.json | 3 +++ web/i18n/it-IT/workflow.json | 3 +++ web/i18n/ja-JP/workflow.json | 3 +++ web/i18n/ko-KR/workflow.json | 3 +++ web/i18n/nl-NL/workflow.json | 3 +++ web/i18n/pl-PL/workflow.json | 3 +++ web/i18n/pt-BR/workflow.json | 3 +++ web/i18n/ro-RO/workflow.json | 3 +++ web/i18n/ru-RU/workflow.json | 3 +++ web/i18n/sl-SI/workflow.json | 3 +++ web/i18n/th-TH/workflow.json | 3 +++ web/i18n/tr-TR/workflow.json | 3 +++ web/i18n/uk-UA/workflow.json | 3 +++ web/i18n/vi-VN/workflow.json | 3 +++ web/i18n/zh-Hans/workflow.json | 3 +++ web/i18n/zh-Hant/workflow.json | 3 +++ 22 files changed, 66 insertions(+) diff --git a/web/i18n/ar-TN/workflow.json b/web/i18n/ar-TN/workflow.json index 2487538071..9396649c69 100644 --- a/web/i18n/ar-TN/workflow.json +++ b/web/i18n/ar-TN/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "تصدير القيم السرية", "env.export.export": "تصدير DSL مع القيم السرية ", "env.export.ignore": "تصدير DSL", + "env.export.name": "الاسم", + "env.export.secret": "سري", "env.export.title": "تصدير متغيرات البيئة السرية؟", + "env.export.value": "القيمة", "env.modal.description": "الوصف", "env.modal.descriptionPlaceholder": "وصف المتغير", "env.modal.editTitle": "تعديل متغير بيئة", diff --git a/web/i18n/de-DE/workflow.json b/web/i18n/de-DE/workflow.json index 362eac19b6..6648450686 100644 --- a/web/i18n/de-DE/workflow.json +++ b/web/i18n/de-DE/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Geheime Werte exportieren", "env.export.export": "DSL mit geheimen Werten exportieren", "env.export.ignore": "DSL exportieren", + "env.export.name": "Name", + "env.export.secret": "Geheim", "env.export.title": "Geheime Umgebungsvariablen exportieren?", + "env.export.value": "Wert", "env.modal.description": "Beschreibung", "env.modal.descriptionPlaceholder": "Beschreiben Sie die Variable", "env.modal.editTitle": "Umgebungsvariable bearbeiten", diff --git a/web/i18n/es-ES/workflow.json b/web/i18n/es-ES/workflow.json index 393859a36f..d23dd40a16 100644 --- a/web/i18n/es-ES/workflow.json +++ b/web/i18n/es-ES/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Exportar valores secretos", "env.export.export": "Exportar DSL con valores secretos", "env.export.ignore": "Exportar DSL", + "env.export.name": "Nombre", + "env.export.secret": "Secreto", "env.export.title": "¿Exportar variables de entorno secretas?", + "env.export.value": "Valor", "env.modal.description": "Descripción", "env.modal.descriptionPlaceholder": "Describa la variable", "env.modal.editTitle": "Editar Variable de Entorno", diff --git a/web/i18n/fa-IR/workflow.json b/web/i18n/fa-IR/workflow.json index 7a8fca11f1..1b1bf59d94 100644 --- a/web/i18n/fa-IR/workflow.json +++ b/web/i18n/fa-IR/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "خروجی مقادیر محرمانه", "env.export.export": "خروجی DSL با مقادیر محرمانه", "env.export.ignore": "خروجی DSL", + "env.export.name": "نام", + "env.export.secret": "محرمانه", "env.export.title": "آیا متغیرهای محیطی محرمانه صادر شوند؟", + "env.export.value": "مقدار", "env.modal.description": "توضیحات", "env.modal.descriptionPlaceholder": "توصیف متغیر", "env.modal.editTitle": "ویرایش متغیر محیطی", diff --git a/web/i18n/fr-FR/workflow.json b/web/i18n/fr-FR/workflow.json index 09d140445e..c172dbf41d 100644 --- a/web/i18n/fr-FR/workflow.json +++ b/web/i18n/fr-FR/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Exporter les valeurs secrètes", "env.export.export": "Exporter les DSL avec des valeurs secrètes", "env.export.ignore": "Exporter DSL", + "env.export.name": "Nom", + "env.export.secret": "Secret", "env.export.title": "Exporter des variables d'environnement secrètes?", + "env.export.value": "valeur", "env.modal.description": "Description", "env.modal.descriptionPlaceholder": "Décrivez la variable", "env.modal.editTitle": "Editer titre", diff --git a/web/i18n/hi-IN/workflow.json b/web/i18n/hi-IN/workflow.json index fb9256536e..2c14a31f55 100644 --- a/web/i18n/hi-IN/workflow.json +++ b/web/i18n/hi-IN/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "गुप्त मान निर्यात करें", "env.export.export": "गुप्त मानों के साथ DSL निर्यात करें", "env.export.ignore": "DSL निर्यात करें", + "env.export.name": "नाम", + "env.export.secret": "गुप्त", "env.export.title": "गुप्त पर्यावरण चर निर्यात करें?", + "env.export.value": "मान", "env.modal.description": "विवरण", "env.modal.descriptionPlaceholder": "चर का वर्णन करें", "env.modal.editTitle": "पर्यावरण चर संपादित करें", diff --git a/web/i18n/id-ID/workflow.json b/web/i18n/id-ID/workflow.json index 76e80be7d7..87d0415793 100644 --- a/web/i18n/id-ID/workflow.json +++ b/web/i18n/id-ID/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Mengekspor nilai rahasia", "env.export.export": "Mengekspor DSL dengan nilai rahasia", "env.export.ignore": "Ekspor DSL", + "env.export.name": "Nama", + "env.export.secret": "Rahasia", "env.export.title": "Mengekspor variabel lingkungan Rahasia?", + "env.export.value": "Nilai", "env.modal.description": "Deskripsi", "env.modal.descriptionPlaceholder": "Jelaskan variabel", "env.modal.editTitle": "Edit Variabel Lingkungan", diff --git a/web/i18n/it-IT/workflow.json b/web/i18n/it-IT/workflow.json index 519d7d7e2a..d9e802c2b6 100644 --- a/web/i18n/it-IT/workflow.json +++ b/web/i18n/it-IT/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Esporta valori segreti", "env.export.export": "Esporta DSL con valori segreti", "env.export.ignore": "Esporta DSL", + "env.export.name": "Nome", + "env.export.secret": "Segreto", "env.export.title": "Esportare variabili d'ambiente segrete?", + "env.export.value": "Valore", "env.modal.description": "Descrizione", "env.modal.descriptionPlaceholder": "Descrivi la variabile", "env.modal.editTitle": "Modifica Variabile d'Ambiente", diff --git a/web/i18n/ja-JP/workflow.json b/web/i18n/ja-JP/workflow.json index acf43b8ce8..0242249d30 100644 --- a/web/i18n/ja-JP/workflow.json +++ b/web/i18n/ja-JP/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "シークレット値を含む", "env.export.export": "シークレット値付きでエクスポート", "env.export.ignore": "DSL をエクスポート", + "env.export.name": "名前", + "env.export.secret": "シークレット", "env.export.title": "シークレット環境変数をエクスポートしますか?", + "env.export.value": "値", "env.modal.description": "説明", "env.modal.descriptionPlaceholder": "変数の説明を入力", "env.modal.editTitle": "環境変数を編集", diff --git a/web/i18n/ko-KR/workflow.json b/web/i18n/ko-KR/workflow.json index 24e8e634d3..2709fa1917 100644 --- a/web/i18n/ko-KR/workflow.json +++ b/web/i18n/ko-KR/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "비밀 값 내보내기", "env.export.export": "비밀 값이 포함된 DSL 내보내기", "env.export.ignore": "DSL 내보내기", + "env.export.name": "이름", + "env.export.secret": "비밀", "env.export.title": "비밀 환경 변수를 내보내시겠습니까?", + "env.export.value": "값", "env.modal.description": "설명", "env.modal.descriptionPlaceholder": "변수에 대해 설명하세요", "env.modal.editTitle": "환경 변수 편집", diff --git a/web/i18n/nl-NL/workflow.json b/web/i18n/nl-NL/workflow.json index 891df72387..eb18daf9f7 100644 --- a/web/i18n/nl-NL/workflow.json +++ b/web/i18n/nl-NL/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Export secret values", "env.export.export": "Export DSL with secret values ", "env.export.ignore": "Export DSL", + "env.export.name": "Naam", + "env.export.secret": "Geheim", "env.export.title": "Export Secret environment variables?", + "env.export.value": "Waarde", "env.modal.description": "Description", "env.modal.descriptionPlaceholder": "Describe the variable", "env.modal.editTitle": "Edit Environment Variable", diff --git a/web/i18n/pl-PL/workflow.json b/web/i18n/pl-PL/workflow.json index 8aad8e0b71..adb639f295 100644 --- a/web/i18n/pl-PL/workflow.json +++ b/web/i18n/pl-PL/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Eksportuj tajne wartości", "env.export.export": "Eksportuj DSL z tajnymi wartościami", "env.export.ignore": "Eksportuj DSL", + "env.export.name": "Nazwa", + "env.export.secret": "Tajny", "env.export.title": "Eksportować tajne zmienne środowiskowe?", + "env.export.value": "Wartość", "env.modal.description": "Opis", "env.modal.descriptionPlaceholder": "Opisz zmienną", "env.modal.editTitle": "Edytuj Zmienną Środowiskową", diff --git a/web/i18n/pt-BR/workflow.json b/web/i18n/pt-BR/workflow.json index dc986df82c..aebf281b34 100644 --- a/web/i18n/pt-BR/workflow.json +++ b/web/i18n/pt-BR/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Exportar valores secretos", "env.export.export": "Exportar DSL com valores secretos", "env.export.ignore": "Exportar DSL", + "env.export.name": "Nome", + "env.export.secret": "Secreto", "env.export.title": "Exportar variáveis de ambiente secretas?", + "env.export.value": "Valor", "env.modal.description": "Descrição", "env.modal.descriptionPlaceholder": "Descreva a variável", "env.modal.editTitle": "Editar Variável de Ambiente", diff --git a/web/i18n/ro-RO/workflow.json b/web/i18n/ro-RO/workflow.json index e34ba42007..ff21dbb9fc 100644 --- a/web/i18n/ro-RO/workflow.json +++ b/web/i18n/ro-RO/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Exportă valori secrete", "env.export.export": "Exportă DSL cu valori secrete", "env.export.ignore": "Exportă DSL", + "env.export.name": "Nume", + "env.export.secret": "Secret", "env.export.title": "Exportă variabile de mediu secrete?", + "env.export.value": "Valoare", "env.modal.description": "Descriere", "env.modal.descriptionPlaceholder": "Descrieți variabila", "env.modal.editTitle": "Editează Variabilă de Mediu", diff --git a/web/i18n/ru-RU/workflow.json b/web/i18n/ru-RU/workflow.json index 73cdad253a..9b302c19f6 100644 --- a/web/i18n/ru-RU/workflow.json +++ b/web/i18n/ru-RU/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Экспортировать секретные значения", "env.export.export": "Экспортировать DSL с секретными значениями ", "env.export.ignore": "Экспортировать DSL", + "env.export.name": "Имя", + "env.export.secret": "Секрет", "env.export.title": "Экспортировать секретные переменные среды?", + "env.export.value": "Значение", "env.modal.description": "Описание", "env.modal.descriptionPlaceholder": "Опишите переменную", "env.modal.editTitle": "Редактировать переменную среды", diff --git a/web/i18n/sl-SI/workflow.json b/web/i18n/sl-SI/workflow.json index a20a30753d..814fe5b117 100644 --- a/web/i18n/sl-SI/workflow.json +++ b/web/i18n/sl-SI/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Izvozi tajne vrednosti", "env.export.export": "Izvozi DSL z skrivnimi vrednostmi", "env.export.ignore": "Izvoz DSL", + "env.export.name": "Ime", + "env.export.secret": "Skrivnost", "env.export.title": "Izvozi skrivne okoljske spremenljivke?", + "env.export.value": "Vrednost", "env.modal.description": "Opis", "env.modal.descriptionPlaceholder": "Opisujte spremenljivko", "env.modal.editTitle": "Uredi okoljsko spremenljivko", diff --git a/web/i18n/th-TH/workflow.json b/web/i18n/th-TH/workflow.json index 7819e884c3..df656580ea 100644 --- a/web/i18n/th-TH/workflow.json +++ b/web/i18n/th-TH/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "ส่งออกค่าข้อมูลลับ", "env.export.export": "ส่งออก DSL ด้วยค่าลับ", "env.export.ignore": "ส่งออก DSL", + "env.export.name": "ชื่อ", + "env.export.secret": "Secret", "env.export.title": "ส่งออกตัวแปรสภาพแวดล้อม Secret หรือไม่", + "env.export.value": "ค่า", "env.modal.description": "คำอธิบาย", "env.modal.descriptionPlaceholder": "อธิบายตัวแปร", "env.modal.editTitle": "แก้ไขตัวแปรสภาพแวดล้อม", diff --git a/web/i18n/tr-TR/workflow.json b/web/i18n/tr-TR/workflow.json index 58e6afae14..847f3a61f4 100644 --- a/web/i18n/tr-TR/workflow.json +++ b/web/i18n/tr-TR/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Gizli değerleri dışa aktar", "env.export.export": "Gizli değerlerle DSL'yi dışa aktar", "env.export.ignore": "DSL'yi dışa aktar", + "env.export.name": "Ad", + "env.export.secret": "Gizli", "env.export.title": "Gizli çevre değişkenleri dışa aktarılsın mı?", + "env.export.value": "Değer", "env.modal.description": "Açıklama", "env.modal.descriptionPlaceholder": "Değişkeni açıklayın", "env.modal.editTitle": "Çevre Değişkenini Düzenle", diff --git a/web/i18n/uk-UA/workflow.json b/web/i18n/uk-UA/workflow.json index 4fa95f6d57..eaf7d551a7 100644 --- a/web/i18n/uk-UA/workflow.json +++ b/web/i18n/uk-UA/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Експортувати секретні значення", "env.export.export": "Експортувати DSL з секретними значеннями", "env.export.ignore": "Експортувати DSL", + "env.export.name": "Назва", + "env.export.secret": "Секрет", "env.export.title": "Експортувати секретні змінні середовища?", + "env.export.value": "Значення", "env.modal.description": "Опис", "env.modal.descriptionPlaceholder": "Опишіть змінну", "env.modal.editTitle": "Редагувати змінну середовища", diff --git a/web/i18n/vi-VN/workflow.json b/web/i18n/vi-VN/workflow.json index 3a8bdbaaf1..94a4dfd848 100644 --- a/web/i18n/vi-VN/workflow.json +++ b/web/i18n/vi-VN/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "Xuất giá trị bí mật", "env.export.export": "Xuất DSL với giá trị bí mật", "env.export.ignore": "Xuất DSL", + "env.export.name": "Tên", + "env.export.secret": "Bí mật", "env.export.title": "Xuất biến môi trường bí mật?", + "env.export.value": "Giá trị", "env.modal.description": "Mô tả", "env.modal.descriptionPlaceholder": "Mô tả biến", "env.modal.editTitle": "Sửa Biến Môi Trường", diff --git a/web/i18n/zh-Hans/workflow.json b/web/i18n/zh-Hans/workflow.json index 29a1f06350..e6fc7d9ba9 100644 --- a/web/i18n/zh-Hans/workflow.json +++ b/web/i18n/zh-Hans/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "导出 secret 值", "env.export.export": "导出包含 Secret 值的 DSL", "env.export.ignore": "导出 DSL", + "env.export.name": "名称", + "env.export.secret": "Secret", "env.export.title": "导出 Secret 类型环境变量?", + "env.export.value": "值", "env.modal.description": "描述", "env.modal.descriptionPlaceholder": "变量的描述", "env.modal.editTitle": "编辑环境变量", diff --git a/web/i18n/zh-Hant/workflow.json b/web/i18n/zh-Hant/workflow.json index b739984977..b7e34018d4 100644 --- a/web/i18n/zh-Hant/workflow.json +++ b/web/i18n/zh-Hant/workflow.json @@ -287,7 +287,10 @@ "env.export.checkbox": "導出機密值", "env.export.export": "導出帶有機密值的 DSL", "env.export.ignore": "導出 DSL", + "env.export.name": "名稱", + "env.export.secret": "機密", "env.export.title": "導出機密環境變數?", + "env.export.value": "值", "env.modal.description": "描述", "env.modal.descriptionPlaceholder": "描述此變數", "env.modal.editTitle": "編輯環境變數", From 90f94be2b3b30f848b9225dadd8d149151fa4ff3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:26:57 +0000 Subject: [PATCH 045/199] chore(i18n): sync translations with en-US (#34338) Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> --- web/i18n/ar-TN/common.json | 9 +++++++++ web/i18n/de-DE/common.json | 9 +++++++++ web/i18n/es-ES/common.json | 9 +++++++++ web/i18n/fa-IR/common.json | 9 +++++++++ web/i18n/fr-FR/common.json | 9 +++++++++ web/i18n/hi-IN/common.json | 9 +++++++++ web/i18n/id-ID/common.json | 9 +++++++++ web/i18n/it-IT/common.json | 9 +++++++++ web/i18n/ja-JP/common.json | 9 +++++++++ web/i18n/ko-KR/common.json | 9 +++++++++ web/i18n/nl-NL/common.json | 9 +++++++++ web/i18n/pl-PL/common.json | 9 +++++++++ web/i18n/pt-BR/common.json | 9 +++++++++ web/i18n/ro-RO/common.json | 9 +++++++++ web/i18n/ru-RU/common.json | 9 +++++++++ web/i18n/sl-SI/common.json | 9 +++++++++ web/i18n/th-TH/common.json | 9 +++++++++ web/i18n/tr-TR/common.json | 9 +++++++++ web/i18n/uk-UA/common.json | 9 +++++++++ web/i18n/vi-VN/common.json | 9 +++++++++ web/i18n/zh-Hans/common.json | 9 +++++++++ web/i18n/zh-Hant/common.json | 9 +++++++++ 22 files changed, 198 insertions(+) diff --git a/web/i18n/ar-TN/common.json b/web/i18n/ar-TN/common.json index 3bc7c05564..2d81e44a71 100644 --- a/web/i18n/ar-TN/common.json +++ b/web/i18n/ar-TN/common.json @@ -162,6 +162,15 @@ "environment.development": "تطوير", "environment.testing": "اختبار", "error": "خطأ", + "errorBoundary.componentStack": "مكدس المكون:", + "errorBoundary.details": "تفاصيل الخطأ (التطوير فقط)", + "errorBoundary.errorCount": "حدث هذا الخطأ {{count}} مرة", + "errorBoundary.fallbackTitle": "عذراً! حدث خطأ ما", + "errorBoundary.message": "حدث خطأ غير متوقع أثناء عرض هذا المكون.", + "errorBoundary.reloadPage": "إعادة تحميل الصفحة", + "errorBoundary.title": "حدث خطأ ما", + "errorBoundary.tryAgain": "حاول مجدداً", + "errorBoundary.tryAgainCompact": "حاول مجدداً", "errorMsg.fieldRequired": "{{field}} مطلوب", "errorMsg.urlError": "يجب أن يبدأ العنوان بـ http:// أو https://", "feedback.content": "محتوى التعليق", diff --git a/web/i18n/de-DE/common.json b/web/i18n/de-DE/common.json index 8639a24f3e..d33c1bcba1 100644 --- a/web/i18n/de-DE/common.json +++ b/web/i18n/de-DE/common.json @@ -162,6 +162,15 @@ "environment.development": "ENTWICKLUNG", "environment.testing": "TESTEN", "error": "Fehler", + "errorBoundary.componentStack": "Komponenten-Stack:", + "errorBoundary.details": "Fehlerdetails (Nur Entwicklung)", + "errorBoundary.errorCount": "Dieser Fehler ist {{count}} Mal aufgetreten", + "errorBoundary.fallbackTitle": "Hoppla! Etwas ist schiefgelaufen", + "errorBoundary.message": "Beim Rendern dieser Komponente ist ein unerwarteter Fehler aufgetreten.", + "errorBoundary.reloadPage": "Seite neu laden", + "errorBoundary.title": "Etwas ist schiefgelaufen", + "errorBoundary.tryAgain": "Erneut versuchen", + "errorBoundary.tryAgainCompact": "Erneut versuchen", "errorMsg.fieldRequired": "{{field}} ist erforderlich", "errorMsg.urlError": "Die URL sollte mit http:// oder https:// beginnen", "feedback.content": "Feedback-Inhalt", diff --git a/web/i18n/es-ES/common.json b/web/i18n/es-ES/common.json index 1b97ce680d..38f9d10396 100644 --- a/web/i18n/es-ES/common.json +++ b/web/i18n/es-ES/common.json @@ -162,6 +162,15 @@ "environment.development": "DESARROLLO", "environment.testing": "PRUEBAS", "error": "Error", + "errorBoundary.componentStack": "Pila de Componentes:", + "errorBoundary.details": "Detalles del Error (Solo Desarrollo)", + "errorBoundary.errorCount": "Este error ha ocurrido {{count}} veces", + "errorBoundary.fallbackTitle": "¡Vaya! Algo salió mal", + "errorBoundary.message": "Ocurrió un error inesperado al renderizar este componente.", + "errorBoundary.reloadPage": "Recargar Página", + "errorBoundary.title": "Algo salió mal", + "errorBoundary.tryAgain": "Intentar de Nuevo", + "errorBoundary.tryAgainCompact": "Intentar de nuevo", "errorMsg.fieldRequired": "{{field}} es requerido", "errorMsg.urlError": "la URL debe comenzar con http:// o https://", "feedback.content": "Contenido de retroalimentación", diff --git a/web/i18n/fa-IR/common.json b/web/i18n/fa-IR/common.json index d2b1e8158c..686a6a5997 100644 --- a/web/i18n/fa-IR/common.json +++ b/web/i18n/fa-IR/common.json @@ -162,6 +162,15 @@ "environment.development": "توسعه", "environment.testing": "آزمایشی", "error": "خطا", + "errorBoundary.componentStack": "پشته کامپوننت:", + "errorBoundary.details": "جزئیات خطا (فقط در محیط توسعه)", + "errorBoundary.errorCount": "این خطا {{count}} بار رخ داده است", + "errorBoundary.fallbackTitle": "اوه! مشکلی پیش آمد", + "errorBoundary.message": "هنگام رندر کردن این کامپوننت، یک خطای غیرمنتظره رخ داد.", + "errorBoundary.reloadPage": "بارگذاری مجدد صفحه", + "errorBoundary.title": "مشکلی پیش آمد", + "errorBoundary.tryAgain": "تلاش مجدد", + "errorBoundary.tryAgainCompact": "تلاش مجدد", "errorMsg.fieldRequired": "{{field}} الزامی است", "errorMsg.urlError": "آدرس باید با http:// یا https:// شروع شود", "feedback.content": "محتوای بازخورد", diff --git a/web/i18n/fr-FR/common.json b/web/i18n/fr-FR/common.json index 8710c04c44..a2bc856bf7 100644 --- a/web/i18n/fr-FR/common.json +++ b/web/i18n/fr-FR/common.json @@ -162,6 +162,15 @@ "environment.development": "DÉVELOPPEMENT", "environment.testing": "TESTER", "error": "Erreur", + "errorBoundary.componentStack": "Pile du Composant :", + "errorBoundary.details": "Détails de l'Erreur (Développement Uniquement)", + "errorBoundary.errorCount": "Cette erreur s'est produite {{count}} fois", + "errorBoundary.fallbackTitle": "Oups ! Quelque chose s'est mal passé", + "errorBoundary.message": "Une erreur inattendue s'est produite lors du rendu de ce composant.", + "errorBoundary.reloadPage": "Recharger la Page", + "errorBoundary.title": "Quelque chose s'est mal passé", + "errorBoundary.tryAgain": "Réessayer", + "errorBoundary.tryAgainCompact": "Réessayer", "errorMsg.fieldRequired": "{{field}} est obligatoire", "errorMsg.urlError": "L’URL doit commencer par http:// ou https://", "feedback.content": "Contenu des retours", diff --git a/web/i18n/hi-IN/common.json b/web/i18n/hi-IN/common.json index e61c96ca45..bbdd619b12 100644 --- a/web/i18n/hi-IN/common.json +++ b/web/i18n/hi-IN/common.json @@ -162,6 +162,15 @@ "environment.development": "विकास", "environment.testing": "परीक्षण", "error": "त्रुटि", + "errorBoundary.componentStack": "कंपोनेंट स्टैक:", + "errorBoundary.details": "त्रुटि विवरण (केवल डेवलपमेंट)", + "errorBoundary.errorCount": "यह त्रुटि {{count}} बार हुई है", + "errorBoundary.fallbackTitle": "उफ़! कुछ गलत हो गया", + "errorBoundary.message": "इस कंपोनेंट को रेंडर करते समय एक अप्रत्याशित त्रुटि हुई।", + "errorBoundary.reloadPage": "पेज रीलोड करें", + "errorBoundary.title": "कुछ गलत हो गया", + "errorBoundary.tryAgain": "पुनः प्रयास करें", + "errorBoundary.tryAgainCompact": "पुनः प्रयास करें", "errorMsg.fieldRequired": "{{field}} आवश्यक है", "errorMsg.urlError": "url को http:// या https:// से शुरू होना चाहिए", "feedback.content": "प्रतिक्रिया सामग्री", diff --git a/web/i18n/id-ID/common.json b/web/i18n/id-ID/common.json index 51cd429992..81245aec7b 100644 --- a/web/i18n/id-ID/common.json +++ b/web/i18n/id-ID/common.json @@ -162,6 +162,15 @@ "environment.development": "PENGEMBANGAN", "environment.testing": "PENGUJIAN", "error": "Kesalahan", + "errorBoundary.componentStack": "Tumpukan Komponen:", + "errorBoundary.details": "Detail Kesalahan (Hanya Pengembangan)", + "errorBoundary.errorCount": "Kesalahan ini telah terjadi {{count}} kali", + "errorBoundary.fallbackTitle": "Ups! Ada yang salah", + "errorBoundary.message": "Terjadi kesalahan tak terduga saat merender komponen ini.", + "errorBoundary.reloadPage": "Muat Ulang Halaman", + "errorBoundary.title": "Ada yang salah", + "errorBoundary.tryAgain": "Coba Lagi", + "errorBoundary.tryAgainCompact": "Coba lagi", "errorMsg.fieldRequired": "{{field}} wajib diisi", "errorMsg.urlError": "URL harus dimulai dengan http:// atau https://", "feedback.content": "Konten Umpan Balik", diff --git a/web/i18n/it-IT/common.json b/web/i18n/it-IT/common.json index 283c090ea8..b389ed09ef 100644 --- a/web/i18n/it-IT/common.json +++ b/web/i18n/it-IT/common.json @@ -162,6 +162,15 @@ "environment.development": "SVILUPPO", "environment.testing": "TEST", "error": "Errore", + "errorBoundary.componentStack": "Stack del Componente:", + "errorBoundary.details": "Dettagli Errore (Solo Sviluppo)", + "errorBoundary.errorCount": "Questo errore si è verificato {{count}} volte", + "errorBoundary.fallbackTitle": "Ops! Qualcosa è andato storto", + "errorBoundary.message": "Si è verificato un errore imprevisto durante il rendering di questo componente.", + "errorBoundary.reloadPage": "Ricarica Pagina", + "errorBoundary.title": "Qualcosa è andato storto", + "errorBoundary.tryAgain": "Riprova", + "errorBoundary.tryAgainCompact": "Riprova", "errorMsg.fieldRequired": "{{field}} è obbligatorio", "errorMsg.urlError": "L'URL deve iniziare con http:// o https://", "feedback.content": "Contenuto del feedback", diff --git a/web/i18n/ja-JP/common.json b/web/i18n/ja-JP/common.json index a65d8e933c..7b2e34e757 100644 --- a/web/i18n/ja-JP/common.json +++ b/web/i18n/ja-JP/common.json @@ -162,6 +162,15 @@ "environment.development": "開発", "environment.testing": "テスト", "error": "エラー", + "errorBoundary.componentStack": "コンポーネントスタック:", + "errorBoundary.details": "エラー詳細(開発環境のみ)", + "errorBoundary.errorCount": "このエラーは{{count}}回発生しました", + "errorBoundary.fallbackTitle": "おっと!問題が発生しました", + "errorBoundary.message": "このコンポーネントのレンダリング中に予期しないエラーが発生しました。", + "errorBoundary.reloadPage": "ページを再読み込み", + "errorBoundary.title": "問題が発生しました", + "errorBoundary.tryAgain": "再試行", + "errorBoundary.tryAgainCompact": "再試行", "errorMsg.fieldRequired": "{{field}}は必要です", "errorMsg.urlError": "URL は http:// または https:// で始まる必要があります", "feedback.content": "フィードバックコンテンツ", diff --git a/web/i18n/ko-KR/common.json b/web/i18n/ko-KR/common.json index e50a7c2428..bdd08a7715 100644 --- a/web/i18n/ko-KR/common.json +++ b/web/i18n/ko-KR/common.json @@ -162,6 +162,15 @@ "environment.development": "개발", "environment.testing": "테스트", "error": "오류", + "errorBoundary.componentStack": "컴포넌트 스택:", + "errorBoundary.details": "오류 세부 정보 (개발 환경 전용)", + "errorBoundary.errorCount": "이 오류가 {{count}}번 발생했습니다", + "errorBoundary.fallbackTitle": "이런! 문제가 발생했습니다", + "errorBoundary.message": "이 컴포넌트를 렌더링하는 동안 예기치 않은 오류가 발생했습니다.", + "errorBoundary.reloadPage": "페이지 새로고침", + "errorBoundary.title": "문제가 발생했습니다", + "errorBoundary.tryAgain": "다시 시도", + "errorBoundary.tryAgainCompact": "다시 시도", "errorMsg.fieldRequired": "{{field}}는 필수입니다.", "errorMsg.urlError": "URL 은 http:// 또는 https:// 로 시작해야 합니다.", "feedback.content": "피드백 내용", diff --git a/web/i18n/nl-NL/common.json b/web/i18n/nl-NL/common.json index fb1b332a0c..592b85dc63 100644 --- a/web/i18n/nl-NL/common.json +++ b/web/i18n/nl-NL/common.json @@ -162,6 +162,15 @@ "environment.development": "DEVELOPMENT", "environment.testing": "TESTING", "error": "Error", + "errorBoundary.componentStack": "Componentstack:", + "errorBoundary.details": "Foutdetails (Alleen Ontwikkeling)", + "errorBoundary.errorCount": "Deze fout is {{count}} keer opgetreden", + "errorBoundary.fallbackTitle": "Oeps! Er is iets fout gegaan", + "errorBoundary.message": "Er is een onverwachte fout opgetreden bij het renderen van dit component.", + "errorBoundary.reloadPage": "Pagina herladen", + "errorBoundary.title": "Er is iets fout gegaan", + "errorBoundary.tryAgain": "Opnieuw proberen", + "errorBoundary.tryAgainCompact": "Opnieuw proberen", "errorMsg.fieldRequired": "{{field}} is required", "errorMsg.urlError": "url should start with http:// or https://", "feedback.content": "Feedback Content", diff --git a/web/i18n/pl-PL/common.json b/web/i18n/pl-PL/common.json index 130950a57c..5e693dd2f5 100644 --- a/web/i18n/pl-PL/common.json +++ b/web/i18n/pl-PL/common.json @@ -162,6 +162,15 @@ "environment.development": "ROZWOJOWA", "environment.testing": "TESTOWANIE", "error": "Błąd", + "errorBoundary.componentStack": "Stos komponentów:", + "errorBoundary.details": "Szczegóły błędu (tylko tryb deweloperski)", + "errorBoundary.errorCount": "Ten błąd wystąpił {{count}} razy", + "errorBoundary.fallbackTitle": "Ups! Coś poszło nie tak", + "errorBoundary.message": "Wystąpił nieoczekiwany błąd podczas renderowania tego komponentu.", + "errorBoundary.reloadPage": "Odśwież stronę", + "errorBoundary.title": "Coś poszło nie tak", + "errorBoundary.tryAgain": "Spróbuj ponownie", + "errorBoundary.tryAgainCompact": "Spróbuj ponownie", "errorMsg.fieldRequired": "{{field}} jest wymagane", "errorMsg.urlError": "Adres URL powinien zaczynać się od http:// lub https://", "feedback.content": "Treść opinii", diff --git a/web/i18n/pt-BR/common.json b/web/i18n/pt-BR/common.json index 6840bb964b..2d40cf1ee7 100644 --- a/web/i18n/pt-BR/common.json +++ b/web/i18n/pt-BR/common.json @@ -162,6 +162,15 @@ "environment.development": "DESENVOLVIMENTO", "environment.testing": "TESTE", "error": "Erro", + "errorBoundary.componentStack": "Stack do Componente:", + "errorBoundary.details": "Detalhes do Erro (Somente Desenvolvimento)", + "errorBoundary.errorCount": "Este erro ocorreu {{count}} vezes", + "errorBoundary.fallbackTitle": "Ops! Algo deu errado", + "errorBoundary.message": "Ocorreu um erro inesperado ao renderizar este componente.", + "errorBoundary.reloadPage": "Recarregar Página", + "errorBoundary.title": "Algo deu errado", + "errorBoundary.tryAgain": "Tentar Novamente", + "errorBoundary.tryAgainCompact": "Tentar novamente", "errorMsg.fieldRequired": "{{field}} é obrigatório", "errorMsg.urlError": "URL deve começar com http:// ou https://", "feedback.content": "Conteúdo do feedback", diff --git a/web/i18n/ro-RO/common.json b/web/i18n/ro-RO/common.json index 306439768b..84ae4cdce0 100644 --- a/web/i18n/ro-RO/common.json +++ b/web/i18n/ro-RO/common.json @@ -162,6 +162,15 @@ "environment.development": "DEZVOLTARE", "environment.testing": "TESTARE", "error": "Eroare", + "errorBoundary.componentStack": "Stiva componentelor:", + "errorBoundary.details": "Detalii eroare (Numai în dezvoltare)", + "errorBoundary.errorCount": "Această eroare a apărut de {{count}} ori", + "errorBoundary.fallbackTitle": "Ups! Ceva a mers prost", + "errorBoundary.message": "A apărut o eroare neașteptată la redarea acestei componente.", + "errorBoundary.reloadPage": "Reîncarcă pagina", + "errorBoundary.title": "Ceva a mers prost", + "errorBoundary.tryAgain": "Încearcă din nou", + "errorBoundary.tryAgainCompact": "Încearcă din nou", "errorMsg.fieldRequired": "{{field}} este obligatoriu", "errorMsg.urlError": "URL-ul ar trebui să înceapă cu http:// sau https://", "feedback.content": "Conținut de feedback", diff --git a/web/i18n/ru-RU/common.json b/web/i18n/ru-RU/common.json index aec9a69483..ba6a3f6078 100644 --- a/web/i18n/ru-RU/common.json +++ b/web/i18n/ru-RU/common.json @@ -162,6 +162,15 @@ "environment.development": "РАЗРАБОТКА", "environment.testing": "ТЕСТИРОВАНИЕ", "error": "Ошибка", + "errorBoundary.componentStack": "Стек компонентов:", + "errorBoundary.details": "Детали ошибки (только разработка)", + "errorBoundary.errorCount": "Эта ошибка произошла {{count}} раз(а)", + "errorBoundary.fallbackTitle": "Упс! Что-то пошло не так", + "errorBoundary.message": "При рендеринге этого компонента произошла непредвиденная ошибка.", + "errorBoundary.reloadPage": "Перезагрузить страницу", + "errorBoundary.title": "Что-то пошло не так", + "errorBoundary.tryAgain": "Попробовать снова", + "errorBoundary.tryAgainCompact": "Попробовать снова", "errorMsg.fieldRequired": "{{field}} обязательно", "errorMsg.urlError": "URL должен начинаться с http:// или https://", "feedback.content": "Содержимое обратной связи", diff --git a/web/i18n/sl-SI/common.json b/web/i18n/sl-SI/common.json index 6ec4fe430c..a0e7130176 100644 --- a/web/i18n/sl-SI/common.json +++ b/web/i18n/sl-SI/common.json @@ -162,6 +162,15 @@ "environment.development": "RAZVOJ", "environment.testing": "PREIZKUŠANJE", "error": "Napaka", + "errorBoundary.componentStack": "Sklad komponent:", + "errorBoundary.details": "Podrobnosti napake (samo razvojna okolja)", + "errorBoundary.errorCount": "Ta napaka se je pojavila {{count}} krat", + "errorBoundary.fallbackTitle": "Ojoj! Nekaj je šlo narobe", + "errorBoundary.message": "Med prikazovanjem te komponente je prišlo do nepričakovane napake.", + "errorBoundary.reloadPage": "Znova naloži stran", + "errorBoundary.title": "Nekaj je šlo narobe", + "errorBoundary.tryAgain": "Poskusi znova", + "errorBoundary.tryAgainCompact": "Poskusi znova", "errorMsg.fieldRequired": "{{field}} je obvezno", "errorMsg.urlError": "url mora začeti z http:// ali https://", "feedback.content": "Vsebina povratnih informacij", diff --git a/web/i18n/th-TH/common.json b/web/i18n/th-TH/common.json index 6eed5eba93..eb45b7e796 100644 --- a/web/i18n/th-TH/common.json +++ b/web/i18n/th-TH/common.json @@ -162,6 +162,15 @@ "environment.development": "พัฒนาการ", "environment.testing": "การทดสอบ", "error": "ข้อผิดพลาด", + "errorBoundary.componentStack": "สแตกของคอมโพเนนต์:", + "errorBoundary.details": "รายละเอียดข้อผิดพลาด (สำหรับการพัฒนาเท่านั้น)", + "errorBoundary.errorCount": "ข้อผิดพลาดนี้เกิดขึ้น {{count}} ครั้ง", + "errorBoundary.fallbackTitle": "อุ๊ปส์! มีบางอย่างผิดพลาด", + "errorBoundary.message": "เกิดข้อผิดพลาดที่ไม่คาดคิดขณะแสดงผลคอมโพเนนต์นี้", + "errorBoundary.reloadPage": "โหลดหน้าใหม่", + "errorBoundary.title": "มีบางอย่างผิดพลาด", + "errorBoundary.tryAgain": "ลองอีกครั้ง", + "errorBoundary.tryAgainCompact": "ลองอีกครั้ง", "errorMsg.fieldRequired": "{{field}} เป็นสิ่งจําเป็น", "errorMsg.urlError": "url ควรขึ้นต้นด้วย http:// หรือ https://", "feedback.content": "เนื้อหาข้อเสนอแนะ", diff --git a/web/i18n/tr-TR/common.json b/web/i18n/tr-TR/common.json index 66e895fd2b..f8877e75ca 100644 --- a/web/i18n/tr-TR/common.json +++ b/web/i18n/tr-TR/common.json @@ -162,6 +162,15 @@ "environment.development": "GELİŞTİRME", "environment.testing": "TEST", "error": "Hata", + "errorBoundary.componentStack": "Bileşen Yığını:", + "errorBoundary.details": "Hata Ayrıntıları (Yalnızca Geliştirme)", + "errorBoundary.errorCount": "Bu hata {{count}} kez oluştu", + "errorBoundary.fallbackTitle": "Hay aksi! Bir şeyler ters gitti", + "errorBoundary.message": "Bu bileşen işlenirken beklenmedik bir hata oluştu.", + "errorBoundary.reloadPage": "Sayfayı Yenile", + "errorBoundary.title": "Bir şeyler ters gitti", + "errorBoundary.tryAgain": "Tekrar Dene", + "errorBoundary.tryAgainCompact": "Tekrar dene", "errorMsg.fieldRequired": "{{field}} gereklidir", "errorMsg.urlError": "URL http:// veya https:// ile başlamalıdır", "feedback.content": "Geri Bildirim İçeriği", diff --git a/web/i18n/uk-UA/common.json b/web/i18n/uk-UA/common.json index 806cbede3d..2eb457c835 100644 --- a/web/i18n/uk-UA/common.json +++ b/web/i18n/uk-UA/common.json @@ -162,6 +162,15 @@ "environment.development": "РОЗРОБКА", "environment.testing": "ТЕСТУВАННЯ", "error": "Помилка", + "errorBoundary.componentStack": "Стек компонентів:", + "errorBoundary.details": "Деталі помилки (тільки розробка)", + "errorBoundary.errorCount": "Ця помилка сталася {{count}} раз(ів)", + "errorBoundary.fallbackTitle": "Ой! Щось пішло не так", + "errorBoundary.message": "Під час відображення цього компонента сталася непередбачена помилка.", + "errorBoundary.reloadPage": "Перезавантажити сторінку", + "errorBoundary.title": "Щось пішло не так", + "errorBoundary.tryAgain": "Спробувати знову", + "errorBoundary.tryAgainCompact": "Спробувати знову", "errorMsg.fieldRequired": "{{field}} є обов'язковим", "errorMsg.urlError": "URL-адреса повинна починатися з http:// або https://", "feedback.content": "Зміст відгуку", diff --git a/web/i18n/vi-VN/common.json b/web/i18n/vi-VN/common.json index 820bfdfdab..13e74daccf 100644 --- a/web/i18n/vi-VN/common.json +++ b/web/i18n/vi-VN/common.json @@ -162,6 +162,15 @@ "environment.development": "DEVELOPMENT", "environment.testing": "TESTING", "error": "Lỗi", + "errorBoundary.componentStack": "Ngăn xếp thành phần:", + "errorBoundary.details": "Chi tiết lỗi (Chỉ dành cho phát triển)", + "errorBoundary.errorCount": "Lỗi này đã xảy ra {{count}} lần", + "errorBoundary.fallbackTitle": "Ôi! Đã xảy ra sự cố", + "errorBoundary.message": "Đã xảy ra lỗi không mong muốn khi hiển thị thành phần này.", + "errorBoundary.reloadPage": "Tải lại trang", + "errorBoundary.title": "Đã xảy ra sự cố", + "errorBoundary.tryAgain": "Thử lại", + "errorBoundary.tryAgainCompact": "Thử lại", "errorMsg.fieldRequired": "{{field}} là bắt buộc", "errorMsg.urlError": "URL phải bắt đầu bằng http:// hoặc https://", "feedback.content": "Nội dung phản hồi", diff --git a/web/i18n/zh-Hans/common.json b/web/i18n/zh-Hans/common.json index 9676b8efb2..3c406e8938 100644 --- a/web/i18n/zh-Hans/common.json +++ b/web/i18n/zh-Hans/common.json @@ -162,6 +162,15 @@ "environment.development": "开发环境", "environment.testing": "测试环境", "error": "错误", + "errorBoundary.componentStack": "组件堆栈:", + "errorBoundary.details": "错误详情(仅开发模式)", + "errorBoundary.errorCount": "此错误已发生 {{count}} 次", + "errorBoundary.fallbackTitle": "哎呀!出了点问题", + "errorBoundary.message": "渲染此组件时发生了意外错误。", + "errorBoundary.reloadPage": "重新加载页面", + "errorBoundary.title": "出了点问题", + "errorBoundary.tryAgain": "重试", + "errorBoundary.tryAgainCompact": "重试", "errorMsg.fieldRequired": "{{field}} 为必填项", "errorMsg.urlError": "url 应该以 http:// 或 https:// 开头", "feedback.content": "反馈内容", diff --git a/web/i18n/zh-Hant/common.json b/web/i18n/zh-Hant/common.json index 9317f68f82..6cabc3638f 100644 --- a/web/i18n/zh-Hant/common.json +++ b/web/i18n/zh-Hant/common.json @@ -162,6 +162,15 @@ "environment.development": "開發環境", "environment.testing": "測試環境", "error": "錯誤", + "errorBoundary.componentStack": "元件堆疊:", + "errorBoundary.details": "錯誤詳情(僅開發模式)", + "errorBoundary.errorCount": "此錯誤已發生 {{count}} 次", + "errorBoundary.fallbackTitle": "哎呀!出了點問題", + "errorBoundary.message": "渲染此元件時發生了意外錯誤。", + "errorBoundary.reloadPage": "重新載入頁面", + "errorBoundary.title": "出了點問題", + "errorBoundary.tryAgain": "重試", + "errorBoundary.tryAgainCompact": "重試", "errorMsg.fieldRequired": "{{field}} 為必填項", "errorMsg.urlError": "URL 應以 http:// 或 https:// 開頭", "feedback.content": "反饋內容", From b818cc07662adaec161367cab630c219ac6d99b5 Mon Sep 17 00:00:00 2001 From: Desel72 Date: Tue, 31 Mar 2026 16:06:42 +0300 Subject: [PATCH 046/199] test: migrate apikey controller tests to testcontainers (#34286) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../controllers/console/test_apikey.py | 153 ++++++++++++++++++ .../controllers/console/test_apikey.py | 139 ---------------- 2 files changed, 153 insertions(+), 139 deletions(-) create mode 100644 api/tests/test_containers_integration_tests/controllers/console/test_apikey.py delete mode 100644 api/tests/unit_tests/controllers/console/test_apikey.py diff --git a/api/tests/test_containers_integration_tests/controllers/console/test_apikey.py b/api/tests/test_containers_integration_tests/controllers/console/test_apikey.py new file mode 100644 index 0000000000..7df63aae1a --- /dev/null +++ b/api/tests/test_containers_integration_tests/controllers/console/test_apikey.py @@ -0,0 +1,153 @@ +"""Integration tests for console API key endpoints using testcontainers.""" + +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +import pytest +from flask.testing import FlaskClient +from sqlalchemy import delete +from sqlalchemy.orm import Session + +from models.enums import ApiTokenType +from models.model import ApiToken, App, AppMode +from tests.test_containers_integration_tests.controllers.console.helpers import ( + authenticate_console_client, + create_console_account_and_tenant, + create_console_app, +) + + +@pytest.fixture +def setup_app( + db_session_with_containers: Session, + test_client_with_containers: FlaskClient, +) -> tuple[FlaskClient, dict[str, str], App]: + """Create an authenticated client with an app for API key tests.""" + account, tenant = create_console_account_and_tenant(db_session_with_containers) + app = create_console_app(db_session_with_containers, tenant.id, account.id, AppMode.CHAT) + headers = authenticate_console_client(test_client_with_containers, account) + return test_client_with_containers, headers, app + + +@pytest.fixture(autouse=True) +def cleanup_api_tokens(db_session_with_containers: Session): + """Remove API tokens created during each test.""" + yield + db_session_with_containers.execute(delete(ApiToken)) + db_session_with_containers.commit() + + +class TestAppApiKeyListResource: + """Tests for GET/POST /apps//api-keys.""" + + def test_get_empty_keys(self, setup_app: tuple[FlaskClient, dict[str, str], App]) -> None: + client, headers, app = setup_app + resp = client.get(f"/console/api/apps/{app.id}/api-keys", headers=headers) + assert resp.status_code == 200 + assert resp.json is not None + assert resp.json["data"] == [] + + def test_create_api_key(self, setup_app: tuple[FlaskClient, dict[str, str], App]) -> None: + client, headers, app = setup_app + resp = client.post(f"/console/api/apps/{app.id}/api-keys", headers=headers) + assert resp.status_code == 201 + data = resp.json + assert data is not None + assert data["token"].startswith("app-") + assert data["id"] is not None + + def test_get_keys_after_create(self, setup_app: tuple[FlaskClient, dict[str, str], App]) -> None: + client, headers, app = setup_app + client.post(f"/console/api/apps/{app.id}/api-keys", headers=headers) + client.post(f"/console/api/apps/{app.id}/api-keys", headers=headers) + + resp = client.get(f"/console/api/apps/{app.id}/api-keys", headers=headers) + assert resp.status_code == 200 + assert resp.json is not None + assert len(resp.json["data"]) == 2 + + def test_create_key_max_limit( + self, + setup_app: tuple[FlaskClient, dict[str, str], App], + db_session_with_containers: Session, + ) -> None: + client, headers, app = setup_app + # Create 10 keys (the max) + for _ in range(10): + client.post(f"/console/api/apps/{app.id}/api-keys", headers=headers) + + # 11th should fail + resp = client.post(f"/console/api/apps/{app.id}/api-keys", headers=headers) + assert resp.status_code == 400 + + def test_get_keys_for_nonexistent_app( + self, + setup_app: tuple[FlaskClient, dict[str, str], App], + ) -> None: + client, headers, _ = setup_app + resp = client.get( + "/console/api/apps/00000000-0000-0000-0000-000000000000/api-keys", + headers=headers, + ) + assert resp.status_code == 404 + + +class TestAppApiKeyResource: + """Tests for DELETE /apps//api-keys/.""" + + def test_delete_key_success(self, setup_app: tuple[FlaskClient, dict[str, str], App]) -> None: + client, headers, app = setup_app + create_resp = client.post(f"/console/api/apps/{app.id}/api-keys", headers=headers) + assert create_resp.json is not None + key_id = create_resp.json["id"] + + resp = client.delete(f"/console/api/apps/{app.id}/api-keys/{key_id}", headers=headers) + assert resp.status_code == 204 + + def test_delete_nonexistent_key(self, setup_app: tuple[FlaskClient, dict[str, str], App]) -> None: + client, headers, app = setup_app + resp = client.delete( + f"/console/api/apps/{app.id}/api-keys/00000000-0000-0000-0000-000000000000", + headers=headers, + ) + assert resp.status_code == 404 + + def test_delete_key_nonexistent_app( + self, + setup_app: tuple[FlaskClient, dict[str, str], App], + ) -> None: + client, headers, _ = setup_app + resp = client.delete( + "/console/api/apps/00000000-0000-0000-0000-000000000000/api-keys/00000000-0000-0000-0000-000000000000", + headers=headers, + ) + assert resp.status_code == 404 + + def test_delete_forbidden_for_non_admin( + self, + flask_app_with_containers, + ) -> None: + """A non-admin member cannot delete API keys via the controller permission check.""" + from werkzeug.exceptions import Forbidden + + from controllers.console.apikey import BaseApiKeyResource + + resource = BaseApiKeyResource() + resource.resource_type = ApiTokenType.APP + resource.resource_model = MagicMock() + resource.resource_id_field = "app_id" + + non_admin = MagicMock() + non_admin.is_admin_or_owner = False + + with ( + flask_app_with_containers.test_request_context("/"), + patch( + "controllers.console.apikey.current_account_with_tenant", + return_value=(non_admin, "tenant-id"), + ), + patch("controllers.console.apikey._get_resource"), + ): + with pytest.raises(Forbidden): + BaseApiKeyResource.delete(resource, "rid", "kid") diff --git a/api/tests/unit_tests/controllers/console/test_apikey.py b/api/tests/unit_tests/controllers/console/test_apikey.py deleted file mode 100644 index 2dff9c4037..0000000000 --- a/api/tests/unit_tests/controllers/console/test_apikey.py +++ /dev/null @@ -1,139 +0,0 @@ -from unittest.mock import MagicMock, patch - -import pytest -from werkzeug.exceptions import Forbidden - -from controllers.console.apikey import ( - BaseApiKeyListResource, - BaseApiKeyResource, - _get_resource, -) -from models.enums import ApiTokenType - - -@pytest.fixture -def tenant_context_admin(): - with patch("controllers.console.apikey.current_account_with_tenant") as mock: - user = MagicMock() - user.is_admin_or_owner = True - mock.return_value = (user, "tenant-123") - yield mock - - -@pytest.fixture -def tenant_context_non_admin(): - with patch("controllers.console.apikey.current_account_with_tenant") as mock: - user = MagicMock() - user.is_admin_or_owner = False - mock.return_value = (user, "tenant-123") - yield mock - - -@pytest.fixture -def db_mock(): - with patch("controllers.console.apikey.db") as mock_db: - mock_db.session = MagicMock() - yield mock_db - - -@pytest.fixture(autouse=True) -def bypass_permissions(): - with patch( - "controllers.console.apikey.edit_permission_required", - lambda f: f, - ): - yield - - -class DummyApiKeyListResource(BaseApiKeyListResource): - resource_type = ApiTokenType.APP - resource_model = MagicMock() - resource_id_field = "app_id" - token_prefix = "app-" - - -class DummyApiKeyResource(BaseApiKeyResource): - resource_type = ApiTokenType.APP - resource_model = MagicMock() - resource_id_field = "app_id" - - -class TestGetResource: - def test_get_resource_success(self): - fake_resource = MagicMock() - - with ( - patch("controllers.console.apikey.select") as mock_select, - patch("controllers.console.apikey.Session") as mock_session, - patch("controllers.console.apikey.db") as mock_db, - ): - mock_db.engine = MagicMock() - mock_select.return_value.filter_by.return_value = MagicMock() - - session = mock_session.return_value.__enter__.return_value - session.execute.return_value.scalar_one_or_none.return_value = fake_resource - - result = _get_resource("rid", "tid", MagicMock) - assert result == fake_resource - - def test_get_resource_not_found(self): - with ( - patch("controllers.console.apikey.select") as mock_select, - patch("controllers.console.apikey.Session") as mock_session, - patch("controllers.console.apikey.db") as mock_db, - patch("controllers.console.apikey.flask_restx.abort") as abort, - ): - mock_db.engine = MagicMock() - mock_select.return_value.filter_by.return_value = MagicMock() - - session = mock_session.return_value.__enter__.return_value - session.execute.return_value.scalar_one_or_none.return_value = None - - _get_resource("rid", "tid", MagicMock) - - abort.assert_called_once() - - -class TestBaseApiKeyListResource: - def test_get_apikeys_success(self, tenant_context_admin, db_mock): - resource = DummyApiKeyListResource() - - with patch("controllers.console.apikey._get_resource"): - db_mock.session.scalars.return_value.all.return_value = [MagicMock(), MagicMock()] - - result = DummyApiKeyListResource.get.__wrapped__(resource, "resource-id") - assert "items" in result - - -class TestBaseApiKeyResource: - def test_delete_forbidden(self, tenant_context_non_admin, db_mock): - resource = DummyApiKeyResource() - - with patch("controllers.console.apikey._get_resource"): - with pytest.raises(Forbidden): - DummyApiKeyResource.delete(resource, "rid", "kid") - - def test_delete_key_not_found(self, tenant_context_admin, db_mock): - resource = DummyApiKeyResource() - db_mock.session.scalar.return_value = None - - with patch("controllers.console.apikey._get_resource"): - with pytest.raises(Exception) as exc_info: - DummyApiKeyResource.delete(resource, "rid", "kid") - - # flask_restx.abort raises HTTPException with message in data attribute - assert exc_info.value.data["message"] == "API key not found" - - def test_delete_success(self, tenant_context_admin, db_mock): - resource = DummyApiKeyResource() - db_mock.session.scalar.return_value = MagicMock() - - with ( - patch("controllers.console.apikey._get_resource"), - patch("controllers.console.apikey.ApiTokenCache.delete"), - ): - result, status = DummyApiKeyResource.delete(resource, "rid", "kid") - - assert status == 204 - assert result == {"result": "success"} - db_mock.session.commit.assert_called_once() From d9a0665b2c8bfee584c2caf59f0312d1e5ace4f1 Mon Sep 17 00:00:00 2001 From: Desel72 Date: Tue, 31 Mar 2026 16:09:18 +0300 Subject: [PATCH 047/199] refactor: use sessionmaker().begin() in console datasets controllers (#34283) --- .../console/datasets/data_source.py | 6 ++--- .../datasets/rag_pipeline/rag_pipeline.py | 4 +-- .../rag_pipeline/rag_pipeline_datasets.py | 4 +-- .../rag_pipeline_draft_variable.py | 8 +++--- .../rag_pipeline/rag_pipeline_import.py | 12 ++++----- .../rag_pipeline/rag_pipeline_workflow.py | 16 ++++-------- .../console/datasets/test_data_source.py | 26 +++++++++---------- 7 files changed, 34 insertions(+), 42 deletions(-) diff --git a/api/controllers/console/datasets/data_source.py b/api/controllers/console/datasets/data_source.py index daef4e005a..ac14349045 100644 --- a/api/controllers/console/datasets/data_source.py +++ b/api/controllers/console/datasets/data_source.py @@ -6,7 +6,7 @@ from flask import request from flask_restx import Resource, fields, marshal_with from pydantic import BaseModel, Field from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import NotFound from controllers.common.schema import get_or_create_model, register_schema_model @@ -159,7 +159,7 @@ class DataSourceApi(Resource): @account_initialization_required def patch(self, binding_id, action: Literal["enable", "disable"]): binding_id = str(binding_id) - with Session(db.engine) as session: + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: data_source_binding = session.execute( select(DataSourceOauthBinding).filter_by(id=binding_id) ).scalar_one_or_none() @@ -211,7 +211,7 @@ class DataSourceNotionListApi(Resource): if not credential: raise NotFound("Credential not found.") exist_page_ids = [] - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: # import notion in the exist dataset if query.dataset_id: dataset = DatasetService.get_dataset(query.dataset_id) diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline.py index 4f31093cfe..1758bad31d 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline.py @@ -3,7 +3,7 @@ import logging from flask import request from flask_restx import Resource from pydantic import BaseModel, Field -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from controllers.common.schema import register_schema_models from controllers.console import console_ns @@ -85,7 +85,7 @@ class CustomizedPipelineTemplateApi(Resource): @account_initialization_required @enterprise_license_required def post(self, template_id: str): - with Session(db.engine) as session: + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: template = ( session.query(PipelineCustomizedTemplate).where(PipelineCustomizedTemplate.id == template_id).first() ) diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_datasets.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_datasets.py index e65cb19b39..a6ca0689d0 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_datasets.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_datasets.py @@ -1,6 +1,6 @@ from flask_restx import Resource, marshal from pydantic import BaseModel -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import Forbidden import services @@ -54,7 +54,7 @@ class CreateRagPipelineDatasetApi(Resource): yaml_content=payload.yaml_content, ) try: - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: rag_pipeline_dsl_service = RagPipelineDslService(session) import_info = rag_pipeline_dsl_service.create_rag_pipeline_dataset( tenant_id=current_tenant_id, diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py index f12cbd3495..d635dcb530 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py @@ -5,7 +5,7 @@ from flask import Response, request from flask_restx import Resource, marshal, marshal_with from graphon.variables.types import SegmentType from pydantic import BaseModel, Field -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import Forbidden from controllers.common.schema import register_schema_models @@ -96,7 +96,7 @@ class RagPipelineVariableCollectionApi(Resource): raise DraftWorkflowNotExist() # fetch draft workflow by app_model - with Session(bind=db.engine, expire_on_commit=False) as session: + with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session: draft_var_srv = WorkflowDraftVariableService( session=session, ) @@ -143,7 +143,7 @@ class RagPipelineNodeVariableCollectionApi(Resource): @marshal_with(workflow_draft_variable_list_model) def get(self, pipeline: Pipeline, node_id: str): validate_node_id(node_id) - with Session(bind=db.engine, expire_on_commit=False) as session: + with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session: draft_var_srv = WorkflowDraftVariableService( session=session, ) @@ -289,7 +289,7 @@ class RagPipelineVariableResetApi(Resource): def _get_variable_list(pipeline: Pipeline, node_id) -> WorkflowDraftVariableList: - with Session(bind=db.engine, expire_on_commit=False) as session: + with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session: draft_var_srv = WorkflowDraftVariableService( session=session, ) diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_import.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_import.py index af142b4646..732a6dc446 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_import.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_import.py @@ -1,7 +1,7 @@ from flask import request from flask_restx import Resource, fields, marshal_with # type: ignore from pydantic import BaseModel, Field -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from controllers.common.schema import get_or_create_model, register_schema_models from controllers.console import console_ns @@ -68,7 +68,7 @@ class RagPipelineImportApi(Resource): payload = RagPipelineImportPayload.model_validate(console_ns.payload or {}) # Create service with session - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: import_service = RagPipelineDslService(session) # Import app account = current_user @@ -80,7 +80,6 @@ class RagPipelineImportApi(Resource): pipeline_id=payload.pipeline_id, dataset_name=payload.name, ) - session.commit() # Return appropriate status code based on result status = result.status @@ -102,12 +101,11 @@ class RagPipelineImportConfirmApi(Resource): current_user, _ = current_account_with_tenant() # Create service with session - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: import_service = RagPipelineDslService(session) # Confirm import account = current_user result = import_service.confirm_import(import_id=import_id, account=account) - session.commit() # Return appropriate status code based on result if result.status == ImportStatus.FAILED: @@ -124,7 +122,7 @@ class RagPipelineImportCheckDependenciesApi(Resource): @edit_permission_required @marshal_with(pipeline_import_check_dependencies_model) def get(self, pipeline: Pipeline): - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: import_service = RagPipelineDslService(session) result = import_service.check_dependencies(pipeline=pipeline) @@ -142,7 +140,7 @@ class RagPipelineExportApi(Resource): # Add include_secret params query = IncludeSecretQuery.model_validate(request.args.to_dict()) - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: export_service = RagPipelineDslService(session) result = export_service.export_rag_pipeline_dsl( pipeline=pipeline, include_secret=query.include_secret == "true" diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py index 8efb59a8e9..e08cb155b6 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py @@ -6,7 +6,7 @@ from flask import abort, request from flask_restx import Resource, marshal_with # type: ignore from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, Forbidden, InternalServerError, NotFound import services @@ -608,7 +608,7 @@ class PublishedRagPipelineApi(Resource): # The role of the current user in the ta table must be admin, owner, or editor current_user, _ = current_account_with_tenant() rag_pipeline_service = RagPipelineService() - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: pipeline = session.merge(pipeline) workflow = rag_pipeline_service.publish_workflow( session=session, @@ -620,8 +620,6 @@ class PublishedRagPipelineApi(Resource): session.add(pipeline) workflow_created_at = TimestampField().format(workflow.created_at) - session.commit() - return { "result": "success", "created_at": workflow_created_at, @@ -695,7 +693,7 @@ class PublishedAllRagPipelineApi(Resource): raise Forbidden() rag_pipeline_service = RagPipelineService() - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: workflows, has_more = rag_pipeline_service.get_all_published_workflow( session=session, pipeline=pipeline, @@ -767,7 +765,7 @@ class RagPipelineByIdApi(Resource): rag_pipeline_service = RagPipelineService() # Create a session and manage the transaction - with Session(db.engine, expire_on_commit=False) as session: + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: workflow = rag_pipeline_service.update_workflow( session=session, workflow_id=workflow_id, @@ -779,9 +777,6 @@ class RagPipelineByIdApi(Resource): if not workflow: raise NotFound("Workflow not found") - # Commit the transaction in the controller - session.commit() - return workflow @setup_required @@ -798,14 +793,13 @@ class RagPipelineByIdApi(Resource): workflow_service = WorkflowService() - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: try: workflow_service.delete_workflow( session=session, workflow_id=workflow_id, tenant_id=pipeline.tenant_id, ) - session.commit() except WorkflowInUseError as e: abort(400, description=str(e)) except DraftWorkflowDeletionError as e: diff --git a/api/tests/test_containers_integration_tests/controllers/console/datasets/test_data_source.py b/api/tests/test_containers_integration_tests/controllers/console/datasets/test_data_source.py index 1c07d4ca1c..1c4c6a899f 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/datasets/test_data_source.py +++ b/api/tests/test_containers_integration_tests/controllers/console/datasets/test_data_source.py @@ -102,12 +102,12 @@ class TestDataSourceApi: with ( app.test_request_context("/"), - patch("controllers.console.datasets.data_source.Session") as mock_session_class, + patch("controllers.console.datasets.data_source.sessionmaker") as mock_session_class, patch("controllers.console.datasets.data_source.db.session.add"), patch("controllers.console.datasets.data_source.db.session.commit"), ): mock_session = MagicMock() - mock_session_class.return_value.__enter__.return_value = mock_session + mock_session_class.return_value.begin.return_value.__enter__.return_value = mock_session mock_session.execute.return_value.scalar_one_or_none.return_value = binding response, status = method(api, "b1", "enable") @@ -123,12 +123,12 @@ class TestDataSourceApi: with ( app.test_request_context("/"), - patch("controllers.console.datasets.data_source.Session") as mock_session_class, + patch("controllers.console.datasets.data_source.sessionmaker") as mock_session_class, patch("controllers.console.datasets.data_source.db.session.add"), patch("controllers.console.datasets.data_source.db.session.commit"), ): mock_session = MagicMock() - mock_session_class.return_value.__enter__.return_value = mock_session + mock_session_class.return_value.begin.return_value.__enter__.return_value = mock_session mock_session.execute.return_value.scalar_one_or_none.return_value = binding response, status = method(api, "b1", "disable") @@ -142,10 +142,10 @@ class TestDataSourceApi: with ( app.test_request_context("/"), - patch("controllers.console.datasets.data_source.Session") as mock_session_class, + patch("controllers.console.datasets.data_source.sessionmaker") as mock_session_class, ): mock_session = MagicMock() - mock_session_class.return_value.__enter__.return_value = mock_session + mock_session_class.return_value.begin.return_value.__enter__.return_value = mock_session mock_session.execute.return_value.scalar_one_or_none.return_value = None with pytest.raises(NotFound): @@ -159,10 +159,10 @@ class TestDataSourceApi: with ( app.test_request_context("/"), - patch("controllers.console.datasets.data_source.Session") as mock_session_class, + patch("controllers.console.datasets.data_source.sessionmaker") as mock_session_class, ): mock_session = MagicMock() - mock_session_class.return_value.__enter__.return_value = mock_session + mock_session_class.return_value.begin.return_value.__enter__.return_value = mock_session mock_session.execute.return_value.scalar_one_or_none.return_value = binding with pytest.raises(ValueError): @@ -176,10 +176,10 @@ class TestDataSourceApi: with ( app.test_request_context("/"), - patch("controllers.console.datasets.data_source.Session") as mock_session_class, + patch("controllers.console.datasets.data_source.sessionmaker") as mock_session_class, ): mock_session = MagicMock() - mock_session_class.return_value.__enter__.return_value = mock_session + mock_session_class.return_value.begin.return_value.__enter__.return_value = mock_session mock_session.execute.return_value.scalar_one_or_none.return_value = binding with pytest.raises(ValueError): @@ -282,7 +282,7 @@ class TestDataSourceNotionListApi: "controllers.console.datasets.data_source.DatasetService.get_dataset", return_value=dataset, ), - patch("controllers.console.datasets.data_source.Session") as mock_session_class, + patch("controllers.console.datasets.data_source.sessionmaker") as mock_session_class, patch( "core.datasource.datasource_manager.DatasourceManager.get_datasource_runtime", return_value=MagicMock( @@ -292,7 +292,7 @@ class TestDataSourceNotionListApi: ), ): mock_session = MagicMock() - mock_session_class.return_value.__enter__.return_value = mock_session + mock_session_class.return_value.begin.return_value.__enter__.return_value = mock_session mock_session.scalars.return_value.all.return_value = [document] response, status = method(api) @@ -315,7 +315,7 @@ class TestDataSourceNotionListApi: "controllers.console.datasets.data_source.DatasetService.get_dataset", return_value=dataset, ), - patch("controllers.console.datasets.data_source.Session"), + patch("controllers.console.datasets.data_source.sessionmaker"), ): with pytest.raises(ValueError): method(api) From cf50d7c7b52449bd3c8ee40b8b0f80b3663b485e Mon Sep 17 00:00:00 2001 From: Desel72 Date: Tue, 31 Mar 2026 16:10:16 +0300 Subject: [PATCH 048/199] refactor: use sessionmaker().begin() in console app controllers (#34282) Co-authored-by: Asuka Minato --- api/controllers/console/app/app.py | 5 ++- api/controllers/console/app/app_import.py | 10 +++--- .../console/app/conversation_variables.py | 4 +-- api/controllers/console/app/workflow.py | 17 +++------- .../console/app/workflow_app_log.py | 6 ++-- .../console/app/workflow_draft_variable.py | 8 ++--- .../console/app/workflow_trigger.py | 11 +++---- .../controllers/console/app/test_app_apis.py | 33 +++++++++++++++---- 8 files changed, 51 insertions(+), 43 deletions(-) diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 738e77b371..ec56cd3baa 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -9,7 +9,7 @@ from graphon.enums import WorkflowExecutionStatus from graphon.file import helpers as file_helpers from pydantic import AliasChoices, BaseModel, ConfigDict, Field, computed_field, field_validator from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest from controllers.common.helpers import FileInfo @@ -642,7 +642,7 @@ class AppCopyApi(Resource): args = CopyAppPayload.model_validate(console_ns.payload or {}) - with Session(db.engine) as session: + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: import_service = AppDslService(session) yaml_content = import_service.export_dsl(app_model=app_model, include_secret=True) result = import_service.import_app( @@ -655,7 +655,6 @@ class AppCopyApi(Resource): icon=args.icon, icon_background=args.icon_background, ) - session.commit() # Inherit web app permission from original app if result.app_id and FeatureService.get_system_features().webapp_auth.enabled: diff --git a/api/controllers/console/app/app_import.py b/api/controllers/console/app/app_import.py index fdef54ba5a..16e1fa3245 100644 --- a/api/controllers/console/app/app_import.py +++ b/api/controllers/console/app/app_import.py @@ -1,6 +1,6 @@ from flask_restx import Resource, fields, marshal_with from pydantic import BaseModel, Field -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( @@ -71,7 +71,7 @@ class AppImportApi(Resource): args = AppImportPayload.model_validate(console_ns.payload) # Create service with session - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: import_service = AppDslService(session) # Import app account = current_user @@ -87,7 +87,6 @@ class AppImportApi(Resource): icon_background=args.icon_background, app_id=args.app_id, ) - session.commit() if result.app_id and FeatureService.get_system_features().webapp_auth.enabled: # update web app setting as private EnterpriseService.WebAppAuth.update_app_access_mode(result.app_id, "private") @@ -112,12 +111,11 @@ class AppImportConfirmApi(Resource): current_user, _ = current_account_with_tenant() # Create service with session - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: import_service = AppDslService(session) # Confirm import account = current_user result = import_service.confirm_import(import_id=import_id, account=account) - session.commit() # Return appropriate status code based on result if result.status == ImportStatus.FAILED: @@ -134,7 +132,7 @@ class AppImportCheckDependenciesApi(Resource): @marshal_with(app_import_check_dependencies_model) @edit_permission_required def get(self, app_model: App): - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: import_service = AppDslService(session) result = import_service.check_dependencies(app_model=app_model) diff --git a/api/controllers/console/app/conversation_variables.py b/api/controllers/console/app/conversation_variables.py index 368a6112ba..369c26a80c 100644 --- a/api/controllers/console/app/conversation_variables.py +++ b/api/controllers/console/app/conversation_variables.py @@ -2,7 +2,7 @@ from flask import request from flask_restx import Resource, fields, marshal_with from pydantic import BaseModel, Field from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from controllers.console import console_ns from controllers.console.app.wraps import get_app_model @@ -69,7 +69,7 @@ class ConversationVariablesApi(Resource): page_size = 100 stmt = stmt.limit(page_size).offset((page - 1) * page_size) - with Session(db.engine) as session: + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: rows = session.scalars(stmt).all() return { diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index 1f5a84c0b2..6df8f7032e 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -10,7 +10,7 @@ from graphon.file import File from graphon.graph_engine.manager import GraphEngineManager from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field, field_validator -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, Forbidden, InternalServerError, NotFound import services @@ -840,7 +840,7 @@ class PublishedWorkflowApi(Resource): args = PublishWorkflowPayload.model_validate(console_ns.payload or {}) workflow_service = WorkflowService() - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: workflow = workflow_service.publish_workflow( session=session, app_model=app_model, @@ -858,8 +858,6 @@ class PublishedWorkflowApi(Resource): workflow_created_at = TimestampField().format(workflow.created_at) - session.commit() - return { "result": "success", "created_at": workflow_created_at, @@ -982,7 +980,7 @@ class PublishedAllWorkflowApi(Resource): raise Forbidden() workflow_service = WorkflowService() - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: workflows, has_more = workflow_service.get_all_published_workflow( session=session, app_model=app_model, @@ -1072,7 +1070,7 @@ class WorkflowByIdApi(Resource): workflow_service = WorkflowService() # Create a session and manage the transaction - with Session(db.engine, expire_on_commit=False) as session: + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: workflow = workflow_service.update_workflow( session=session, workflow_id=workflow_id, @@ -1084,9 +1082,6 @@ class WorkflowByIdApi(Resource): if not workflow: raise NotFound("Workflow not found") - # Commit the transaction in the controller - session.commit() - return workflow @setup_required @@ -1101,13 +1096,11 @@ class WorkflowByIdApi(Resource): workflow_service = WorkflowService() # Create a session and manage the transaction - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: try: workflow_service.delete_workflow( session=session, workflow_id=workflow_id, tenant_id=app_model.tenant_id ) - # Commit the transaction in the controller - session.commit() except WorkflowInUseError as e: abort(400, description=str(e)) except DraftWorkflowDeletionError as e: diff --git a/api/controllers/console/app/workflow_app_log.py b/api/controllers/console/app/workflow_app_log.py index f0e26c86a5..3b24c2a402 100644 --- a/api/controllers/console/app/workflow_app_log.py +++ b/api/controllers/console/app/workflow_app_log.py @@ -5,7 +5,7 @@ from flask import request from flask_restx import Resource, marshal_with from graphon.enums import WorkflowExecutionStatus from pydantic import BaseModel, Field, field_validator -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from controllers.console import console_ns from controllers.console.app.wraps import get_app_model @@ -87,7 +87,7 @@ class WorkflowAppLogApi(Resource): # get paginate workflow app logs workflow_app_service = WorkflowAppService() - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: workflow_app_log_pagination = workflow_app_service.get_paginate_workflow_app_logs( session=session, app_model=app_model, @@ -124,7 +124,7 @@ class WorkflowArchivedLogApi(Resource): args = WorkflowAppLogQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore workflow_app_service = WorkflowAppService() - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: workflow_app_log_pagination = workflow_app_service.get_paginate_workflow_archive_logs( session=session, app_model=app_model, diff --git a/api/controllers/console/app/workflow_draft_variable.py b/api/controllers/console/app/workflow_draft_variable.py index 4052897e9a..35e2df847c 100644 --- a/api/controllers/console/app/workflow_draft_variable.py +++ b/api/controllers/console/app/workflow_draft_variable.py @@ -10,7 +10,7 @@ from graphon.variables.segment_group import SegmentGroup from graphon.variables.segments import ArrayFileSegment, FileSegment, Segment from graphon.variables.types import SegmentType from pydantic import BaseModel, Field -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from controllers.console import console_ns from controllers.console.app.error import ( @@ -244,7 +244,7 @@ class WorkflowVariableCollectionApi(Resource): raise DraftWorkflowNotExist() # fetch draft workflow by app_model - with Session(bind=db.engine, expire_on_commit=False) as session: + with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session: draft_var_srv = WorkflowDraftVariableService( session=session, ) @@ -298,7 +298,7 @@ class NodeVariableCollectionApi(Resource): @marshal_with(workflow_draft_variable_list_model) def get(self, app_model: App, node_id: str): validate_node_id(node_id) - with Session(bind=db.engine, expire_on_commit=False) as session: + with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session: draft_var_srv = WorkflowDraftVariableService( session=session, ) @@ -465,7 +465,7 @@ class VariableResetApi(Resource): def _get_variable_list(app_model: App, node_id) -> WorkflowDraftVariableList: - with Session(bind=db.engine, expire_on_commit=False) as session: + with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session: draft_var_srv = WorkflowDraftVariableService( session=session, ) diff --git a/api/controllers/console/app/workflow_trigger.py b/api/controllers/console/app/workflow_trigger.py index 8236e766ae..aa37d24738 100644 --- a/api/controllers/console/app/workflow_trigger.py +++ b/api/controllers/console/app/workflow_trigger.py @@ -4,7 +4,7 @@ from flask import request from flask_restx import Resource, fields, marshal_with from pydantic import BaseModel from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import NotFound from configs import dify_config @@ -64,7 +64,7 @@ class WebhookTriggerApi(Resource): node_id = args.node_id - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: # Get webhook trigger for this app and node webhook_trigger = ( session.query(WorkflowWebhookTrigger) @@ -95,7 +95,7 @@ class AppTriggersApi(Resource): assert isinstance(current_user, Account) assert current_user.current_tenant_id is not None - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: # Get all triggers for this app using select API triggers = ( session.execute( @@ -137,7 +137,7 @@ class AppTriggerEnableApi(Resource): assert current_user.current_tenant_id is not None trigger_id = args.trigger_id - with Session(db.engine) as session: + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: # Find the trigger using select trigger = session.execute( select(AppTrigger).where( @@ -153,9 +153,6 @@ class AppTriggerEnableApi(Resource): # Update status based on enable_trigger boolean trigger.status = AppTriggerStatus.ENABLED if args.enable_trigger else AppTriggerStatus.DISABLED - session.commit() - session.refresh(trigger) - # Add computed icon field url_prefix = dify_config.CONSOLE_API_URL + "/console/api/workspaces/current/tool-provider/builtin/" if trigger.trigger_type == "trigger-plugin": diff --git a/api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py b/api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py index fbaec069bb..0841217fcf 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py +++ b/api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py @@ -383,14 +383,21 @@ class TestWorkflowAppLogEndpoints: monkeypatch.setattr(workflow_app_log_module, "db", SimpleNamespace(engine=MagicMock())) - class DummySession: + class DummySessionCtx: def __enter__(self): return "session" def __exit__(self, exc_type, exc, tb): return False - monkeypatch.setattr(workflow_app_log_module, "Session", lambda *args, **kwargs: DummySession()) + class DummySessionMaker: + def __init__(self, *args, **kwargs): + pass + + def begin(self): + return DummySessionCtx() + + monkeypatch.setattr(workflow_app_log_module, "sessionmaker", DummySessionMaker) def fake_get_paginate(self, **_kwargs): return {"items": [], "total": 0} @@ -423,13 +430,20 @@ class TestWorkflowDraftVariableEndpoints: monkeypatch.setattr(workflow_draft_variable_module, "db", SimpleNamespace(engine=MagicMock())) monkeypatch.setattr(workflow_draft_variable_module, "current_user", SimpleNamespace(id="user-1")) - class DummySession: + class DummySessionCtx: def __enter__(self): return "session" def __exit__(self, exc_type, exc, tb): return False + class DummySessionMaker: + def __init__(self, *args, **kwargs): + pass + + def begin(self): + return DummySessionCtx() + class DummyDraftService: def __init__(self, session): self.session = session @@ -437,7 +451,7 @@ class TestWorkflowDraftVariableEndpoints: def list_variables_without_values(self, **_kwargs): return {"items": [], "total": 0} - monkeypatch.setattr(workflow_draft_variable_module, "Session", lambda *args, **kwargs: DummySession()) + monkeypatch.setattr(workflow_draft_variable_module, "sessionmaker", DummySessionMaker) class DummyWorkflowService: def is_workflow_exist(self, *args, **kwargs): @@ -543,14 +557,21 @@ class TestWorkflowTriggerEndpoints: session = MagicMock() session.query.return_value.where.return_value.first.return_value = trigger - class DummySession: + class DummySessionCtx: def __enter__(self): return session def __exit__(self, exc_type, exc, tb): return False - monkeypatch.setattr(workflow_trigger_module, "Session", lambda *_args, **_kwargs: DummySession()) + class DummySessionMaker: + def __init__(self, *args, **kwargs): + pass + + def begin(self): + return DummySessionCtx() + + monkeypatch.setattr(workflow_trigger_module, "sessionmaker", DummySessionMaker) with app.test_request_context("/?node_id=node-1"): result = method(app_model=SimpleNamespace(id="app-1")) From 2c8b47ce443d81222c6abf12c308e87d57a0c9ff Mon Sep 17 00:00:00 2001 From: Desel72 Date: Tue, 31 Mar 2026 17:26:37 +0300 Subject: [PATCH 049/199] refactor: use sessionmaker().begin() in web and mcp controllers (#34281) --- api/controllers/mcp/mcp.py | 10 ++++------ api/controllers/web/conversation.py | 4 ++-- api/controllers/web/forgot_password.py | 11 +++++------ api/controllers/web/wraps.py | 4 ++-- .../controllers/web/test_web_forgot_password.py | 13 ++++++------- 5 files changed, 19 insertions(+), 23 deletions(-) diff --git a/api/controllers/mcp/mcp.py b/api/controllers/mcp/mcp.py index 58ec76243b..3c59535a48 100644 --- a/api/controllers/mcp/mcp.py +++ b/api/controllers/mcp/mcp.py @@ -4,7 +4,7 @@ from flask import Response from flask_restx import Resource from graphon.variables.input_entities import VariableEntity from pydantic import BaseModel, Field, ValidationError -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, sessionmaker from controllers.common.schema import register_schema_model from controllers.mcp import mcp_ns @@ -67,7 +67,7 @@ class MCPAppApi(Resource): request_id: Union[int, str] | None = args.id mcp_request = self._parse_mcp_request(args.model_dump(exclude_none=True)) - with Session(db.engine, expire_on_commit=False) as session: + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: # Get MCP server and app mcp_server, app = self._get_mcp_server_and_app(server_code, session) self._validate_server_status(mcp_server) @@ -189,7 +189,7 @@ class MCPAppApi(Resource): def _retrieve_end_user(self, tenant_id: str, mcp_server_id: str) -> EndUser | None: """Get end user - manages its own database session""" - with Session(db.engine, expire_on_commit=False) as session, session.begin(): + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: return ( session.query(EndUser) .where(EndUser.tenant_id == tenant_id) @@ -229,9 +229,7 @@ class MCPAppApi(Resource): if not end_user and isinstance(mcp_request.root, mcp_types.InitializeRequest): client_info = mcp_request.root.params.clientInfo client_name = f"{client_info.name}@{client_info.version}" - # Commit the session before creating end user to avoid transaction conflicts - session.commit() - with Session(db.engine, expire_on_commit=False) as create_session, create_session.begin(): + with sessionmaker(db.engine, expire_on_commit=False).begin() as create_session: end_user = self._create_end_user(client_name, app.tenant_id, app.id, mcp_server.id, create_session) return handle_mcp_request(app, mcp_request, user_input_form, mcp_server, end_user, request_id) diff --git a/api/controllers/web/conversation.py b/api/controllers/web/conversation.py index e76649495a..d5baa5fb7d 100644 --- a/api/controllers/web/conversation.py +++ b/api/controllers/web/conversation.py @@ -2,7 +2,7 @@ from typing import Literal from flask import request from pydantic import BaseModel, Field, TypeAdapter, field_validator, model_validator -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import NotFound from controllers.common.schema import register_schema_models @@ -99,7 +99,7 @@ class ConversationListApi(WebApiResource): query = ConversationListQuery.model_validate(raw_args) try: - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: pagination = WebConversationService.pagination_by_last_id( session=session, app_model=app_model, diff --git a/api/controllers/web/forgot_password.py b/api/controllers/web/forgot_password.py index 91d206f727..d69571cc9c 100644 --- a/api/controllers/web/forgot_password.py +++ b/api/controllers/web/forgot_password.py @@ -4,7 +4,7 @@ import secrets from flask import request from flask_restx import Resource from pydantic import BaseModel, Field, field_validator -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from controllers.common.schema import register_schema_models from controllers.console.auth.error import ( @@ -81,7 +81,7 @@ class ForgotPasswordSendEmailApi(Resource): else: language = "en-US" - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: account = AccountService.get_account_by_email_with_case_fallback(request_email, session=session) token = None if account is None: @@ -180,18 +180,17 @@ class ForgotPasswordResetApi(Resource): email = reset_data.get("email", "") - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: account = AccountService.get_account_by_email_with_case_fallback(email, session=session) if account: - self._update_existing_account(account, password_hashed, salt, session) + self._update_existing_account(account, password_hashed, salt) else: raise AuthenticationFailedError() return {"result": "success"} - def _update_existing_account(self, account: Account, password_hashed, salt, session): + def _update_existing_account(self, account: Account, password_hashed, salt): # Update existing account credentials account.password = base64.b64encode(password_hashed).decode() account.password_salt = base64.b64encode(salt).decode() - session.commit() diff --git a/api/controllers/web/wraps.py b/api/controllers/web/wraps.py index 152137f39c..654951a1aa 100644 --- a/api/controllers/web/wraps.py +++ b/api/controllers/web/wraps.py @@ -6,7 +6,7 @@ from typing import Concatenate, ParamSpec, TypeVar from flask import request from flask_restx import Resource from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, NotFound, Unauthorized from constants import HEADER_NAME_APP_CODE @@ -49,7 +49,7 @@ def decode_jwt_token(app_code: str | None = None, user_id: str | None = None): decoded = PassportService().verify(tk) app_code = decoded.get("app_code") app_id = decoded.get("app_id") - with Session(db.engine, expire_on_commit=False) as session: + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: app_model = session.scalar(select(App).where(App.id == app_id)) site = session.scalar(select(Site).where(Site.code == app_code)) if not app_model: diff --git a/api/tests/test_containers_integration_tests/controllers/web/test_web_forgot_password.py b/api/tests/test_containers_integration_tests/controllers/web/test_web_forgot_password.py index 19057726c3..04ad143103 100644 --- a/api/tests/test_containers_integration_tests/controllers/web/test_web_forgot_password.py +++ b/api/tests/test_containers_integration_tests/controllers/web/test_web_forgot_password.py @@ -37,7 +37,7 @@ class TestForgotPasswordSendEmailApi: @patch("controllers.web.forgot_password.AccountService.get_account_by_email_with_case_fallback") @patch("controllers.web.forgot_password.AccountService.is_email_send_ip_limit", return_value=False) @patch("controllers.web.forgot_password.extract_remote_ip", return_value="127.0.0.1") - @patch("controllers.web.forgot_password.Session") + @patch("controllers.web.forgot_password.sessionmaker") def test_should_normalize_email_before_sending( self, mock_session_cls, @@ -51,7 +51,7 @@ class TestForgotPasswordSendEmailApi: mock_get_account.return_value = mock_account mock_send_mail.return_value = "token-123" mock_session = MagicMock() - mock_session_cls.return_value.__enter__.return_value = mock_session + mock_session_cls.return_value.begin.return_value.__enter__.return_value = mock_session with patch("controllers.web.forgot_password.db", SimpleNamespace(engine="engine")): with app.test_request_context( @@ -153,7 +153,7 @@ class TestForgotPasswordResetApi: @patch("controllers.web.forgot_password.ForgotPasswordResetApi._update_existing_account") @patch("controllers.web.forgot_password.AccountService.get_account_by_email_with_case_fallback") - @patch("controllers.web.forgot_password.Session") + @patch("controllers.web.forgot_password.sessionmaker") @patch("controllers.web.forgot_password.AccountService.revoke_reset_password_token") @patch("controllers.web.forgot_password.AccountService.get_reset_password_data") def test_should_fetch_account_with_fallback( @@ -169,7 +169,7 @@ class TestForgotPasswordResetApi: mock_account = MagicMock() mock_get_account.return_value = mock_account mock_session = MagicMock() - mock_session_cls.return_value.__enter__.return_value = mock_session + mock_session_cls.return_value.begin.return_value.__enter__.return_value = mock_session with patch("controllers.web.forgot_password.db", SimpleNamespace(engine="engine")): with app.test_request_context( @@ -190,7 +190,7 @@ class TestForgotPasswordResetApi: @patch("controllers.web.forgot_password.hash_password", return_value=b"hashed-value") @patch("controllers.web.forgot_password.secrets.token_bytes", return_value=b"0123456789abcdef") - @patch("controllers.web.forgot_password.Session") + @patch("controllers.web.forgot_password.sessionmaker") @patch("controllers.web.forgot_password.AccountService.revoke_reset_password_token") @patch("controllers.web.forgot_password.AccountService.get_reset_password_data") @patch("controllers.web.forgot_password.AccountService.get_account_by_email_with_case_fallback") @@ -208,7 +208,7 @@ class TestForgotPasswordResetApi: account = MagicMock() mock_get_account.return_value = account mock_session = MagicMock() - mock_session_cls.return_value.__enter__.return_value = mock_session + mock_session_cls.return_value.begin.return_value.__enter__.return_value = mock_session with patch("controllers.web.forgot_password.db", SimpleNamespace(engine="engine")): with app.test_request_context( @@ -231,4 +231,3 @@ class TestForgotPasswordResetApi: assert account.password == expected_password expected_salt = base64.b64encode(b"0123456789abcdef").decode() assert account.password_salt == expected_salt - mock_session.commit.assert_called_once() From dbdbb098d5cc04e7e89880f1e2ad43bad3e7cd5f Mon Sep 17 00:00:00 2001 From: Desel72 Date: Tue, 31 Mar 2026 17:28:05 +0300 Subject: [PATCH 050/199] =?UTF-8?q?refactor:=20use=20sessionmaker().begin(?= =?UTF-8?q?)=20in=20console=20workspace=20and=20misc=20co=E2=80=A6=20(#342?= =?UTF-8?q?84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/controllers/console/apikey.py | 4 +-- .../console/explore/conversation.py | 4 +-- api/controllers/console/workspace/__init__.py | 4 +-- api/controllers/console/workspace/account.py | 4 +-- .../console/workspace/tool_providers.py | 26 +++++++++---------- .../console/workspace/trigger_providers.py | 5 ++-- .../console/workspace/test_tool_provider.py | 4 +-- .../workspace/test_trigger_providers.py | 8 +++--- 8 files changed, 29 insertions(+), 30 deletions(-) diff --git a/api/controllers/console/apikey.py b/api/controllers/console/apikey.py index 783cb5c444..772bb9d0f1 100644 --- a/api/controllers/console/apikey.py +++ b/api/controllers/console/apikey.py @@ -2,7 +2,7 @@ import flask_restx from flask_restx import Resource, fields, marshal_with from flask_restx._http import HTTPStatus from sqlalchemy import delete, func, select -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import Forbidden from extensions.ext_database import db @@ -34,7 +34,7 @@ api_key_list_model = console_ns.model( def _get_resource(resource_id, tenant_id, resource_model): - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: resource = session.execute( select(resource_model).filter_by(id=resource_id, tenant_id=tenant_id) ).scalar_one_or_none() diff --git a/api/controllers/console/explore/conversation.py b/api/controllers/console/explore/conversation.py index 933c80f509..092f509f1c 100644 --- a/api/controllers/console/explore/conversation.py +++ b/api/controllers/console/explore/conversation.py @@ -2,7 +2,7 @@ from typing import Any from flask import request from pydantic import BaseModel, Field, TypeAdapter, model_validator -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import NotFound from controllers.common.schema import register_schema_models @@ -74,7 +74,7 @@ class ConversationListApi(InstalledAppResource): try: if not isinstance(current_user, Account): raise ValueError("current_user must be an Account instance") - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: pagination = WebConversationService.pagination_by_last_id( session=session, app_model=app_model, diff --git a/api/controllers/console/workspace/__init__.py b/api/controllers/console/workspace/__init__.py index 876e2301f2..9484cc773e 100644 --- a/api/controllers/console/workspace/__init__.py +++ b/api/controllers/console/workspace/__init__.py @@ -2,7 +2,7 @@ from collections.abc import Callable from functools import wraps from typing import ParamSpec, TypeVar -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import Forbidden from extensions.ext_database import db @@ -24,7 +24,7 @@ def plugin_permission_required( user = current_user tenant_id = current_tenant_id - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: permission = ( session.query(TenantPluginPermission) .where( diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index 6f93ff1e70..dcd4438b67 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -8,7 +8,7 @@ from flask import request from flask_restx import Resource, fields, marshal_with from pydantic import BaseModel, Field, field_validator, model_validator from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from configs import dify_config from constants.languages import supported_language @@ -562,7 +562,7 @@ class ChangeEmailSendEmailApi(Resource): user_email = current_user.email else: - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: account = AccountService.get_account_by_email_with_case_fallback(args.email, session=session) if account is None: raise AccountNotFound() diff --git a/api/controllers/console/workspace/tool_providers.py b/api/controllers/console/workspace/tool_providers.py index 80216915cd..c9956501e2 100644 --- a/api/controllers/console/workspace/tool_providers.py +++ b/api/controllers/console/workspace/tool_providers.py @@ -7,7 +7,7 @@ from flask import make_response, redirect, request, send_file from flask_restx import Resource from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field, HttpUrl, field_validator, model_validator -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import Forbidden from configs import dify_config @@ -1019,7 +1019,7 @@ class ToolProviderMCPApi(Resource): # Step 1: Get provider data for URL validation (short-lived session, no network I/O) validation_data = None - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: service = MCPToolManageService(session=session) validation_data = service.get_provider_for_url_validation( tenant_id=current_tenant_id, provider_id=payload.provider_id @@ -1034,7 +1034,7 @@ class ToolProviderMCPApi(Resource): ) # Step 3: Perform database update in a transaction - with Session(db.engine) as session, session.begin(): + with sessionmaker(db.engine).begin() as session: service = MCPToolManageService(session=session) service.update_provider( tenant_id=current_tenant_id, @@ -1061,7 +1061,7 @@ class ToolProviderMCPApi(Resource): payload = MCPProviderDeletePayload.model_validate(console_ns.payload or {}) _, current_tenant_id = current_account_with_tenant() - with Session(db.engine) as session, session.begin(): + with sessionmaker(db.engine).begin() as session: service = MCPToolManageService(session=session) service.delete_provider(tenant_id=current_tenant_id, provider_id=payload.provider_id) @@ -1079,7 +1079,7 @@ class ToolMCPAuthApi(Resource): provider_id = payload.provider_id _, tenant_id = current_account_with_tenant() - with Session(db.engine) as session, session.begin(): + with sessionmaker(db.engine).begin() as session: service = MCPToolManageService(session=session) db_provider = service.get_provider(provider_id=provider_id, tenant_id=tenant_id) if not db_provider: @@ -1100,7 +1100,7 @@ class ToolMCPAuthApi(Resource): sse_read_timeout=provider_entity.sse_read_timeout, ): # Update credentials in new transaction - with Session(db.engine) as session, session.begin(): + with sessionmaker(db.engine).begin() as session: service = MCPToolManageService(session=session) service.update_provider_credentials( provider_id=provider_id, @@ -1118,17 +1118,17 @@ class ToolMCPAuthApi(Resource): resource_metadata_url=e.resource_metadata_url, scope_hint=e.scope_hint, ) - with Session(db.engine) as session, session.begin(): + with sessionmaker(db.engine).begin() as session: service = MCPToolManageService(session=session) response = service.execute_auth_actions(auth_result) return response except MCPRefreshTokenError as e: - with Session(db.engine) as session, session.begin(): + with sessionmaker(db.engine).begin() as session: service = MCPToolManageService(session=session) service.clear_provider_credentials(provider_id=provider_id, tenant_id=tenant_id) raise ValueError(f"Failed to refresh token, please try to authorize again: {e}") from e except (MCPError, ValueError) as e: - with Session(db.engine) as session, session.begin(): + with sessionmaker(db.engine).begin() as session: service = MCPToolManageService(session=session) service.clear_provider_credentials(provider_id=provider_id, tenant_id=tenant_id) raise ValueError(f"Failed to connect to MCP server: {e}") from e @@ -1141,7 +1141,7 @@ class ToolMCPDetailApi(Resource): @account_initialization_required def get(self, provider_id): _, tenant_id = current_account_with_tenant() - with Session(db.engine) as session, session.begin(): + with sessionmaker(db.engine).begin() as session: service = MCPToolManageService(session=session) provider = service.get_provider(provider_id=provider_id, tenant_id=tenant_id) return jsonable_encoder(ToolTransformService.mcp_provider_to_user_provider(provider, for_list=True)) @@ -1155,7 +1155,7 @@ class ToolMCPListAllApi(Resource): def get(self): _, tenant_id = current_account_with_tenant() - with Session(db.engine) as session, session.begin(): + with sessionmaker(db.engine).begin() as session: service = MCPToolManageService(session=session) # Skip sensitive data decryption for list view to improve performance tools = service.list_providers(tenant_id=tenant_id, include_sensitive=False) @@ -1170,7 +1170,7 @@ class ToolMCPUpdateApi(Resource): @account_initialization_required def get(self, provider_id): _, tenant_id = current_account_with_tenant() - with Session(db.engine) as session, session.begin(): + with sessionmaker(db.engine).begin() as session: service = MCPToolManageService(session=session) tools = service.list_provider_tools( tenant_id=tenant_id, @@ -1188,7 +1188,7 @@ class ToolMCPCallbackApi(Resource): authorization_code = query.code # Create service instance for handle_callback - with Session(db.engine) as session, session.begin(): + with sessionmaker(db.engine).begin() as session: mcp_service = MCPToolManageService(session=session) # handle_callback now returns state data and tokens state_data, tokens = handle_callback(state_key, authorization_code) diff --git a/api/controllers/console/workspace/trigger_providers.py b/api/controllers/console/workspace/trigger_providers.py index 76d64cb97c..7a28a09861 100644 --- a/api/controllers/console/workspace/trigger_providers.py +++ b/api/controllers/console/workspace/trigger_providers.py @@ -5,7 +5,7 @@ from flask import make_response, redirect, request from flask_restx import Resource from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, model_validator -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, Forbidden from configs import dify_config @@ -375,7 +375,7 @@ class TriggerSubscriptionDeleteApi(Resource): assert user.current_tenant_id is not None try: - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: # Delete trigger provider subscription TriggerProviderService.delete_trigger_provider( session=session, @@ -388,7 +388,6 @@ class TriggerSubscriptionDeleteApi(Resource): tenant_id=user.current_tenant_id, subscription_id=subscription_id, ) - session.commit() return {"result": "success"} except ValueError as e: raise BadRequest(str(e)) diff --git a/api/tests/test_containers_integration_tests/controllers/console/workspace/test_tool_provider.py b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_tool_provider.py index e36bd213d9..f2e7104b18 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/workspace/test_tool_provider.py +++ b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_tool_provider.py @@ -69,7 +69,7 @@ def client(flask_app_with_containers): return_value=(MagicMock(id="u1"), "t1"), autospec=True, ) -@patch("controllers.console.workspace.tool_providers.Session", autospec=True) +@patch("controllers.console.workspace.tool_providers.sessionmaker", autospec=True) @patch("controllers.console.workspace.tool_providers.MCPToolManageService._reconnect_with_url", autospec=True) @pytest.mark.usefixtures("_mock_cache", "_mock_user_tenant") def test_create_mcp_provider_populates_tools(mock_reconnect, mock_session, mock_current_account_with_tenant, client): @@ -88,7 +88,7 @@ def test_create_mcp_provider_populates_tools(mock_reconnect, mock_session, mock_ create_result.id = "provider-1" svc.create_provider.return_value = create_result svc.get_provider.return_value = MagicMock(id="provider-1", tenant_id="t1") # used by reload path - mock_session.return_value.__enter__.return_value = MagicMock() + mock_session.return_value.begin.return_value.__enter__.return_value = MagicMock() # Patch MCPToolManageService constructed inside controller with patch("controllers.console.workspace.tool_providers.MCPToolManageService", return_value=svc, autospec=True): payload = { diff --git a/api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py index b4d12bff62..ca8195af53 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py +++ b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py @@ -306,14 +306,14 @@ class TestTriggerSubscriptionCrud: app.test_request_context("/"), patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch("controllers.console.workspace.trigger_providers.db") as mock_db, - patch("controllers.console.workspace.trigger_providers.Session") as mock_session_cls, + patch("controllers.console.workspace.trigger_providers.sessionmaker") as mock_session_cls, patch("controllers.console.workspace.trigger_providers.TriggerProviderService.delete_trigger_provider"), patch( "controllers.console.workspace.trigger_providers.TriggerSubscriptionOperatorService.delete_plugin_trigger_by_subscription" ), ): mock_db.engine = MagicMock() - mock_session_cls.return_value.__enter__.return_value = mock_session + mock_session_cls.return_value.begin.return_value.__enter__.return_value = mock_session result = method(api, "sub1") @@ -327,14 +327,14 @@ class TestTriggerSubscriptionCrud: app.test_request_context("/"), patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch("controllers.console.workspace.trigger_providers.db") as mock_db, - patch("controllers.console.workspace.trigger_providers.Session") as session_cls, + patch("controllers.console.workspace.trigger_providers.sessionmaker") as session_cls, patch( "controllers.console.workspace.trigger_providers.TriggerProviderService.delete_trigger_provider", side_effect=ValueError("bad"), ), ): mock_db.engine = MagicMock() - session_cls.return_value.__enter__.return_value = MagicMock() + session_cls.return_value.begin.return_value.__enter__.return_value = MagicMock() with pytest.raises(BadRequest): method(api, "sub1") From 19530e880ae7de50d733679d1b281a598e3115bc Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Wed, 1 Apr 2026 06:52:35 +0800 Subject: [PATCH 051/199] =?UTF-8?q?refactor(api):=20clean=20redundant=20ty?= =?UTF-8?q?pe=20ignore=20in=20request=20query=20parsing=20=F0=9F=A4=96?= =?UTF-8?q?=F0=9F=A4=96=F0=9F=A4=96=20(#34350)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/controllers/console/billing/billing.py | 2 +- api/controllers/console/billing/compliance.py | 2 +- api/controllers/console/workspace/account.py | 2 +- api/controllers/console/workspace/model_providers.py | 4 ++-- api/controllers/console/workspace/workspace.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/controllers/console/billing/billing.py b/api/controllers/console/billing/billing.py index ac039f9c5d..23c01eedb1 100644 --- a/api/controllers/console/billing/billing.py +++ b/api/controllers/console/billing/billing.py @@ -36,7 +36,7 @@ class Subscription(Resource): @only_edition_cloud def get(self): current_user, current_tenant_id = current_account_with_tenant() - args = SubscriptionQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore + args = SubscriptionQuery.model_validate(request.args.to_dict(flat=True)) BillingService.is_tenant_owner_or_admin(current_user) return BillingService.get_subscription(args.plan, args.interval, current_user.email, current_tenant_id) diff --git a/api/controllers/console/billing/compliance.py b/api/controllers/console/billing/compliance.py index afc5f92b68..b5a08e0791 100644 --- a/api/controllers/console/billing/compliance.py +++ b/api/controllers/console/billing/compliance.py @@ -31,7 +31,7 @@ class ComplianceApi(Resource): @only_edition_cloud def get(self): current_user, current_tenant_id = current_account_with_tenant() - args = ComplianceDownloadQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore + args = ComplianceDownloadQuery.model_validate(request.args.to_dict(flat=True)) ip_address = extract_remote_ip(request) device_info = request.headers.get("User-Agent", "Unknown device") diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index dcd4438b67..626d330e9d 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -519,7 +519,7 @@ class EducationAutoCompleteApi(Resource): @cloud_edition_billing_enabled @marshal_with(data_fields) def get(self): - payload = request.args.to_dict(flat=True) # type: ignore + payload = request.args.to_dict(flat=True) args = EducationAutocompleteQuery.model_validate(payload) return BillingService.EducationIdentity.autocomplete(args.keywords, args.page, args.limit) diff --git a/api/controllers/console/workspace/model_providers.py b/api/controllers/console/workspace/model_providers.py index 8e0aefc9e3..cbb9677309 100644 --- a/api/controllers/console/workspace/model_providers.py +++ b/api/controllers/console/workspace/model_providers.py @@ -99,7 +99,7 @@ class ModelProviderListApi(Resource): _, current_tenant_id = current_account_with_tenant() tenant_id = current_tenant_id - payload = request.args.to_dict(flat=True) # type: ignore + payload = request.args.to_dict(flat=True) args = ParserModelList.model_validate(payload) model_provider_service = ModelProviderService() @@ -118,7 +118,7 @@ class ModelProviderCredentialApi(Resource): _, current_tenant_id = current_account_with_tenant() tenant_id = current_tenant_id # if credential_id is not provided, return current used credential - payload = request.args.to_dict(flat=True) # type: ignore + payload = request.args.to_dict(flat=True) args = ParserCredentialId.model_validate(payload) model_provider_service = ModelProviderService() diff --git a/api/controllers/console/workspace/workspace.py b/api/controllers/console/workspace/workspace.py index 88fd2c010f..a06b4fd195 100644 --- a/api/controllers/console/workspace/workspace.py +++ b/api/controllers/console/workspace/workspace.py @@ -155,7 +155,7 @@ class WorkspaceListApi(Resource): @setup_required @admin_required def get(self): - payload = request.args.to_dict(flat=True) # type: ignore + payload = request.args.to_dict(flat=True) args = WorkspaceListQuery.model_validate(payload) stmt = select(Tenant).order_by(Tenant.created_at.desc()) From 57f358a96b68f72b490fb55342228af965a21685 Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Wed, 1 Apr 2026 09:19:32 +0800 Subject: [PATCH 052/199] perf: use global httpx client instead of per request create new one (#34311) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/core/plugin/impl/base.py | 10 +++++-- .../vdb/tidb_on_qdrant/tidb_service.py | 25 ++++++++++++---- api/libs/oauth.py | 18 +++++++---- api/libs/oauth_data_source.py | 18 +++++++---- api/services/auth/jina.py | 8 ++++- api/services/auth/jina/jina.py | 8 ++++- api/services/billing_service.py | 8 ++++- api/services/website_service.py | 25 +++++++++++----- .../services/auth/test_auth_integration.py | 2 +- .../core/datasource/test_website_crawl.py | 7 +++-- .../core/plugin/impl/test_base_client_impl.py | 2 +- .../core/plugin/test_endpoint_client.py | 11 +++++++ .../core/plugin/test_plugin_runtime.py | 14 +++++++++ .../unit_tests/libs/test_oauth_clients.py | 18 +++++------ .../services/auth/test_jina_auth.py | 14 ++++----- .../auth/test_jina_auth_standalone_module.py | 8 ++--- .../services/test_billing_service.py | 2 +- .../test_datasource_provider_service.py | 3 ++ .../services/test_website_service.py | 30 ++++++++++++------- 19 files changed, 167 insertions(+), 64 deletions(-) diff --git a/api/core/plugin/impl/base.py b/api/core/plugin/impl/base.py index 2d0ab3fcd7..706ae248f0 100644 --- a/api/core/plugin/impl/base.py +++ b/api/core/plugin/impl/base.py @@ -17,6 +17,7 @@ from pydantic import BaseModel from yarl import URL from configs import dify_config +from core.helper.http_client_pooling import get_pooled_http_client from core.plugin.endpoint.exc import EndpointSetupFailedError from core.plugin.entities.plugin_daemon import PluginDaemonBasicResponse, PluginDaemonError, PluginDaemonInnerError from core.plugin.impl.exc import ( @@ -54,6 +55,11 @@ T = TypeVar("T", bound=(BaseModel | dict[str, Any] | list[Any] | bool | str)) logger = logging.getLogger(__name__) +_httpx_client: httpx.Client = get_pooled_http_client( + "plugin_daemon", + lambda: httpx.Client(limits=httpx.Limits(max_keepalive_connections=50, max_connections=100), trust_env=False), +) + class BasePluginClient: def _request( @@ -84,7 +90,7 @@ class BasePluginClient: request_kwargs["content"] = prepared_data try: - response = httpx.request(**request_kwargs) + response = _httpx_client.request(**request_kwargs) except httpx.RequestError: logger.exception("Request to Plugin Daemon Service failed") raise PluginDaemonInnerError(code=-500, message="Request to Plugin Daemon Service failed") @@ -171,7 +177,7 @@ class BasePluginClient: stream_kwargs["content"] = prepared_data try: - with httpx.stream(**stream_kwargs) as response: + with _httpx_client.stream(**stream_kwargs) as response: for raw_line in response.iter_lines(): if not raw_line: continue diff --git a/api/core/rag/datasource/vdb/tidb_on_qdrant/tidb_service.py b/api/core/rag/datasource/vdb/tidb_on_qdrant/tidb_service.py index 06b17b9e62..37114be6e7 100644 --- a/api/core/rag/datasource/vdb/tidb_on_qdrant/tidb_service.py +++ b/api/core/rag/datasource/vdb/tidb_on_qdrant/tidb_service.py @@ -6,11 +6,18 @@ import httpx from httpx import DigestAuth from configs import dify_config +from core.helper.http_client_pooling import get_pooled_http_client from extensions.ext_database import db from extensions.ext_redis import redis_client from models.dataset import TidbAuthBinding from models.enums import TidbAuthBindingStatus +# Reuse a pooled HTTP client for all TiDB Cloud requests to minimize connection churn +_tidb_http_client: httpx.Client = get_pooled_http_client( + "tidb:cloud", + lambda: httpx.Client(limits=httpx.Limits(max_keepalive_connections=50, max_connections=100)), +) + class TidbService: @staticmethod @@ -50,7 +57,9 @@ class TidbService: "rootPassword": password, } - response = httpx.post(f"{api_url}/clusters", json=cluster_data, auth=DigestAuth(public_key, private_key)) + response = _tidb_http_client.post( + f"{api_url}/clusters", json=cluster_data, auth=DigestAuth(public_key, private_key) + ) if response.status_code == 200: response_data = response.json() @@ -84,7 +93,9 @@ class TidbService: :return: The response from the API. """ - response = httpx.delete(f"{api_url}/clusters/{cluster_id}", auth=DigestAuth(public_key, private_key)) + response = _tidb_http_client.delete( + f"{api_url}/clusters/{cluster_id}", auth=DigestAuth(public_key, private_key) + ) if response.status_code == 200: return response.json() @@ -103,7 +114,7 @@ class TidbService: :return: The response from the API. """ - response = httpx.get(f"{api_url}/clusters/{cluster_id}", auth=DigestAuth(public_key, private_key)) + response = _tidb_http_client.get(f"{api_url}/clusters/{cluster_id}", auth=DigestAuth(public_key, private_key)) if response.status_code == 200: return response.json() @@ -128,7 +139,7 @@ class TidbService: body = {"password": new_password, "builtinRole": "role_admin", "customRoles": []} - response = httpx.patch( + response = _tidb_http_client.patch( f"{api_url}/clusters/{cluster_id}/sqlUsers/{account}", json=body, auth=DigestAuth(public_key, private_key), @@ -162,7 +173,9 @@ class TidbService: tidb_serverless_list_map = {item.cluster_id: item for item in tidb_serverless_list} cluster_ids = [item.cluster_id for item in tidb_serverless_list] params = {"clusterIds": cluster_ids, "view": "BASIC"} - response = httpx.get(f"{api_url}/clusters:batchGet", params=params, auth=DigestAuth(public_key, private_key)) + response = _tidb_http_client.get( + f"{api_url}/clusters:batchGet", params=params, auth=DigestAuth(public_key, private_key) + ) if response.status_code == 200: response_data = response.json() @@ -223,7 +236,7 @@ class TidbService: clusters.append(cluster_data) request_body = {"requests": clusters} - response = httpx.post( + response = _tidb_http_client.post( f"{api_url}/clusters:batchCreate", json=request_body, auth=DigestAuth(public_key, private_key) ) diff --git a/api/libs/oauth.py b/api/libs/oauth.py index 76e741301c..a2f1114033 100644 --- a/api/libs/oauth.py +++ b/api/libs/oauth.py @@ -7,6 +7,8 @@ from typing import NotRequired import httpx from pydantic import TypeAdapter, ValidationError +from core.helper.http_client_pooling import get_pooled_http_client + if sys.version_info >= (3, 12): from typing import TypedDict else: @@ -20,6 +22,12 @@ JsonObjectList = list[JsonObject] JSON_OBJECT_ADAPTER = TypeAdapter(JsonObject) JSON_OBJECT_LIST_ADAPTER = TypeAdapter(JsonObjectList) +# Reuse a pooled httpx.Client for OAuth flows (public endpoints, no SSRF proxy). +_http_client: httpx.Client = get_pooled_http_client( + "oauth:default", + lambda: httpx.Client(limits=httpx.Limits(max_keepalive_connections=50, max_connections=100)), +) + class AccessTokenResponse(TypedDict, total=False): access_token: str @@ -115,7 +123,7 @@ class GitHubOAuth(OAuth): "redirect_uri": self.redirect_uri, } headers = {"Accept": "application/json"} - response = httpx.post(self._TOKEN_URL, data=data, headers=headers) + response = _http_client.post(self._TOKEN_URL, data=data, headers=headers) response_json = ACCESS_TOKEN_RESPONSE_ADAPTER.validate_python(_json_object(response)) access_token = response_json.get("access_token") @@ -127,7 +135,7 @@ class GitHubOAuth(OAuth): def get_raw_user_info(self, token: str) -> JsonObject: headers = {"Authorization": f"token {token}"} - response = httpx.get(self._USER_INFO_URL, headers=headers) + response = _http_client.get(self._USER_INFO_URL, headers=headers) response.raise_for_status() user_info = GITHUB_RAW_USER_INFO_ADAPTER.validate_python(_json_object(response)) @@ -147,7 +155,7 @@ class GitHubOAuth(OAuth): Returns an empty string when no usable email is found. """ try: - email_response = httpx.get(GitHubOAuth._EMAIL_INFO_URL, headers=headers) + email_response = _http_client.get(GitHubOAuth._EMAIL_INFO_URL, headers=headers) email_response.raise_for_status() email_records = GITHUB_EMAIL_RECORDS_ADAPTER.validate_python(_json_list(email_response)) except (httpx.HTTPStatusError, ValidationError): @@ -204,7 +212,7 @@ class GoogleOAuth(OAuth): "redirect_uri": self.redirect_uri, } headers = {"Accept": "application/json"} - response = httpx.post(self._TOKEN_URL, data=data, headers=headers) + response = _http_client.post(self._TOKEN_URL, data=data, headers=headers) response_json = ACCESS_TOKEN_RESPONSE_ADAPTER.validate_python(_json_object(response)) access_token = response_json.get("access_token") @@ -216,7 +224,7 @@ class GoogleOAuth(OAuth): def get_raw_user_info(self, token: str) -> JsonObject: headers = {"Authorization": f"Bearer {token}"} - response = httpx.get(self._USER_INFO_URL, headers=headers) + response = _http_client.get(self._USER_INFO_URL, headers=headers) response.raise_for_status() return _json_object(response) diff --git a/api/libs/oauth_data_source.py b/api/libs/oauth_data_source.py index d5dc35ac97..190558e1f3 100644 --- a/api/libs/oauth_data_source.py +++ b/api/libs/oauth_data_source.py @@ -7,6 +7,7 @@ from flask_login import current_user from pydantic import TypeAdapter from sqlalchemy import select +from core.helper.http_client_pooling import get_pooled_http_client from extensions.ext_database import db from libs.datetime_utils import naive_utc_now from models.source import DataSourceOauthBinding @@ -38,6 +39,13 @@ NOTION_SOURCE_INFO_ADAPTER = TypeAdapter(NotionSourceInfo) NOTION_PAGE_SUMMARY_ADAPTER = TypeAdapter(NotionPageSummary) +# Reuse a small pooled client for OAuth data source flows. +_http_client: httpx.Client = get_pooled_http_client( + "oauth:notion", + lambda: httpx.Client(limits=httpx.Limits(max_keepalive_connections=50, max_connections=100)), +) + + class OAuthDataSource: client_id: str client_secret: str @@ -75,7 +83,7 @@ class NotionOAuth(OAuthDataSource): data = {"code": code, "grant_type": "authorization_code", "redirect_uri": self.redirect_uri} headers = {"Accept": "application/json"} auth = (self.client_id, self.client_secret) - response = httpx.post(self._TOKEN_URL, data=data, auth=auth, headers=headers) + response = _http_client.post(self._TOKEN_URL, data=data, auth=auth, headers=headers) response_json = response.json() access_token = response_json.get("access_token") @@ -268,7 +276,7 @@ class NotionOAuth(OAuthDataSource): "Notion-Version": "2022-06-28", } - response = httpx.post(url=self._NOTION_PAGE_SEARCH, json=data, headers=headers) + response = _http_client.post(url=self._NOTION_PAGE_SEARCH, json=data, headers=headers) response_json = response.json() results.extend(response_json.get("results", [])) @@ -283,7 +291,7 @@ class NotionOAuth(OAuthDataSource): "Authorization": f"Bearer {access_token}", "Notion-Version": "2022-06-28", } - response = httpx.get(url=f"{self._NOTION_BLOCK_SEARCH}/{block_id}", headers=headers) + response = _http_client.get(url=f"{self._NOTION_BLOCK_SEARCH}/{block_id}", headers=headers) response_json = response.json() if response.status_code != 200: message = response_json.get("message", "unknown error") @@ -299,7 +307,7 @@ class NotionOAuth(OAuthDataSource): "Authorization": f"Bearer {access_token}", "Notion-Version": "2022-06-28", } - response = httpx.get(url=self._NOTION_BOT_USER, headers=headers) + response = _http_client.get(url=self._NOTION_BOT_USER, headers=headers) response_json = response.json() if "object" in response_json and response_json["object"] == "user": user_type = response_json["type"] @@ -323,7 +331,7 @@ class NotionOAuth(OAuthDataSource): "Authorization": f"Bearer {access_token}", "Notion-Version": "2022-06-28", } - response = httpx.post(url=self._NOTION_PAGE_SEARCH, json=data, headers=headers) + response = _http_client.post(url=self._NOTION_PAGE_SEARCH, json=data, headers=headers) response_json = response.json() results.extend(response_json.get("results", [])) diff --git a/api/services/auth/jina.py b/api/services/auth/jina.py index e5e2319ce1..e63c9a3a4d 100644 --- a/api/services/auth/jina.py +++ b/api/services/auth/jina.py @@ -2,8 +2,14 @@ import json import httpx +from core.helper.http_client_pooling import get_pooled_http_client from services.auth.api_key_auth_base import ApiKeyAuthBase, AuthCredentials +_http_client: httpx.Client = get_pooled_http_client( + "auth:jina_standalone", + lambda: httpx.Client(limits=httpx.Limits(max_keepalive_connections=50, max_connections=100)), +) + class JinaAuth(ApiKeyAuthBase): def __init__(self, credentials: AuthCredentials): @@ -31,7 +37,7 @@ class JinaAuth(ApiKeyAuthBase): return {"Content-Type": "application/json", "Authorization": f"Bearer {self.api_key}"} def _post_request(self, url, data, headers): - return httpx.post(url, headers=headers, json=data) + return _http_client.post(url, headers=headers, json=data) def _handle_error(self, response): if response.status_code in {402, 409, 500}: diff --git a/api/services/auth/jina/jina.py b/api/services/auth/jina/jina.py index e5e2319ce1..8ea0b6cd69 100644 --- a/api/services/auth/jina/jina.py +++ b/api/services/auth/jina/jina.py @@ -2,8 +2,14 @@ import json import httpx +from core.helper.http_client_pooling import get_pooled_http_client from services.auth.api_key_auth_base import ApiKeyAuthBase, AuthCredentials +_http_client: httpx.Client = get_pooled_http_client( + "auth:jina", + lambda: httpx.Client(limits=httpx.Limits(max_keepalive_connections=50, max_connections=100)), +) + class JinaAuth(ApiKeyAuthBase): def __init__(self, credentials: AuthCredentials): @@ -31,7 +37,7 @@ class JinaAuth(ApiKeyAuthBase): return {"Content-Type": "application/json", "Authorization": f"Bearer {self.api_key}"} def _post_request(self, url, data, headers): - return httpx.post(url, headers=headers, json=data) + return _http_client.post(url, headers=headers, json=data) def _handle_error(self, response): if response.status_code in {402, 409, 500}: diff --git a/api/services/billing_service.py b/api/services/billing_service.py index 70d4ce1ee6..54c595e0cb 100644 --- a/api/services/billing_service.py +++ b/api/services/billing_service.py @@ -10,6 +10,7 @@ from tenacity import retry, retry_if_exception_type, stop_before_delay, wait_fix from typing_extensions import TypedDict from werkzeug.exceptions import InternalServerError +from core.helper.http_client_pooling import get_pooled_http_client from enums.cloud_plan import CloudPlan from extensions.ext_database import db from extensions.ext_redis import redis_client @@ -18,6 +19,11 @@ from models import Account, TenantAccountJoin, TenantAccountRole logger = logging.getLogger(__name__) +_http_client: httpx.Client = get_pooled_http_client( + "billing:default", + lambda: httpx.Client(limits=httpx.Limits(max_keepalive_connections=50, max_connections=100)), +) + class SubscriptionPlan(TypedDict): """Tenant subscriptionplan information.""" @@ -131,7 +137,7 @@ class BillingService: headers = {"Content-Type": "application/json", "Billing-Api-Secret-Key": cls.secret_key} url = f"{cls.base_url}{endpoint}" - response = httpx.request(method, url, json=json, params=params, headers=headers, follow_redirects=True) + response = _http_client.request(method, url, json=json, params=params, headers=headers, follow_redirects=True) if method == "GET" and response.status_code != httpx.codes.OK: raise ValueError("Unable to retrieve billing information. Please try again later or contact support.") if method == "PUT": diff --git a/api/services/website_service.py b/api/services/website_service.py index b2917ba152..6a521a9cc0 100644 --- a/api/services/website_service.py +++ b/api/services/website_service.py @@ -9,12 +9,23 @@ import httpx from flask_login import current_user from core.helper import encrypter +from core.helper.http_client_pooling import get_pooled_http_client from core.rag.extractor.firecrawl.firecrawl_app import CrawlStatusResponse, FirecrawlApp, FirecrawlDocumentData from core.rag.extractor.watercrawl.provider import WaterCrawlProvider from extensions.ext_redis import redis_client from extensions.ext_storage import storage from services.datasource_provider_service import DatasourceProviderService +# Reuse pooled HTTP clients to avoid creating new connections per request and ease testing. +_jina_http_client: httpx.Client = get_pooled_http_client( + "website:jinareader", + lambda: httpx.Client(limits=httpx.Limits(max_keepalive_connections=50, max_connections=100)), +) +_adaptive_http_client: httpx.Client = get_pooled_http_client( + "website:adaptivecrawl", + lambda: httpx.Client(limits=httpx.Limits(max_keepalive_connections=50, max_connections=100)), +) + @dataclass class CrawlOptions: @@ -225,7 +236,7 @@ class WebsiteService: @classmethod def _crawl_with_jinareader(cls, request: CrawlRequest, api_key: str) -> dict[str, Any]: if not request.options.crawl_sub_pages: - response = httpx.get( + response = _jina_http_client.get( f"https://r.jina.ai/{request.url}", headers={"Accept": "application/json", "Authorization": f"Bearer {api_key}"}, ) @@ -233,7 +244,7 @@ class WebsiteService: raise ValueError("Failed to crawl:") return {"status": "active", "data": response.json().get("data")} else: - response = httpx.post( + response = _adaptive_http_client.post( "https://adaptivecrawl-kir3wx7b3a-uc.a.run.app", json={ "url": request.url, @@ -296,7 +307,7 @@ class WebsiteService: @classmethod def _get_jinareader_status(cls, job_id: str, api_key: str) -> dict[str, Any]: - response = httpx.post( + response = _adaptive_http_client.post( "https://adaptivecrawlstatus-kir3wx7b3a-uc.a.run.app", headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}, json={"taskId": job_id}, @@ -312,7 +323,7 @@ class WebsiteService: } if crawl_status_data["status"] == "completed": - response = httpx.post( + response = _adaptive_http_client.post( "https://adaptivecrawlstatus-kir3wx7b3a-uc.a.run.app", headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}, json={"taskId": job_id, "urls": list(data.get("processed", {}).keys())}, @@ -374,7 +385,7 @@ class WebsiteService: @classmethod def _get_jinareader_url_data(cls, job_id: str, url: str, api_key: str) -> dict[str, Any] | None: if not job_id: - response = httpx.get( + response = _jina_http_client.get( f"https://r.jina.ai/{url}", headers={"Accept": "application/json", "Authorization": f"Bearer {api_key}"}, ) @@ -383,7 +394,7 @@ class WebsiteService: return dict(response.json().get("data", {})) else: # Get crawl status first - status_response = httpx.post( + status_response = _adaptive_http_client.post( "https://adaptivecrawlstatus-kir3wx7b3a-uc.a.run.app", headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}, json={"taskId": job_id}, @@ -393,7 +404,7 @@ class WebsiteService: raise ValueError("Crawl job is not completed") # Get processed data - data_response = httpx.post( + data_response = _adaptive_http_client.post( "https://adaptivecrawlstatus-kir3wx7b3a-uc.a.run.app", headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}, json={"taskId": job_id, "urls": list(status_data.get("processed", {}).keys())}, diff --git a/api/tests/test_containers_integration_tests/services/auth/test_auth_integration.py b/api/tests/test_containers_integration_tests/services/auth/test_auth_integration.py index dc4c0fda1d..f48c6da690 100644 --- a/api/tests/test_containers_integration_tests/services/auth/test_auth_integration.py +++ b/api/tests/test_containers_integration_tests/services/auth/test_auth_integration.py @@ -79,7 +79,7 @@ class TestAuthIntegration: @patch("services.auth.api_key_auth_service.encrypter.encrypt_token") @patch("services.auth.firecrawl.firecrawl.httpx.post") - @patch("services.auth.jina.jina.httpx.post") + @patch("services.auth.jina.jina._http_client.post") def test_multi_tenant_isolation( self, mock_jina_http, diff --git a/api/tests/unit_tests/core/datasource/test_website_crawl.py b/api/tests/unit_tests/core/datasource/test_website_crawl.py index 1d79db2640..53000881dd 100644 --- a/api/tests/unit_tests/core/datasource/test_website_crawl.py +++ b/api/tests/unit_tests/core/datasource/test_website_crawl.py @@ -560,7 +560,10 @@ class TestWebsiteService: mock_response = Mock() mock_response.json.return_value = {"code": 200, "data": {"taskId": "task-789"}} - mock_httpx_post = mocker.patch("services.website_service.httpx.post", return_value=mock_response) + mock_httpx_post = mocker.patch( + "services.website_service._adaptive_http_client.post", + return_value=mock_response, + ) from services.website_service import WebsiteCrawlApiRequest @@ -1340,7 +1343,7 @@ class TestProviderSpecificFeatures: "url": "https://example.com/page", }, } - mocker.patch("services.website_service.httpx.get", return_value=mock_response) + mocker.patch("services.website_service._jina_http_client.get", return_value=mock_response) from services.website_service import WebsiteCrawlApiRequest diff --git a/api/tests/unit_tests/core/plugin/impl/test_base_client_impl.py b/api/tests/unit_tests/core/plugin/impl/test_base_client_impl.py index c216906d68..23894bd417 100644 --- a/api/tests/unit_tests/core/plugin/impl/test_base_client_impl.py +++ b/api/tests/unit_tests/core/plugin/impl/test_base_client_impl.py @@ -57,7 +57,7 @@ class TestBasePluginClientImpl: def test_stream_request_handles_data_lines_and_dict_payload(self, mocker): client = BasePluginClient() stream_mock = mocker.patch( - "core.plugin.impl.base.httpx.stream", + "httpx.Client.stream", return_value=_StreamContext([b"", b"data: hello", "world"]), ) diff --git a/api/tests/unit_tests/core/plugin/test_endpoint_client.py b/api/tests/unit_tests/core/plugin/test_endpoint_client.py index 48e30e9c2f..ff9deb918a 100644 --- a/api/tests/unit_tests/core/plugin/test_endpoint_client.py +++ b/api/tests/unit_tests/core/plugin/test_endpoint_client.py @@ -10,12 +10,23 @@ Tests follow the Arrange-Act-Assert pattern for clarity. from unittest.mock import MagicMock, patch +import httpx import pytest from core.plugin.impl.endpoint import PluginEndpointClient from core.plugin.impl.exc import PluginDaemonInternalServerError +@pytest.fixture(autouse=True) +def _patch_shared_httpx_client(): + """Patch module-level client methods to delegate to module httpx.request/stream.""" + with ( + patch("core.plugin.impl.base._httpx_client.request", side_effect=lambda **kw: httpx.request(**kw)), + patch("core.plugin.impl.base._httpx_client.stream", side_effect=lambda **kw: httpx.stream(**kw)), + ): + yield + + class TestPluginEndpointClientDelete: """Unit tests for PluginEndpointClient delete_endpoint operation. diff --git a/api/tests/unit_tests/core/plugin/test_plugin_runtime.py b/api/tests/unit_tests/core/plugin/test_plugin_runtime.py index 3063ca0197..a3b1e5f6b0 100644 --- a/api/tests/unit_tests/core/plugin/test_plugin_runtime.py +++ b/api/tests/unit_tests/core/plugin/test_plugin_runtime.py @@ -47,6 +47,20 @@ from core.plugin.impl.plugin import PluginInstaller from core.plugin.impl.tool import PluginToolManager +@pytest.fixture(autouse=True) +def _patch_shared_httpx_client(): + """Make BasePluginClient's module-level httpx client delegate to patched httpx.request/stream. + + After refactor, code uses core.plugin.impl.base._httpx_client directly. + Patch its request/stream to route through module-level httpx so existing mocks still apply. + """ + with ( + patch("core.plugin.impl.base._httpx_client.request", side_effect=lambda **kw: httpx.request(**kw)), + patch("core.plugin.impl.base._httpx_client.stream", side_effect=lambda **kw: httpx.stream(**kw)), + ): + yield + + class TestPluginRuntimeExecution: """Unit tests for plugin execution functionality. diff --git a/api/tests/unit_tests/libs/test_oauth_clients.py b/api/tests/unit_tests/libs/test_oauth_clients.py index ab468c8687..830284e697 100644 --- a/api/tests/unit_tests/libs/test_oauth_clients.py +++ b/api/tests/unit_tests/libs/test_oauth_clients.py @@ -68,7 +68,7 @@ class TestGitHubOAuth(BaseOAuthTest): ({}, None, True), ], ) - @patch("httpx.post", autospec=True) + @patch("libs.oauth._http_client.post", autospec=True) def test_should_retrieve_access_token( self, mock_post, oauth, mock_response, response_data, expected_token, should_raise ): @@ -109,7 +109,7 @@ class TestGitHubOAuth(BaseOAuthTest): ), ], ) - @patch("httpx.get", autospec=True) + @patch("libs.oauth._http_client.get", autospec=True) def test_should_retrieve_user_info_correctly(self, mock_get, oauth, user_data, email_data, expected_email): user_response = MagicMock() user_response.json.return_value = user_data @@ -127,7 +127,7 @@ class TestGitHubOAuth(BaseOAuthTest): # The profile email is absent/null, so /user/emails should be called assert mock_get.call_count == 2 - @patch("httpx.get", autospec=True) + @patch("libs.oauth._http_client.get", autospec=True) def test_should_skip_email_endpoint_when_profile_email_present(self, mock_get, oauth): """When the /user profile already contains an email, do not call /user/emails.""" user_response = MagicMock() @@ -162,7 +162,7 @@ class TestGitHubOAuth(BaseOAuthTest): ), ], ) - @patch("httpx.get", autospec=True) + @patch("libs.oauth._http_client.get", autospec=True) def test_should_use_noreply_email_when_no_usable_email(self, mock_get, oauth, user_data, email_data): user_response = MagicMock() user_response.json.return_value = user_data @@ -177,7 +177,7 @@ class TestGitHubOAuth(BaseOAuthTest): assert user_info.id == str(user_data["id"]) assert user_info.email == "12345@users.noreply.github.com" - @patch("httpx.get", autospec=True) + @patch("libs.oauth._http_client.get", autospec=True) def test_should_use_noreply_email_when_email_endpoint_fails(self, mock_get, oauth): user_response = MagicMock() user_response.json.return_value = {"id": 12345, "login": "testuser", "name": "Test User"} @@ -194,7 +194,7 @@ class TestGitHubOAuth(BaseOAuthTest): assert user_info.id == "12345" assert user_info.email == "12345@users.noreply.github.com" - @patch("httpx.get", autospec=True) + @patch("libs.oauth._http_client.get", autospec=True) def test_should_handle_network_errors(self, mock_get, oauth): mock_get.side_effect = httpx.RequestError("Network error") @@ -240,7 +240,7 @@ class TestGoogleOAuth(BaseOAuthTest): ({}, None, True), ], ) - @patch("httpx.post", autospec=True) + @patch("libs.oauth._http_client.post", autospec=True) def test_should_retrieve_access_token( self, mock_post, oauth, oauth_config, mock_response, response_data, expected_token, should_raise ): @@ -274,7 +274,7 @@ class TestGoogleOAuth(BaseOAuthTest): ({"sub": "123", "email": "test@example.com", "name": "Test User"}, ""), # Always returns empty string ], ) - @patch("httpx.get", autospec=True) + @patch("libs.oauth._http_client.get", autospec=True) def test_should_retrieve_user_info_correctly(self, mock_get, oauth, mock_response, user_data, expected_name): mock_response.json.return_value = user_data mock_get.return_value = mock_response @@ -295,7 +295,7 @@ class TestGoogleOAuth(BaseOAuthTest): httpx.TimeoutException, ], ) - @patch("httpx.get", autospec=True) + @patch("libs.oauth._http_client.get", autospec=True) def test_should_handle_http_errors(self, mock_get, oauth, exception_type): mock_response = MagicMock() mock_response.raise_for_status.side_effect = exception_type("Error") diff --git a/api/tests/unit_tests/services/auth/test_jina_auth.py b/api/tests/unit_tests/services/auth/test_jina_auth.py index 67f252390d..2c34d46f1e 100644 --- a/api/tests/unit_tests/services/auth/test_jina_auth.py +++ b/api/tests/unit_tests/services/auth/test_jina_auth.py @@ -35,7 +35,7 @@ class TestJinaAuth: JinaAuth(credentials) assert str(exc_info.value) == "No API key provided" - @patch("services.auth.jina.jina.httpx.post", autospec=True) + @patch("services.auth.jina.jina._http_client.post", autospec=True) def test_should_validate_valid_credentials_successfully(self, mock_post): """Test successful credential validation""" mock_response = MagicMock() @@ -53,7 +53,7 @@ class TestJinaAuth: json={"url": "https://example.com"}, ) - @patch("services.auth.jina.jina.httpx.post", autospec=True) + @patch("services.auth.jina.jina._http_client.post", autospec=True) def test_should_handle_http_402_error(self, mock_post): """Test handling of 402 Payment Required error""" mock_response = MagicMock() @@ -68,7 +68,7 @@ class TestJinaAuth: auth.validate_credentials() assert str(exc_info.value) == "Failed to authorize. Status code: 402. Error: Payment required" - @patch("services.auth.jina.jina.httpx.post", autospec=True) + @patch("services.auth.jina.jina._http_client.post", autospec=True) def test_should_handle_http_409_error(self, mock_post): """Test handling of 409 Conflict error""" mock_response = MagicMock() @@ -83,7 +83,7 @@ class TestJinaAuth: auth.validate_credentials() assert str(exc_info.value) == "Failed to authorize. Status code: 409. Error: Conflict error" - @patch("services.auth.jina.jina.httpx.post", autospec=True) + @patch("services.auth.jina.jina._http_client.post", autospec=True) def test_should_handle_http_500_error(self, mock_post): """Test handling of 500 Internal Server Error""" mock_response = MagicMock() @@ -98,7 +98,7 @@ class TestJinaAuth: auth.validate_credentials() assert str(exc_info.value) == "Failed to authorize. Status code: 500. Error: Internal server error" - @patch("services.auth.jina.jina.httpx.post", autospec=True) + @patch("services.auth.jina.jina._http_client.post", autospec=True) def test_should_handle_unexpected_error_with_text_response(self, mock_post): """Test handling of unexpected errors with text response""" mock_response = MagicMock() @@ -114,7 +114,7 @@ class TestJinaAuth: auth.validate_credentials() assert str(exc_info.value) == "Failed to authorize. Status code: 403. Error: Forbidden" - @patch("services.auth.jina.jina.httpx.post", autospec=True) + @patch("services.auth.jina.jina._http_client.post", autospec=True) def test_should_handle_unexpected_error_without_text(self, mock_post): """Test handling of unexpected errors without text response""" mock_response = MagicMock() @@ -130,7 +130,7 @@ class TestJinaAuth: auth.validate_credentials() assert str(exc_info.value) == "Unexpected error occurred while trying to authorize. Status code: 404" - @patch("services.auth.jina.jina.httpx.post", autospec=True) + @patch("services.auth.jina.jina._http_client.post", autospec=True) def test_should_handle_network_errors(self, mock_post): """Test handling of network connection errors""" mock_post.side_effect = httpx.ConnectError("Network error") diff --git a/api/tests/unit_tests/services/auth/test_jina_auth_standalone_module.py b/api/tests/unit_tests/services/auth/test_jina_auth_standalone_module.py index c2fcd71875..4b5a97bf3f 100644 --- a/api/tests/unit_tests/services/auth/test_jina_auth_standalone_module.py +++ b/api/tests/unit_tests/services/auth/test_jina_auth_standalone_module.py @@ -60,7 +60,7 @@ def test_prepare_headers_includes_bearer_api_key(jina_module: ModuleType) -> Non def test_post_request_calls_httpx(jina_module: ModuleType, monkeypatch: pytest.MonkeyPatch) -> None: auth = jina_module.JinaAuth(_credentials(api_key="k")) post_mock = MagicMock(name="httpx.post") - monkeypatch.setattr(jina_module.httpx, "post", post_mock) + monkeypatch.setattr(jina_module._http_client, "post", post_mock) auth._post_request("https://r.jina.ai", {"url": "https://example.com"}, {"h": "v"}) post_mock.assert_called_once_with("https://r.jina.ai", headers={"h": "v"}, json={"url": "https://example.com"}) @@ -72,7 +72,7 @@ def test_validate_credentials_success(jina_module: ModuleType, monkeypatch: pyte response = MagicMock() response.status_code = 200 post_mock = MagicMock(return_value=response) - monkeypatch.setattr(jina_module.httpx, "post", post_mock) + monkeypatch.setattr(jina_module._http_client, "post", post_mock) assert auth.validate_credentials() is True post_mock.assert_called_once_with( @@ -90,7 +90,7 @@ def test_validate_credentials_non_200_raises_via_handle_error( response = MagicMock() response.status_code = 402 response.json.return_value = {"error": "Payment required"} - monkeypatch.setattr(jina_module.httpx, "post", MagicMock(return_value=response)) + monkeypatch.setattr(jina_module._http_client, "post", MagicMock(return_value=response)) with pytest.raises(Exception, match="Status code: 402.*Payment required"): auth.validate_credentials() @@ -151,7 +151,7 @@ def test_validate_credentials_propagates_network_errors( jina_module: ModuleType, monkeypatch: pytest.MonkeyPatch ) -> None: auth = jina_module.JinaAuth(_credentials(api_key="k")) - monkeypatch.setattr(jina_module.httpx, "post", MagicMock(side_effect=httpx.ConnectError("boom"))) + monkeypatch.setattr(jina_module._http_client, "post", MagicMock(side_effect=httpx.ConnectError("boom"))) with pytest.raises(httpx.ConnectError, match="boom"): auth.validate_credentials() diff --git a/api/tests/unit_tests/services/test_billing_service.py b/api/tests/unit_tests/services/test_billing_service.py index 316381f0ca..b3d2e60802 100644 --- a/api/tests/unit_tests/services/test_billing_service.py +++ b/api/tests/unit_tests/services/test_billing_service.py @@ -38,7 +38,7 @@ class TestBillingServiceSendRequest: @pytest.fixture def mock_httpx_request(self): """Mock httpx.request for testing.""" - with patch("services.billing_service.httpx.request") as mock_request: + with patch("services.billing_service._http_client.request") as mock_request: yield mock_request @pytest.fixture diff --git a/api/tests/unit_tests/services/test_datasource_provider_service.py b/api/tests/unit_tests/services/test_datasource_provider_service.py index 3df7d500cf..da414816ff 100644 --- a/api/tests/unit_tests/services/test_datasource_provider_service.py +++ b/api/tests/unit_tests/services/test_datasource_provider_service.py @@ -1,5 +1,6 @@ from unittest.mock import MagicMock, patch +import httpx import pytest from graphon.model_runtime.entities.provider_entities import FormType from sqlalchemy.orm import Session @@ -71,6 +72,8 @@ class TestDatasourceProviderService: @pytest.fixture(autouse=True) def patch_externals(self): with ( + patch("core.plugin.impl.base._httpx_client.request", side_effect=lambda **kw: httpx.request(**kw)), + patch("core.plugin.impl.base._httpx_client.stream", side_effect=lambda **kw: httpx.stream(**kw)), patch("httpx.request") as mock_httpx, patch("services.datasource_provider_service.dify_config") as mock_cfg, patch("services.datasource_provider_service.encrypter") as mock_enc, diff --git a/api/tests/unit_tests/services/test_website_service.py b/api/tests/unit_tests/services/test_website_service.py index e973da7d56..b0ddc7388a 100644 --- a/api/tests/unit_tests/services/test_website_service.py +++ b/api/tests/unit_tests/services/test_website_service.py @@ -343,7 +343,7 @@ def test_crawl_with_watercrawl_passes_options_dict(monkeypatch: pytest.MonkeyPat def test_crawl_with_jinareader_single_page_success(monkeypatch: pytest.MonkeyPatch) -> None: get_mock = MagicMock(return_value=_DummyHttpxResponse({"code": 200, "data": {"title": "t"}})) - monkeypatch.setattr(website_service_module.httpx, "get", get_mock) + monkeypatch.setattr(website_service_module._jina_http_client, "get", get_mock) req = WebsiteCrawlApiRequest( provider="jinareader", url="https://example.com", options={"crawl_sub_pages": False} @@ -356,7 +356,11 @@ def test_crawl_with_jinareader_single_page_success(monkeypatch: pytest.MonkeyPat def test_crawl_with_jinareader_single_page_failure(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(website_service_module.httpx, "get", MagicMock(return_value=_DummyHttpxResponse({"code": 500}))) + monkeypatch.setattr( + website_service_module._jina_http_client, + "get", + MagicMock(return_value=_DummyHttpxResponse({"code": 500})), + ) req = WebsiteCrawlApiRequest( provider="jinareader", url="https://example.com", options={"crawl_sub_pages": False} ).to_crawl_request() @@ -368,7 +372,7 @@ def test_crawl_with_jinareader_single_page_failure(monkeypatch: pytest.MonkeyPat def test_crawl_with_jinareader_multi_page_success(monkeypatch: pytest.MonkeyPatch) -> None: post_mock = MagicMock(return_value=_DummyHttpxResponse({"code": 200, "data": {"taskId": "t1"}})) - monkeypatch.setattr(website_service_module.httpx, "post", post_mock) + monkeypatch.setattr(website_service_module._adaptive_http_client, "post", post_mock) req = WebsiteCrawlApiRequest( provider="jinareader", @@ -384,7 +388,7 @@ def test_crawl_with_jinareader_multi_page_success(monkeypatch: pytest.MonkeyPatc def test_crawl_with_jinareader_multi_page_failure(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr( - website_service_module.httpx, "post", MagicMock(return_value=_DummyHttpxResponse({"code": 400})) + website_service_module._adaptive_http_client, "post", MagicMock(return_value=_DummyHttpxResponse({"code": 400})) ) req = WebsiteCrawlApiRequest( provider="jinareader", @@ -482,7 +486,7 @@ def test_get_jinareader_status_active(monkeypatch: pytest.MonkeyPatch) -> None: } ) ) - monkeypatch.setattr(website_service_module.httpx, "post", post_mock) + monkeypatch.setattr(website_service_module._adaptive_http_client, "post", post_mock) result = WebsiteService._get_jinareader_status("job-1", "k") assert result["status"] == "active" @@ -518,7 +522,7 @@ def test_get_jinareader_status_completed_formats_processed_items(monkeypatch: py } } post_mock = MagicMock(side_effect=[_DummyHttpxResponse(status_payload), _DummyHttpxResponse(processed_payload)]) - monkeypatch.setattr(website_service_module.httpx, "post", post_mock) + monkeypatch.setattr(website_service_module._adaptive_http_client, "post", post_mock) result = WebsiteService._get_jinareader_status("job-1", "k") assert result["status"] == "completed" @@ -619,7 +623,7 @@ def test_get_watercrawl_url_data_delegates(monkeypatch: pytest.MonkeyPatch) -> N def test_get_jinareader_url_data_without_job_id_success(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr( - website_service_module.httpx, + website_service_module._jina_http_client, "get", MagicMock(return_value=_DummyHttpxResponse({"code": 200, "data": {"url": "u"}})), ) @@ -627,7 +631,11 @@ def test_get_jinareader_url_data_without_job_id_success(monkeypatch: pytest.Monk def test_get_jinareader_url_data_without_job_id_failure(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(website_service_module.httpx, "get", MagicMock(return_value=_DummyHttpxResponse({"code": 500}))) + monkeypatch.setattr( + website_service_module._jina_http_client, + "get", + MagicMock(return_value=_DummyHttpxResponse({"code": 500})), + ) with pytest.raises(ValueError, match="Failed to crawl$"): WebsiteService._get_jinareader_url_data("", "u", "k") @@ -637,7 +645,7 @@ def test_get_jinareader_url_data_with_job_id_completed_returns_matching_item(mon processed_payload = {"data": {"processed": {"u1": {"data": {"url": "u", "title": "t"}}}}} post_mock = MagicMock(side_effect=[_DummyHttpxResponse(status_payload), _DummyHttpxResponse(processed_payload)]) - monkeypatch.setattr(website_service_module.httpx, "post", post_mock) + monkeypatch.setattr(website_service_module._adaptive_http_client, "post", post_mock) assert WebsiteService._get_jinareader_url_data("job-1", "u", "k") == {"url": "u", "title": "t"} assert post_mock.call_count == 2 @@ -645,7 +653,7 @@ def test_get_jinareader_url_data_with_job_id_completed_returns_matching_item(mon def test_get_jinareader_url_data_with_job_id_not_completed_raises(monkeypatch: pytest.MonkeyPatch) -> None: post_mock = MagicMock(return_value=_DummyHttpxResponse({"data": {"status": "active"}})) - monkeypatch.setattr(website_service_module.httpx, "post", post_mock) + monkeypatch.setattr(website_service_module._adaptive_http_client, "post", post_mock) with pytest.raises(ValueError, match=r"Crawl job is no\s*t completed"): WebsiteService._get_jinareader_url_data("job-1", "u", "k") @@ -658,7 +666,7 @@ def test_get_jinareader_url_data_with_job_id_completed_but_not_found_returns_non processed_payload = {"data": {"processed": {"u1": {"data": {"url": "other"}}}}} post_mock = MagicMock(side_effect=[_DummyHttpxResponse(status_payload), _DummyHttpxResponse(processed_payload)]) - monkeypatch.setattr(website_service_module.httpx, "post", post_mock) + monkeypatch.setattr(website_service_module._adaptive_http_client, "post", post_mock) assert WebsiteService._get_jinareader_url_data("job-1", "u", "k") is None From d2baacdd4b7f3a717fa7e19820300d9ae63d4d5f Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Wed, 1 Apr 2026 09:31:42 +0800 Subject: [PATCH 053/199] feat(docker): add healthcheck for api, worker, and worker_beat services (#34345) Signed-off-by: majiayu000 <1835304752@qq.com> --- docker/docker-compose-template.yaml | 18 ++++++++++++++++++ docker/docker-compose.yaml | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index e55cf942c3..57584cb829 100644 --- a/docker/docker-compose-template.yaml +++ b/docker/docker-compose-template.yaml @@ -56,6 +56,12 @@ services: volumes: # Mount the storage directory to the container, for storing user files. - ./volumes/app/storage:/app/api/storage + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5001/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s networks: - ssrf_proxy_network - default @@ -95,6 +101,12 @@ services: volumes: # Mount the storage directory to the container, for storing user files. - ./volumes/app/storage:/app/api/storage + healthcheck: + test: ["CMD-SHELL", "celery -A celery_entrypoint.celery inspect ping"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s networks: - ssrf_proxy_network - default @@ -126,6 +138,12 @@ services: required: false redis: condition: service_started + healthcheck: + test: ["CMD-SHELL", "celery -A app.celery inspect ping"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s networks: - ssrf_proxy_network - default diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index ed68107f46..097fadc959 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -765,6 +765,12 @@ services: volumes: # Mount the storage directory to the container, for storing user files. - ./volumes/app/storage:/app/api/storage + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5001/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s networks: - ssrf_proxy_network - default @@ -804,6 +810,12 @@ services: volumes: # Mount the storage directory to the container, for storing user files. - ./volumes/app/storage:/app/api/storage + healthcheck: + test: ["CMD-SHELL", "celery -A celery_entrypoint.celery inspect ping"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s networks: - ssrf_proxy_network - default @@ -835,6 +847,12 @@ services: required: false redis: condition: service_started + healthcheck: + test: ["CMD-SHELL", "celery -A app.celery inspect ping"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s networks: - ssrf_proxy_network - default From 324b47507c0781567b8b441477aeb78216241719 Mon Sep 17 00:00:00 2001 From: Wu Tianwei <30284043+WTW0313@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:50:02 +0800 Subject: [PATCH 054/199] refactor: enhance ELK layout handling (#34334) --- .../utils/__tests__/elk-layout.spec.ts | 275 ++++++++++++++++++ .../components/workflow/utils/elk-layout.ts | 135 +++++---- 2 files changed, 357 insertions(+), 53 deletions(-) diff --git a/web/app/components/workflow/utils/__tests__/elk-layout.spec.ts b/web/app/components/workflow/utils/__tests__/elk-layout.spec.ts index 1a3c52ec2d..54eb289abe 100644 --- a/web/app/components/workflow/utils/__tests__/elk-layout.spec.ts +++ b/web/app/components/workflow/utils/__tests__/elk-layout.spec.ts @@ -486,6 +486,242 @@ describe('getLayoutByELK', () => { expect(hiNode.ports).toHaveLength(2) }) + it('should build ports for QuestionClassifier sorted by classes order', async () => { + const nodes = [ + makeWorkflowNode({ + id: 'qc-1', + data: { + type: BlockEnum.QuestionClassifier, + title: '', + desc: '', + classes: [{ id: 'cls-a', name: 'A' }, { id: 'cls-b', name: 'B' }, { id: 'cls-c', name: 'C' }], + }, + }), + makeWorkflowNode({ id: 'x', data: { type: BlockEnum.Code, title: '', desc: '' } }), + makeWorkflowNode({ id: 'y', data: { type: BlockEnum.Code, title: '', desc: '' } }), + makeWorkflowNode({ id: 'z', data: { type: BlockEnum.Code, title: '', desc: '' } }), + ] + const edges = [ + makeWorkflowEdge({ id: 'e-c', source: 'qc-1', target: 'z', sourceHandle: 'cls-c' }), + makeWorkflowEdge({ id: 'e-a', source: 'qc-1', target: 'x', sourceHandle: 'cls-a' }), + makeWorkflowEdge({ id: 'e-b', source: 'qc-1', target: 'y', sourceHandle: 'cls-b' }), + ] + + await getLayoutByELK(nodes, edges) + const qcNode = layoutCallArgs!.children!.find((c: ElkChild) => c.id === 'qc-1')! + const portIds = qcNode.ports!.map((p: { id: string }) => p.id) + expect(portIds).toEqual([ + 'qc-1-out-cls-a', + 'qc-1-out-cls-b', + 'qc-1-out-cls-c', + ]) + }) + + it('should build ports for QuestionClassifier with single class', async () => { + const nodes = [ + makeWorkflowNode({ + id: 'qc-1', + data: { + type: BlockEnum.QuestionClassifier, + title: '', + desc: '', + classes: [{ id: 'cls-only', name: 'Only' }], + }, + }), + makeWorkflowNode({ id: 'x', data: { type: BlockEnum.Code, title: '', desc: '' } }), + ] + const edges = [ + makeWorkflowEdge({ id: 'e1', source: 'qc-1', target: 'x', sourceHandle: 'cls-only' }), + ] + + await getLayoutByELK(nodes, edges) + const qcNode = layoutCallArgs!.children!.find((c: ElkChild) => c.id === 'qc-1')! + expect(qcNode.ports).toHaveLength(1) + expect(qcNode.ports![0].layoutOptions!['elk.port.side']).toBe('EAST') + }) + + it('should only create output (EAST) ports, not input (WEST) ports', async () => { + const nodes = [ + makeWorkflowNode({ id: 'a', data: { type: BlockEnum.Start, title: '', desc: '' } }), + makeWorkflowNode({ id: 'b', data: { type: BlockEnum.Code, title: '', desc: '' } }), + makeWorkflowNode({ id: 'c', data: { type: BlockEnum.End, title: '', desc: '' } }), + ] + const edges = [ + makeWorkflowEdge({ id: 'e1', source: 'a', target: 'b' }), + makeWorkflowEdge({ id: 'e2', source: 'b', target: 'c' }), + ] + + await getLayoutByELK(nodes, edges) + layoutCallArgs!.children!.forEach((child: ElkChild) => { + if (child.ports) { + child.ports.forEach((port) => { + expect(port.layoutOptions!['elk.port.side']).toBe('EAST') + }) + } + }) + const endNode = layoutCallArgs!.children!.find((c: ElkChild) => c.id === 'c')! + expect(endNode.ports).toBeUndefined() + }) + + it('should order children array by DFS following port order', async () => { + const nodes = [ + makeWorkflowNode({ + id: 'if-1', + data: { + type: BlockEnum.IfElse, + title: '', + desc: '', + cases: [{ case_id: 'case-a', logical_operator: 'and', conditions: [] }], + }, + }), + makeWorkflowNode({ id: 'start', data: { type: BlockEnum.Start, title: '', desc: '' } }), + makeWorkflowNode({ id: 'branch-a', data: { type: BlockEnum.Code, title: '', desc: '' } }), + makeWorkflowNode({ id: 'branch-else', data: { type: BlockEnum.Code, title: '', desc: '' } }), + makeWorkflowNode({ id: 'end', data: { type: BlockEnum.End, title: '', desc: '' } }), + ] + const edges = [ + makeWorkflowEdge({ source: 'start', target: 'if-1' }), + makeWorkflowEdge({ id: 'e-else', source: 'if-1', target: 'branch-else', sourceHandle: 'false' }), + makeWorkflowEdge({ id: 'e-a', source: 'if-1', target: 'branch-a', sourceHandle: 'case-a' }), + makeWorkflowEdge({ source: 'branch-a', target: 'end' }), + makeWorkflowEdge({ source: 'branch-else', target: 'end' }), + ] + + await getLayoutByELK(nodes, edges) + const childIds = layoutCallArgs!.children!.map((c: ElkChild) => c.id) + // DFS from start: start → if-1 → branch-a (case-a first) → end → branch-else + const idxA = childIds.indexOf('branch-a') + const idxElse = childIds.indexOf('branch-else') + expect(idxA).toBeLessThan(idxElse) + }) + + it('should order children by DFS across nested branching nodes', async () => { + const nodes = [ + makeWorkflowNode({ id: 'start', data: { type: BlockEnum.Start, title: '', desc: '' } }), + makeWorkflowNode({ + id: 'qc-1', + data: { + type: BlockEnum.QuestionClassifier, + title: '', + desc: '', + classes: [{ id: 'c1', name: 'C1' }, { id: 'c2', name: 'C2' }], + }, + }), + makeWorkflowNode({ id: 'upper', data: { type: BlockEnum.Code, title: '', desc: '' } }), + makeWorkflowNode({ id: 'lower', data: { type: BlockEnum.Code, title: '', desc: '' } }), + ] + const edges = [ + makeWorkflowEdge({ source: 'start', target: 'qc-1' }), + makeWorkflowEdge({ id: 'e-c2', source: 'qc-1', target: 'lower', sourceHandle: 'c2' }), + makeWorkflowEdge({ id: 'e-c1', source: 'qc-1', target: 'upper', sourceHandle: 'c1' }), + ] + + await getLayoutByELK(nodes, edges) + const childIds = layoutCallArgs!.children!.map((c: ElkChild) => c.id) + // DFS: start → qc-1 → upper (c1 first) → lower (c2 second) + expect(childIds.indexOf('upper')).toBeLessThan(childIds.indexOf('lower')) + }) + + it('should handle QuestionClassifier with no classes property', async () => { + const nodes = [ + makeWorkflowNode({ id: 'qc-1', data: { type: BlockEnum.QuestionClassifier, title: '', desc: '' } }), + makeWorkflowNode({ id: 'b', data: { type: BlockEnum.Code, title: '', desc: '' } }), + makeWorkflowNode({ id: 'c', data: { type: BlockEnum.Code, title: '', desc: '' } }), + ] + const edges = [ + makeWorkflowEdge({ id: 'e1', source: 'qc-1', target: 'b', sourceHandle: 'cls-1' }), + makeWorkflowEdge({ id: 'e2', source: 'qc-1', target: 'c', sourceHandle: 'cls-2' }), + ] + + await getLayoutByELK(nodes, edges) + const qcNode = layoutCallArgs!.children!.find((c: ElkChild) => c.id === 'qc-1')! + expect(qcNode.ports).toHaveLength(2) + }) + + it('should handle QuestionClassifier edges where handle not found in classes', async () => { + const nodes = [ + makeWorkflowNode({ + id: 'qc-1', + data: { type: BlockEnum.QuestionClassifier, title: '', desc: '', classes: [{ id: 'known', name: 'K' }] }, + }), + makeWorkflowNode({ id: 'b', data: { type: BlockEnum.Code, title: '', desc: '' } }), + makeWorkflowNode({ id: 'c', data: { type: BlockEnum.Code, title: '', desc: '' } }), + ] + const edges = [ + makeWorkflowEdge({ id: 'e1', source: 'qc-1', target: 'b', sourceHandle: 'unknown-1' }), + makeWorkflowEdge({ id: 'e2', source: 'qc-1', target: 'c', sourceHandle: 'unknown-2' }), + ] + + await getLayoutByELK(nodes, edges) + const qcNode = layoutCallArgs!.children!.find((c: ElkChild) => c.id === 'qc-1')! + expect(qcNode.ports).toHaveLength(2) + }) + + it('should include disconnected nodes in the layout', async () => { + const nodes = [ + makeWorkflowNode({ id: 'start', data: { type: BlockEnum.Start, title: '', desc: '' } }), + makeWorkflowNode({ id: 'connected', data: { type: BlockEnum.Code, title: '', desc: '' } }), + makeWorkflowNode({ id: 'isolated', data: { type: BlockEnum.Code, title: '', desc: '' } }), + ] + const edges = [ + makeWorkflowEdge({ source: 'start', target: 'connected' }), + ] + + await getLayoutByELK(nodes, edges) + const childIds = layoutCallArgs!.children!.map((c: ElkChild) => c.id) + expect(childIds).toContain('isolated') + expect(childIds).toHaveLength(3) + }) + + it('should build edges in DFS order matching port order', async () => { + const nodes = [ + makeWorkflowNode({ id: 'start', data: { type: BlockEnum.Start, title: '', desc: '' } }), + makeWorkflowNode({ + id: 'if-1', + data: { type: BlockEnum.IfElse, title: '', desc: '', cases: [{ case_id: 'case-a' }] }, + }), + makeWorkflowNode({ id: 'a', data: { type: BlockEnum.Code, title: '', desc: '' } }), + makeWorkflowNode({ id: 'b', data: { type: BlockEnum.Code, title: '', desc: '' } }), + ] + const edges = [ + makeWorkflowEdge({ source: 'start', target: 'if-1' }), + makeWorkflowEdge({ id: 'e-else', source: 'if-1', target: 'b', sourceHandle: 'false' }), + makeWorkflowEdge({ id: 'e-a', source: 'if-1', target: 'a', sourceHandle: 'case-a' }), + ] + + await getLayoutByELK(nodes, edges) + const elkEdges = layoutCallArgs!.edges as Array<{ sources: string[], targets: string[] }> + const ifEdges = elkEdges.filter(e => e.sources[0] === 'if-1') + expect(ifEdges[0].targets[0]).toBe('a') + expect(ifEdges[1].targets[0]).toBe('b') + }) + + it('should keep edges for components where every node has an incoming edge', async () => { + const nodes = [ + makeWorkflowNode({ + id: 'if-1', + data: { type: BlockEnum.IfElse, title: '', desc: '', cases: [{ case_id: 'case-a' }] }, + }), + makeWorkflowNode({ id: 'a', data: { type: BlockEnum.Code, title: '', desc: '' } }), + makeWorkflowNode({ id: 'b', data: { type: BlockEnum.Code, title: '', desc: '' } }), + ] + const edges = [ + makeWorkflowEdge({ id: 'e-a', source: 'if-1', target: 'a', sourceHandle: 'case-a' }), + makeWorkflowEdge({ id: 'e-b', source: 'if-1', target: 'b', sourceHandle: 'false' }), + makeWorkflowEdge({ id: 'e-back', source: 'a', target: 'if-1' }), + ] + + await getLayoutByELK(nodes, edges) + + const elkEdges = layoutCallArgs!.edges as Array<{ sources: string[], targets: string[] }> + expect(elkEdges).toHaveLength(3) + expect(elkEdges).toEqual(expect.arrayContaining([ + expect.objectContaining({ sources: ['if-1'], targets: ['a'] }), + expect.objectContaining({ sources: ['if-1'], targets: ['b'] }), + expect.objectContaining({ sources: ['a'], targets: ['if-1'] }), + ])) + }) + it('should filter loop internal edges', async () => { const nodes = [ makeWorkflowNode({ id: 'a', data: { type: BlockEnum.Start, title: '', desc: '' } }), @@ -650,6 +886,45 @@ describe('getLayoutForChildNodes', () => { expect(result!.nodes.size).toBe(2) }) + it('should build ports and DFS-order for branching nodes inside iteration', async () => { + const nodes = [ + makeWorkflowNode({ id: 'parent', data: { type: BlockEnum.Iteration, title: '', desc: '' } }), + makeWorkflowNode({ + id: 'iter-start', + type: CUSTOM_ITERATION_START_NODE, + parentId: 'parent', + data: { type: BlockEnum.IterationStart, title: '', desc: '' }, + }), + makeWorkflowNode({ + id: 'qc-child', + parentId: 'parent', + data: { + type: BlockEnum.QuestionClassifier, + title: '', + desc: '', + classes: [{ id: 'cls-1', name: 'C1' }, { id: 'cls-2', name: 'C2' }], + }, + }), + makeWorkflowNode({ id: 'upper', parentId: 'parent', data: { type: BlockEnum.Code, title: '', desc: '' } }), + makeWorkflowNode({ id: 'lower', parentId: 'parent', data: { type: BlockEnum.Code, title: '', desc: '' } }), + ] + const edges = [ + makeWorkflowEdge({ source: 'iter-start', target: 'qc-child', data: { isInIteration: true, iteration_id: 'parent' } }), + makeWorkflowEdge({ id: 'e-c2', source: 'qc-child', target: 'lower', sourceHandle: 'cls-2', data: { isInIteration: true, iteration_id: 'parent' } }), + makeWorkflowEdge({ id: 'e-c1', source: 'qc-child', target: 'upper', sourceHandle: 'cls-1', data: { isInIteration: true, iteration_id: 'parent' } }), + ] + + await getLayoutForChildNodes('parent', nodes, edges) + + const qcElk = layoutCallArgs!.children!.find((c: ElkChild) => c.id === 'qc-child')! + expect(qcElk.ports).toHaveLength(2) + expect(qcElk.ports![0].id).toContain('cls-1') + expect(qcElk.ports![1].id).toContain('cls-2') + + const childIds = layoutCallArgs!.children!.map((c: ElkChild) => c.id) + expect(childIds.indexOf('upper')).toBeLessThan(childIds.indexOf('lower')) + }) + it('should return original layout when bounds are not finite', async () => { mockReturnOverride = (graph: ElkGraph) => ({ ...graph, diff --git a/web/app/components/workflow/utils/elk-layout.ts b/web/app/components/workflow/utils/elk-layout.ts index 9860bbc770..781416f3c4 100644 --- a/web/app/components/workflow/utils/elk-layout.ts +++ b/web/app/components/workflow/utils/elk-layout.ts @@ -1,6 +1,7 @@ import type { ElkNode, LayoutOptions } from 'elkjs/lib/elk-api' import type { HumanInputNodeType } from '@/app/components/workflow/nodes/human-input/types' import type { CaseItem, IfElseNodeType } from '@/app/components/workflow/nodes/if-else/types' +import type { QuestionClassifierNodeType, Topic } from '@/app/components/workflow/nodes/question-classifier/types' import type { Edge, Node, @@ -37,13 +38,13 @@ const ROOT_LAYOUT_OPTIONS = { // === Port Configuration === 'elk.portConstraints': 'FIXED_ORDER', - 'elk.layered.considerModelOrder.strategy': 'PREFER_EDGES', + 'elk.layered.considerModelOrder.strategy': 'NODES_AND_EDGES', + 'elk.layered.crossingMinimization.forceNodeModelOrder': 'true', - // === Node Placement - Best quality === - 'elk.layered.nodePlacement.strategy': 'NETWORK_SIMPLEX', + // === Node Placement - Balanced centering === + 'elk.layered.nodePlacement.strategy': 'BRANDES_KOEPF', 'elk.layered.nodePlacement.favorStraightEdges': 'true', - 'elk.layered.nodePlacement.linearSegments.deflectionDampening': '0.5', - 'elk.layered.nodePlacement.networkSimplex.nodeFlexibility': 'NODE_SIZE', + 'elk.layered.nodePlacement.bk.fixedAlignment': 'BALANCED', // === Edge Routing - Maximum quality === 'elk.edgeRouting': 'SPLINES', @@ -56,7 +57,7 @@ const ROOT_LAYOUT_OPTIONS = { 'elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP', 'elk.layered.crossingMinimization.greedySwitch.type': 'TWO_SIDED', 'elk.layered.crossingMinimization.greedySwitchHierarchical.type': 'TWO_SIDED', - 'elk.layered.crossingMinimization.semiInteractive': 'true', + 'elk.layered.crossingMinimization.semiInteractive': 'false', 'elk.layered.crossingMinimization.hierarchicalSweepiness': '0.9', // === Layering Strategy - Best quality === @@ -115,11 +116,15 @@ const CHILD_LAYOUT_OPTIONS = { 'elk.spacing.edgeLabel': '8', 'elk.spacing.portPort': '15', - // === Node Placement - Best quality === - 'elk.layered.nodePlacement.strategy': 'NETWORK_SIMPLEX', + // === Port Configuration === + 'elk.portConstraints': 'FIXED_ORDER', + 'elk.layered.considerModelOrder.strategy': 'NODES_AND_EDGES', + 'elk.layered.crossingMinimization.forceNodeModelOrder': 'true', + + // === Node Placement - Balanced centering === + 'elk.layered.nodePlacement.strategy': 'BRANDES_KOEPF', 'elk.layered.nodePlacement.favorStraightEdges': 'true', - 'elk.layered.nodePlacement.linearSegments.deflectionDampening': '0.5', - 'elk.layered.nodePlacement.networkSimplex.nodeFlexibility': 'NODE_SIZE', + 'elk.layered.nodePlacement.bk.fixedAlignment': 'BALANCED', // === Edge Routing - Maximum quality === 'elk.edgeRouting': 'SPLINES', @@ -129,7 +134,7 @@ const CHILD_LAYOUT_OPTIONS = { // === Crossing Minimization - Aggressive === 'elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP', 'elk.layered.crossingMinimization.greedySwitch.type': 'TWO_SIDED', - 'elk.layered.crossingMinimization.semiInteractive': 'true', + 'elk.layered.crossingMinimization.semiInteractive': 'false', // === Layering Strategy === 'elk.layered.layering.strategy': 'NETWORK_SIMPLEX', @@ -197,12 +202,6 @@ type ElkEdgeShape = { targetPort?: string } -const toElkNode = (node: Node): ElkNodeShape => ({ - id: node.id, - width: node.width ?? DEFAULT_NODE_WIDTH, - height: node.height ?? DEFAULT_NODE_HEIGHT, -}) - let edgeCounter = 0 const nextEdgeId = () => `elk-edge-${edgeCounter++}` @@ -297,6 +296,24 @@ const sortIfElseOutEdges = (ifElseNode: Node, outEdges: Edge[]): Edge[] => { }) } +const sortQuestionClassifierOutEdges = (classifierNode: Node, outEdges: Edge[]): Edge[] => { + return [...outEdges].sort((edgeA, edgeB) => { + const handleA = edgeA.sourceHandle + const handleB = edgeB.sourceHandle + + if (handleA && handleB) { + const classes = (classifierNode.data as QuestionClassifierNodeType).classes || [] + const indexA = classes.findIndex((t: Topic) => t.id === handleA) + const indexB = classes.findIndex((t: Topic) => t.id === handleB) + + if (indexA !== -1 && indexB !== -1) + return indexA - indexB + } + + return 0 + }) +} + const sortHumanInputOutEdges = (humanInputNode: Node, outEdges: Edge[]): Edge[] => { return [...outEdges].sort((edgeA, edgeB) => { const handleA = edgeA.sourceHandle @@ -352,63 +369,45 @@ const normaliseBounds = (layout: LayoutResult): LayoutResult => { } } -export const getLayoutByELK = async (originNodes: Node[], originEdges: Edge[]): Promise => { - edgeCounter = 0 - const nodes = cloneDeep(originNodes).filter(node => !node.parentId && node.type === CUSTOM_NODE) - const edges = cloneDeep(originEdges).filter(edge => (!edge.data?.isInIteration && !edge.data?.isInLoop)) - +/** + * Build ELK nodes with output ports (sorted for branching types) + * and edges ordered by a DFS traversal that follows port order. + */ +const buildPortAwareGraph = (nodes: Node[], edges: Edge[]) => { const outEdgesByNode = new Map() - const inEdgesByNode = new Map() edges.forEach((edge) => { if (!outEdgesByNode.has(edge.source)) outEdgesByNode.set(edge.source, []) outEdgesByNode.get(edge.source)!.push(edge) - if (!inEdgesByNode.has(edge.target)) - inEdgesByNode.set(edge.target, []) - inEdgesByNode.get(edge.target)!.push(edge) }) const elkNodes: ElkNodeShape[] = [] const elkEdges: ElkEdgeShape[] = [] const sourcePortMap = new Map() - const targetPortMap = new Map() const sortedOutEdgesByNode = new Map() nodes.forEach((node) => { - const inEdges = inEdgesByNode.get(node.id) || [] let outEdges = outEdgesByNode.get(node.id) || [] if (node.data.type === BlockEnum.IfElse) outEdges = sortIfElseOutEdges(node, outEdges) + else if (node.data.type === BlockEnum.QuestionClassifier) + outEdges = sortQuestionClassifierOutEdges(node, outEdges) else if (node.data.type === BlockEnum.HumanInput) outEdges = sortHumanInputOutEdges(node, outEdges) sortedOutEdgesByNode.set(node.id, outEdges) - const ports: ElkPortShape[] = [] - - inEdges.forEach((edge, index) => { - const portId = `${node.id}-in-${index}` - ports.push({ - id: portId, - layoutOptions: { - 'elk.port.side': 'WEST', - 'elk.port.index': String(index), - }, - }) - targetPortMap.set(edge.id, portId) - }) - - outEdges.forEach((edge, index) => { + const ports: ElkPortShape[] = outEdges.map((edge, index) => { const portId = `${node.id}-out-${edge.sourceHandle || index}` - ports.push({ + sourcePortMap.set(edge.id, portId) + return { id: portId, layoutOptions: { 'elk.port.side': 'EAST', 'elk.port.index': String(index), }, - }) - sourcePortMap.set(edge.id, portId) + } }) elkNodes.push({ @@ -422,19 +421,51 @@ export const getLayoutByELK = async (originNodes: Node[], originEdges: Edge[]): }) }) - // Build edges in sorted per-node order so PREFER_EDGES aligns with port order - nodes.forEach((node) => { - const outEdges = sortedOutEdgesByNode.get(node.id) || [] + // DFS in port order to determine the definitive vertical ordering of nodes. + // forceNodeModelOrder makes ELK respect the children-array order within each layer. + const nodeIdSet = new Set(nodes.map(n => n.id)) + const visited = new Set() + const orderedIds: string[] = [] + + const dfs = (id: string) => { + if (visited.has(id) || !nodeIdSet.has(id)) + return + visited.add(id) + orderedIds.push(id) + const outEdges = sortedOutEdgesByNode.get(id) || [] + outEdges.forEach(e => dfs(e.target)) + } + + nodes.forEach((n) => { + if (!edges.some(e => e.target === n.id)) + dfs(n.id) + }) + nodes.forEach(n => dfs(n.id)) + + const nodeOrder = new Map(orderedIds.map((id, i) => [id, i])) + elkNodes.sort((a, b) => (nodeOrder.get(a.id) ?? 0) - (nodeOrder.get(b.id) ?? 0)) + + orderedIds.forEach((id) => { + const outEdges = sortedOutEdgesByNode.get(id) || [] outEdges.forEach((edge) => { elkEdges.push(createEdge( edge.source, edge.target, sourcePortMap.get(edge.id), - targetPortMap.get(edge.id), )) }) }) + return { elkNodes, elkEdges } +} + +export const getLayoutByELK = async (originNodes: Node[], originEdges: Edge[]): Promise => { + edgeCounter = 0 + const nodes = cloneDeep(originNodes).filter(node => !node.parentId && node.type === CUSTOM_NODE) + const edges = cloneDeep(originEdges).filter(edge => (!edge.data?.isInIteration && !edge.data?.isInLoop)) + + const { elkNodes, elkEdges } = buildPortAwareGraph(nodes, edges) + const graph = { id: 'workflow-root', layoutOptions: ROOT_LAYOUT_OPTIONS, @@ -443,7 +474,6 @@ export const getLayoutByELK = async (originNodes: Node[], originEdges: Edge[]): } const layoutedGraph = await elk.layout(graph) - // No need to filter dummy nodes anymore, as we're using ports const layout = collectLayout(layoutedGraph, () => true) return normaliseBounds(layout) } @@ -532,8 +562,7 @@ export const getLayoutForChildNodes = async ( || (edge.data?.isInLoop && edge.data?.loop_id === parentNodeId), ) - const elkNodes: ElkNodeShape[] = nodes.map(toElkNode) - const elkEdges: ElkEdgeShape[] = edges.map(edge => createEdge(edge.source, edge.target)) + const { elkNodes, elkEdges } = buildPortAwareGraph(nodes, edges) const graph = { id: parentNodeId, From 4bd388669aedc342ccc76ae7529785d615b10323 Mon Sep 17 00:00:00 2001 From: Renzo <170978465+RenzoMXD@users.noreply.github.com> Date: Tue, 31 Mar 2026 21:20:56 -0500 Subject: [PATCH 055/199] refactor: core/app pipeline, core/datasource, and core/indexing_runner (#34359) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../app/apps/pipeline/pipeline_generator.py | 2 +- api/core/app/apps/pipeline/pipeline_runner.py | 17 ++- .../datasource/datasource_file_manager.py | 8 +- api/core/indexing_runner.py | 105 ++++++++++-------- .../apps/pipeline/test_pipeline_generator.py | 2 +- .../app/apps/pipeline/test_pipeline_runner.py | 25 +---- .../test_datasource_file_manager.py | 50 +++------ .../core/rag/indexing/test_indexing_runner.py | 73 ++++++------ 8 files changed, 131 insertions(+), 151 deletions(-) diff --git a/api/core/app/apps/pipeline/pipeline_generator.py b/api/core/app/apps/pipeline/pipeline_generator.py index fa242003a2..9cc1a197d5 100644 --- a/api/core/app/apps/pipeline/pipeline_generator.py +++ b/api/core/app/apps/pipeline/pipeline_generator.py @@ -302,7 +302,7 @@ class PipelineGenerator(BaseAppGenerator): """ with preserve_flask_contexts(flask_app, context_vars=context): # init queue manager - workflow = db.session.query(Workflow).where(Workflow.id == workflow_id).first() + workflow = db.session.get(Workflow, workflow_id) if not workflow: raise ValueError(f"Workflow not found: {workflow_id}") queue_manager = PipelineQueueManager( diff --git a/api/core/app/apps/pipeline/pipeline_runner.py b/api/core/app/apps/pipeline/pipeline_runner.py index 4c188dac68..b4d2310da8 100644 --- a/api/core/app/apps/pipeline/pipeline_runner.py +++ b/api/core/app/apps/pipeline/pipeline_runner.py @@ -9,6 +9,7 @@ from graphon.graph_events import GraphEngineEvent, GraphRunFailedEvent from graphon.runtime import GraphRuntimeState, VariablePool from graphon.variable_loader import VariableLoader from graphon.variables.variables import RAGPipelineVariable, RAGPipelineVariableInput +from sqlalchemy import select from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.apps.pipeline.pipeline_config_manager import PipelineConfig @@ -84,13 +85,13 @@ class PipelineRunner(WorkflowBasedAppRunner): user_id = None if invoke_from in {InvokeFrom.WEB_APP, InvokeFrom.SERVICE_API}: - end_user = db.session.query(EndUser).where(EndUser.id == self.application_generate_entity.user_id).first() + end_user = db.session.get(EndUser, self.application_generate_entity.user_id) if end_user: user_id = end_user.session_id else: user_id = self.application_generate_entity.user_id - pipeline = db.session.query(Pipeline).where(Pipeline.id == app_config.app_id).first() + pipeline = db.session.get(Pipeline, app_config.app_id) if not pipeline: raise ValueError("Pipeline not found") @@ -213,10 +214,10 @@ class PipelineRunner(WorkflowBasedAppRunner): Get workflow """ # fetch workflow by workflow_id - workflow = ( - db.session.query(Workflow) + workflow = db.session.scalar( + select(Workflow) .where(Workflow.tenant_id == pipeline.tenant_id, Workflow.app_id == pipeline.id, Workflow.id == workflow_id) - .first() + .limit(1) ) # return workflow @@ -297,10 +298,8 @@ class PipelineRunner(WorkflowBasedAppRunner): """ if isinstance(event, GraphRunFailedEvent): if document_id and dataset_id: - document = ( - db.session.query(Document) - .where(Document.id == document_id, Document.dataset_id == dataset_id) - .first() + document = db.session.scalar( + select(Document).where(Document.id == document_id, Document.dataset_id == dataset_id).limit(1) ) if document: document.indexing_status = "error" diff --git a/api/core/datasource/datasource_file_manager.py b/api/core/datasource/datasource_file_manager.py index fe40d8f0e5..492b507aa9 100644 --- a/api/core/datasource/datasource_file_manager.py +++ b/api/core/datasource/datasource_file_manager.py @@ -153,7 +153,7 @@ class DatasourceFileManager: :return: the binary of the file, mime type """ - upload_file: UploadFile | None = db.session.query(UploadFile).where(UploadFile.id == id).first() + upload_file: UploadFile | None = db.session.get(UploadFile, id) if not upload_file: return None @@ -171,7 +171,7 @@ class DatasourceFileManager: :return: the binary of the file, mime type """ - message_file: MessageFile | None = db.session.query(MessageFile).where(MessageFile.id == id).first() + message_file: MessageFile | None = db.session.get(MessageFile, id) # Check if message_file is not None if message_file is not None: @@ -185,7 +185,7 @@ class DatasourceFileManager: else: tool_file_id = None - tool_file: ToolFile | None = db.session.query(ToolFile).where(ToolFile.id == tool_file_id).first() + tool_file: ToolFile | None = db.session.get(ToolFile, tool_file_id) if not tool_file: return None @@ -203,7 +203,7 @@ class DatasourceFileManager: :return: the binary of the file, mime type """ - upload_file: UploadFile | None = db.session.query(UploadFile).where(UploadFile.id == upload_file_id).first() + upload_file: UploadFile | None = db.session.get(UploadFile, upload_file_id) if not upload_file: return None, None diff --git a/api/core/indexing_runner.py b/api/core/indexing_runner.py index 3ec17bc986..b8d5ca2f50 100644 --- a/api/core/indexing_runner.py +++ b/api/core/indexing_runner.py @@ -10,7 +10,7 @@ from typing import Any from flask import Flask, current_app from graphon.model_runtime.entities.model_entities import ModelType -from sqlalchemy import select +from sqlalchemy import delete, func, select, update from sqlalchemy.orm.exc import ObjectDeletedError from configs import dify_config @@ -78,7 +78,7 @@ class IndexingRunner: continue # get dataset - dataset = db.session.query(Dataset).filter_by(id=requeried_document.dataset_id).first() + dataset = db.session.get(Dataset, requeried_document.dataset_id) if not dataset: raise ValueError("no dataset found") @@ -95,7 +95,7 @@ class IndexingRunner: text_docs = self._extract(index_processor, requeried_document, processing_rule.to_dict()) # transform - current_user = db.session.query(Account).filter_by(id=requeried_document.created_by).first() + current_user = db.session.get(Account, requeried_document.created_by) if not current_user: raise ValueError("no current user found") current_user.set_tenant_id(dataset.tenant_id) @@ -137,23 +137,24 @@ class IndexingRunner: return # get dataset - dataset = db.session.query(Dataset).filter_by(id=requeried_document.dataset_id).first() + dataset = db.session.get(Dataset, requeried_document.dataset_id) if not dataset: raise ValueError("no dataset found") # get exist document_segment list and delete - document_segments = ( - db.session.query(DocumentSegment) - .filter_by(dataset_id=dataset.id, document_id=requeried_document.id) - .all() - ) + document_segments = db.session.scalars( + select(DocumentSegment).where( + DocumentSegment.dataset_id == dataset.id, + DocumentSegment.document_id == requeried_document.id, + ) + ).all() for document_segment in document_segments: db.session.delete(document_segment) if requeried_document.doc_form == IndexStructureType.PARENT_CHILD_INDEX: # delete child chunks - db.session.query(ChildChunk).where(ChildChunk.segment_id == document_segment.id).delete() + db.session.execute(delete(ChildChunk).where(ChildChunk.segment_id == document_segment.id)) db.session.commit() # get the process rule stmt = select(DatasetProcessRule).where(DatasetProcessRule.id == requeried_document.dataset_process_rule_id) @@ -167,7 +168,7 @@ class IndexingRunner: text_docs = self._extract(index_processor, requeried_document, processing_rule.to_dict()) # transform - current_user = db.session.query(Account).filter_by(id=requeried_document.created_by).first() + current_user = db.session.get(Account, requeried_document.created_by) if not current_user: raise ValueError("no current user found") current_user.set_tenant_id(dataset.tenant_id) @@ -207,17 +208,18 @@ class IndexingRunner: return # get dataset - dataset = db.session.query(Dataset).filter_by(id=requeried_document.dataset_id).first() + dataset = db.session.get(Dataset, requeried_document.dataset_id) if not dataset: raise ValueError("no dataset found") # get exist document_segment list and delete - document_segments = ( - db.session.query(DocumentSegment) - .filter_by(dataset_id=dataset.id, document_id=requeried_document.id) - .all() - ) + document_segments = db.session.scalars( + select(DocumentSegment).where( + DocumentSegment.dataset_id == dataset.id, + DocumentSegment.document_id == requeried_document.id, + ) + ).all() documents = [] if document_segments: @@ -289,7 +291,7 @@ class IndexingRunner: embedding_model_instance = None if dataset_id: - dataset = db.session.query(Dataset).filter_by(id=dataset_id).first() + dataset = db.session.get(Dataset, dataset_id) if not dataset: raise ValueError("Dataset not found.") if IndexTechniqueType.HIGH_QUALITY in {dataset.indexing_technique, indexing_technique}: @@ -652,24 +654,26 @@ class IndexingRunner: @staticmethod def _process_keyword_index(flask_app, dataset_id, document_id, documents): with flask_app.app_context(): - dataset = db.session.query(Dataset).filter_by(id=dataset_id).first() + dataset = db.session.get(Dataset, dataset_id) if not dataset: raise ValueError("no dataset found") keyword = Keyword(dataset) keyword.create(documents) if dataset.indexing_technique != IndexTechniqueType.HIGH_QUALITY: document_ids = [document.metadata["doc_id"] for document in documents] - db.session.query(DocumentSegment).where( - DocumentSegment.document_id == document_id, - DocumentSegment.dataset_id == dataset_id, - DocumentSegment.index_node_id.in_(document_ids), - DocumentSegment.status == SegmentStatus.INDEXING, - ).update( - { - DocumentSegment.status: SegmentStatus.COMPLETED, - DocumentSegment.enabled: True, - DocumentSegment.completed_at: naive_utc_now(), - } + db.session.execute( + update(DocumentSegment) + .where( + DocumentSegment.document_id == document_id, + DocumentSegment.dataset_id == dataset_id, + DocumentSegment.index_node_id.in_(document_ids), + DocumentSegment.status == SegmentStatus.INDEXING, + ) + .values( + status=SegmentStatus.COMPLETED, + enabled=True, + completed_at=naive_utc_now(), + ) ) db.session.commit() @@ -703,17 +707,19 @@ class IndexingRunner: ) document_ids = [document.metadata["doc_id"] for document in chunk_documents] - db.session.query(DocumentSegment).where( - DocumentSegment.document_id == dataset_document.id, - DocumentSegment.dataset_id == dataset.id, - DocumentSegment.index_node_id.in_(document_ids), - DocumentSegment.status == SegmentStatus.INDEXING, - ).update( - { - DocumentSegment.status: SegmentStatus.COMPLETED, - DocumentSegment.enabled: True, - DocumentSegment.completed_at: naive_utc_now(), - } + db.session.execute( + update(DocumentSegment) + .where( + DocumentSegment.document_id == dataset_document.id, + DocumentSegment.dataset_id == dataset.id, + DocumentSegment.index_node_id.in_(document_ids), + DocumentSegment.status == SegmentStatus.INDEXING, + ) + .values( + status=SegmentStatus.COMPLETED, + enabled=True, + completed_at=naive_utc_now(), + ) ) db.session.commit() @@ -734,10 +740,17 @@ class IndexingRunner: """ Update the document indexing status. """ - count = db.session.query(DatasetDocument).filter_by(id=document_id, is_paused=True).count() + count = ( + db.session.scalar( + select(func.count()) + .select_from(DatasetDocument) + .where(DatasetDocument.id == document_id, DatasetDocument.is_paused == True) + ) + or 0 + ) if count > 0: raise DocumentIsPausedError() - document = db.session.query(DatasetDocument).filter_by(id=document_id).first() + document = db.session.get(DatasetDocument, document_id) if not document: raise DocumentIsDeletedPausedError() @@ -745,7 +758,7 @@ class IndexingRunner: if extra_update_params: update_params.update(extra_update_params) - db.session.query(DatasetDocument).filter_by(id=document_id).update(update_params) # type: ignore + db.session.execute(update(DatasetDocument).where(DatasetDocument.id == document_id).values(update_params)) # type: ignore db.session.commit() @staticmethod @@ -753,7 +766,9 @@ class IndexingRunner: """ Update the document segment by document id. """ - db.session.query(DocumentSegment).filter_by(document_id=dataset_document_id).update(update_params) + db.session.execute( + update(DocumentSegment).where(DocumentSegment.document_id == dataset_document_id).values(update_params) + ) db.session.commit() def _transform( diff --git a/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_generator.py b/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_generator.py index 06face41fe..0047f6659d 100644 --- a/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_generator.py +++ b/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_generator.py @@ -345,7 +345,7 @@ def test_generate_raises_when_workflow_not_found(generator, mocker): mocker.patch.object(module, "preserve_flask_contexts", _dummy_preserve) session = MagicMock() - session.query.return_value.where.return_value.first.return_value = None + session.get.return_value = None mocker.patch.object(module.db, "session", session) with pytest.raises(ValueError): diff --git a/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_runner.py b/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_runner.py index ab70996f0a..c8ae288e6f 100644 --- a/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_runner.py +++ b/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_runner.py @@ -80,9 +80,7 @@ def test_get_workflow_returns_workflow(mocker, runner): pipeline = MagicMock(tenant_id="tenant", id="pipe") workflow = MagicMock(id="wf") - query = MagicMock() - query.where.return_value.first.return_value = workflow - mocker.patch.object(module.db, "session", MagicMock(query=MagicMock(return_value=query))) + mocker.patch.object(module.db, "session", MagicMock(scalar=MagicMock(return_value=workflow))) result = runner.get_workflow(pipeline=pipeline, workflow_id="wf") @@ -115,11 +113,8 @@ def test_init_rag_pipeline_graph_not_found(mocker, runner): def test_update_document_status_on_failure(mocker, runner): document = MagicMock() - query = MagicMock() - query.where.return_value.first.return_value = document - session = MagicMock() - session.query.return_value = query + session.scalar.return_value = document mocker.patch.object(module.db, "session", session) event = GraphRunFailedEvent(error="boom") @@ -189,14 +184,10 @@ def test_run_single_iteration_path(mocker): app_generate_entity.single_iteration_run = MagicMock() pipeline = MagicMock(id="pipe") - query_pipeline = MagicMock() - query_pipeline.where.return_value.first.return_value = pipeline - - query_end_user = MagicMock() - query_end_user.where.return_value.first.return_value = MagicMock(session_id="sess") + end_user = MagicMock(session_id="sess") session = MagicMock() - session.query.side_effect = [query_end_user, query_pipeline] + session.get.side_effect = [end_user, pipeline] mocker.patch.object(module.db, "session", session) runner = PipelineRunner( @@ -241,14 +232,10 @@ def test_run_normal_path_builds_graph(mocker): app_generate_entity = _build_app_generate_entity() pipeline = MagicMock(id="pipe") - query_pipeline = MagicMock() - query_pipeline.where.return_value.first.return_value = pipeline - - query_end_user = MagicMock() - query_end_user.where.return_value.first.return_value = MagicMock(session_id="sess") + end_user = MagicMock(session_id="sess") session = MagicMock() - session.query.side_effect = [query_end_user, query_pipeline] + session.get.side_effect = [end_user, pipeline] mocker.patch.object(module.db, "session", session) workflow = MagicMock( diff --git a/api/tests/unit_tests/core/datasource/test_datasource_file_manager.py b/api/tests/unit_tests/core/datasource/test_datasource_file_manager.py index 7cd1fdf06b..4f39d38831 100644 --- a/api/tests/unit_tests/core/datasource/test_datasource_file_manager.py +++ b/api/tests/unit_tests/core/datasource/test_datasource_file_manager.py @@ -287,9 +287,7 @@ class TestDatasourceFileManager: mock_upload_file.key = "some_key" mock_upload_file.mime_type = "image/png" - mock_query = mock_db.session.query.return_value - mock_where = mock_query.where.return_value - mock_where.first.return_value = mock_upload_file + mock_db.session.get.return_value = mock_upload_file mock_storage.load_once.return_value = b"file content" @@ -300,7 +298,7 @@ class TestDatasourceFileManager: assert result == (b"file content", "image/png") # Case: Not found - mock_where.first.return_value = None + mock_db.session.get.return_value = None assert DatasourceFileManager.get_file_binary("unknown") is None @patch("core.datasource.datasource_file_manager.db") @@ -314,16 +312,14 @@ class TestDatasourceFileManager: mock_tool_file.file_key = "tool_key" mock_tool_file.mimetype = "image/png" - # Mock query sequence - def mock_query(model): - m = MagicMock() + def mock_get(model, id): if model == MessageFile: - m.where.return_value.first.return_value = mock_message_file + return mock_message_file elif model == ToolFile: - m.where.return_value.first.return_value = mock_tool_file - return m + return mock_tool_file + return None - mock_db.session.query.side_effect = mock_query + mock_db.session.get.side_effect = mock_get mock_storage.load_once.return_value = b"tool content" # Execute @@ -344,15 +340,12 @@ class TestDatasourceFileManager: mock_tool_file.file_key = "tk" mock_tool_file.mimetype = "image/png" - def mock_query(model): - m = MagicMock() + def mock_get(model, id): if model == MessageFile: - m.where.return_value.first.return_value = mock_message_file - else: - m.where.return_value.first.return_value = mock_tool_file - return m + return mock_message_file + return mock_tool_file - mock_db.session.query.side_effect = mock_query + mock_db.session.get.side_effect = mock_get mock_storage.load_once.return_value = b"bits" result = DatasourceFileManager.get_file_binary_by_message_file_id("m") @@ -361,27 +354,20 @@ class TestDatasourceFileManager: @patch("core.datasource.datasource_file_manager.db") @patch("core.datasource.datasource_file_manager.storage") def test_get_file_binary_by_message_file_id_failures(self, mock_storage, mock_db): - # Setup common mock - mock_query_obj = MagicMock() - mock_db.session.query.return_value = mock_query_obj - mock_query_obj.where.return_value.first.return_value = None - # Case 1: Message file not found + mock_db.session.get.return_value = None assert DatasourceFileManager.get_file_binary_by_message_file_id("none") is None # Case 2: Message file found but tool file not found mock_message_file = MagicMock(spec=MessageFile) mock_message_file.url = None - def mock_query_v2(model): - m = MagicMock() + def mock_get_v2(model, id): if model == MessageFile: - m.where.return_value.first.return_value = mock_message_file - else: - m.where.return_value.first.return_value = None - return m + return mock_message_file + return None - mock_db.session.query.side_effect = mock_query_v2 + mock_db.session.get.side_effect = mock_get_v2 assert DatasourceFileManager.get_file_binary_by_message_file_id("msg_id") is None @patch("core.datasource.datasource_file_manager.db") @@ -392,7 +378,7 @@ class TestDatasourceFileManager: mock_upload_file.key = "upload_key" mock_upload_file.mime_type = "text/plain" - mock_db.session.query.return_value.where.return_value.first.return_value = mock_upload_file + mock_db.session.get.return_value = mock_upload_file mock_storage.load_stream.return_value = iter([b"chunk1", b"chunk2"]) @@ -404,7 +390,7 @@ class TestDatasourceFileManager: assert list(stream) == [b"chunk1", b"chunk2"] # Case: Not found - mock_db.session.query.return_value.where.return_value.first.return_value = None + mock_db.session.get.return_value = None stream, mimetype = DatasourceFileManager.get_file_generator_by_upload_file_id("none") assert stream is None assert mimetype is None diff --git a/api/tests/unit_tests/core/rag/indexing/test_indexing_runner.py b/api/tests/unit_tests/core/rag/indexing/test_indexing_runner.py index 450e716636..641c5d9ba0 100644 --- a/api/tests/unit_tests/core/rag/indexing/test_indexing_runner.py +++ b/api/tests/unit_tests/core/rag/indexing/test_indexing_runner.py @@ -795,33 +795,21 @@ class TestIndexingRunnerRun: doc = sample_dataset_documents[0] # Mock database queries - mock_dependencies["db"].session.get.return_value = doc - mock_dataset = Mock(spec=Dataset) mock_dataset.id = doc.dataset_id mock_dataset.tenant_id = doc.tenant_id mock_dataset.indexing_technique = IndexTechniqueType.ECONOMY - mock_dependencies["db"].session.query.return_value.filter_by.return_value.first.return_value = mock_dataset + + mock_current_user = MagicMock() + mock_current_user.set_tenant_id = MagicMock() + + get_dispatch = {"Document": doc, "Dataset": mock_dataset, "Account": mock_current_user} + mock_dependencies["db"].session.get.side_effect = lambda model, id: get_dispatch.get(model.__name__) mock_process_rule = Mock(spec=DatasetProcessRule) mock_process_rule.to_dict.return_value = {"mode": "automatic", "rules": {}} mock_dependencies["db"].session.scalar.return_value = mock_process_rule - # Mock current_user (Account) for _transform - mock_current_user = MagicMock() - mock_current_user.set_tenant_id = MagicMock() - - # Setup db.session.query to return different results based on the model - def mock_query_side_effect(model): - mock_query_result = MagicMock() - if model.__name__ == "Dataset": - mock_query_result.filter_by.return_value.first.return_value = mock_dataset - elif model.__name__ == "Account": - mock_query_result.filter_by.return_value.first.return_value = mock_current_user - return mock_query_result - - mock_dependencies["db"].session.query.side_effect = mock_query_side_effect - # Mock processor mock_processor = MagicMock() mock_dependencies["factory"].return_value.init_index_processor.return_value = mock_processor @@ -891,10 +879,11 @@ class TestIndexingRunnerRun: doc = sample_dataset_documents[0] # Mock database - mock_dependencies["db"].session.get.return_value = doc - mock_dataset = Mock(spec=Dataset) - mock_dependencies["db"].session.query.return_value.filter_by.return_value.first.return_value = mock_dataset + mock_dataset.tenant_id = doc.tenant_id + + get_dispatch = {"Document": doc, "Dataset": mock_dataset} + mock_dependencies["db"].session.get.side_effect = lambda model, id: get_dispatch.get(model.__name__) mock_process_rule = Mock(spec=DatasetProcessRule) mock_process_rule.to_dict.return_value = {"mode": "automatic", "rules": {}} @@ -917,11 +906,12 @@ class TestIndexingRunnerRun: runner = IndexingRunner() doc = sample_dataset_documents[0] - # Mock database to raise ObjectDeletedError - mock_dependencies["db"].session.get.return_value = doc - + # Mock database mock_dataset = Mock(spec=Dataset) - mock_dependencies["db"].session.query.return_value.filter_by.return_value.first.return_value = mock_dataset + mock_dataset.tenant_id = doc.tenant_id + + get_dispatch = {"Document": doc, "Dataset": mock_dataset} + mock_dependencies["db"].session.get.side_effect = lambda model, id: get_dispatch.get(model.__name__) mock_process_rule = Mock(spec=DatasetProcessRule) mock_process_rule.to_dict.return_value = {"mode": "automatic", "rules": {}} @@ -945,17 +935,21 @@ class TestIndexingRunnerRun: docs = sample_dataset_documents # Mock database - def get_side_effect(model_class, doc_id): - for doc in docs: - if doc.id == doc_id: - return doc - return None - - mock_dependencies["db"].session.get.side_effect = get_side_effect - mock_dataset = Mock(spec=Dataset) mock_dataset.indexing_technique = IndexTechniqueType.ECONOMY - mock_dependencies["db"].session.query.return_value.filter_by.return_value.first.return_value = mock_dataset + mock_current_user = MagicMock() + mock_current_user.set_tenant_id = MagicMock() + + doc_map = {doc.id: doc for doc in docs} + model_dispatch = {"Dataset": mock_dataset, "Account": mock_current_user} + + def get_side_effect(model_class, id): + name = model_class.__name__ + if name == "Document": + return doc_map.get(id) + return model_dispatch.get(name) + + mock_dependencies["db"].session.get.side_effect = get_side_effect mock_process_rule = Mock(spec=DatasetProcessRule) mock_process_rule.to_dict.return_value = {"mode": "automatic", "rules": {}} @@ -1035,9 +1029,8 @@ class TestIndexingRunnerRetryLogic: mock_document = Mock(spec=DatasetDocument) mock_document.id = document_id - mock_dependencies["db"].session.query.return_value.filter_by.return_value.count.return_value = 0 - mock_dependencies["db"].session.query.return_value.filter_by.return_value.first.return_value = mock_document - mock_dependencies["db"].session.query.return_value.filter_by.return_value.update.return_value = None + mock_dependencies["db"].session.scalar.return_value = 0 + mock_dependencies["db"].session.get.return_value = mock_document # Act IndexingRunner._update_document_index_status( @@ -1053,7 +1046,7 @@ class TestIndexingRunnerRetryLogic: """Test document status update when document is paused.""" # Arrange document_id = str(uuid.uuid4()) - mock_dependencies["db"].session.query.return_value.filter_by.return_value.count.return_value = 1 + mock_dependencies["db"].session.scalar.return_value = 1 # Act & Assert with pytest.raises(DocumentIsPausedError): @@ -1063,8 +1056,8 @@ class TestIndexingRunnerRetryLogic: """Test document status update when document is deleted.""" # Arrange document_id = str(uuid.uuid4()) - mock_dependencies["db"].session.query.return_value.filter_by.return_value.count.return_value = 0 - mock_dependencies["db"].session.query.return_value.filter_by.return_value.first.return_value = None + mock_dependencies["db"].session.scalar.return_value = 0 + mock_dependencies["db"].session.get.return_value = None # Act & Assert with pytest.raises(DocumentIsDeletedPausedError): From 42d7623cc6e8b38aa0913d1e006f22205b12a962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Wed, 1 Apr 2026 10:32:01 +0800 Subject: [PATCH 056/199] fix: Variable Aggregator cannot click group swich (#34361) --- .../__tests__/use-config.spec.tsx | 30 +++++++++++++++ .../variable-assigner/use-config.helpers.ts | 23 +++++++++++- .../nodes/variable-assigner/use-config.ts | 37 +++++++++++++------ 3 files changed, 77 insertions(+), 13 deletions(-) diff --git a/web/app/components/workflow/nodes/variable-assigner/__tests__/use-config.spec.tsx b/web/app/components/workflow/nodes/variable-assigner/__tests__/use-config.spec.tsx index 1137f20a0c..cb8c2db52f 100644 --- a/web/app/components/workflow/nodes/variable-assigner/__tests__/use-config.spec.tsx +++ b/web/app/components/workflow/nodes/variable-assigner/__tests__/use-config.spec.tsx @@ -91,6 +91,15 @@ const createPayload = (overrides: Partial = {}): Varia ...overrides, }) +const createPayloadWithoutAdvancedSettings = (): VariableAssignerNodeType => { + const payload = createPayload() as Omit & { + advanced_settings?: VariableAssignerNodeType['advanced_settings'] + } + delete payload.advanced_settings + + return payload as VariableAssignerNodeType +} + describe('useConfig', () => { beforeEach(() => { vi.clearAllMocks() @@ -252,4 +261,25 @@ describe('useConfig', () => { advanced_settings: expect.objectContaining({ group_enabled: false }), })) }) + + it('should not throw when enabling groups with missing advanced settings', () => { + const { result } = renderHook(() => useConfig('assigner-node', createPayloadWithoutAdvancedSettings())) + + expect(() => { + result.current.handleGroupEnabledChange(true) + }).not.toThrow() + + expect(mockHandleOutVarRenameChange).toHaveBeenCalledWith( + 'assigner-node', + ['assigner-node', 'output'], + ['assigner-node', 'Group1', 'output'], + ) + expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({ + advanced_settings: expect.objectContaining({ + group_enabled: true, + groups: [expect.objectContaining({ group_name: 'Group1' })], + }), + })) + expect(mockDeleteNodeInspectorVars).toHaveBeenCalledWith('assigner-node') + }) }) diff --git a/web/app/components/workflow/nodes/variable-assigner/use-config.helpers.ts b/web/app/components/workflow/nodes/variable-assigner/use-config.helpers.ts index 31300557b2..2cc91c65ac 100644 --- a/web/app/components/workflow/nodes/variable-assigner/use-config.helpers.ts +++ b/web/app/components/workflow/nodes/variable-assigner/use-config.helpers.ts @@ -26,7 +26,13 @@ export const updateNestedVarGroupItem = ( groupId: string, payload: VarGroupItem, ) => produce(inputs, (draft) => { + if (!draft.advanced_settings) + return + const index = draft.advanced_settings.groups.findIndex(item => item.groupId === groupId) + if (index < 0) + return + draft.advanced_settings.groups[index] = { ...draft.advanced_settings.groups[index], ...payload, @@ -37,6 +43,11 @@ export const removeGroupByIndex = ( inputs: VariableAssignerNodeType, index: number, ) => produce(inputs, (draft) => { + if (!draft.advanced_settings) + return + if (index < 0 || index >= draft.advanced_settings.groups.length) + return + draft.advanced_settings.groups.splice(index, 1) }) @@ -70,7 +81,8 @@ export const toggleGroupEnabled = ({ export const addGroup = (inputs: VariableAssignerNodeType) => { let maxInGroupName = 1 - inputs.advanced_settings.groups.forEach((item) => { + const groups = inputs.advanced_settings?.groups ?? [] + groups.forEach((item) => { const match = /(\d+)$/.exec(item.group_name) if (match) { const num = Number.parseInt(match[1], 10) @@ -80,6 +92,9 @@ export const addGroup = (inputs: VariableAssignerNodeType) => { }) return produce(inputs, (draft) => { + if (!draft.advanced_settings) + draft.advanced_settings = { group_enabled: false, groups: [] } + draft.advanced_settings.groups.push({ output_type: VarType.any, variables: [], @@ -94,6 +109,12 @@ export const renameGroup = ( groupId: string, name: string, ) => produce(inputs, (draft) => { + if (!draft.advanced_settings) + return + const index = draft.advanced_settings.groups.findIndex(item => item.groupId === groupId) + if (index < 0) + return + draft.advanced_settings.groups[index].group_name = name }) diff --git a/web/app/components/workflow/nodes/variable-assigner/use-config.ts b/web/app/components/workflow/nodes/variable-assigner/use-config.ts index 6d4b27e50b..cecf185d4f 100644 --- a/web/app/components/workflow/nodes/variable-assigner/use-config.ts +++ b/web/app/components/workflow/nodes/variable-assigner/use-config.ts @@ -54,10 +54,15 @@ const useConfig = (id: string, payload: VariableAssignerNodeType) => { const [removedGroupIndex, setRemovedGroupIndex] = useState(-1) const handleGroupRemoved = useCallback((groupId: string) => { return () => { - const index = inputs.advanced_settings.groups.findIndex(item => item.groupId === groupId) - if (isVarUsedInNodes([id, inputs.advanced_settings.groups[index].group_name, 'output'])) { + const groups = inputs.advanced_settings?.groups ?? [] + const index = groups.findIndex(item => item.groupId === groupId) + if (index < 0) + return + + const groupName = groups[index].group_name + if (isVarUsedInNodes([id, groupName, 'output'])) { showRemoveVarConfirm() - setRemovedVars([[id, inputs.advanced_settings.groups[index].group_name, 'output']]) + setRemovedVars([[id, groupName, 'output']]) setRemoveType('group') setRemovedGroupIndex(index) return @@ -67,13 +72,15 @@ const useConfig = (id: string, payload: VariableAssignerNodeType) => { }, [id, inputs, isVarUsedInNodes, setInputs, showRemoveVarConfirm]) const handleGroupEnabledChange = useCallback((enabled: boolean) => { - if (enabled && inputs.advanced_settings.groups.length === 0) { + const groups = inputs.advanced_settings?.groups ?? [] + + if (enabled && groups.length === 0) { handleOutVarRenameChange(id, [id, 'output'], [id, 'Group1', 'output']) } - if (!enabled && inputs.advanced_settings.groups.length > 0) { - if (inputs.advanced_settings.groups.length > 1) { - const useVars = inputs.advanced_settings.groups.filter((item, index) => index > 0 && isVarUsedInNodes([id, item.group_name, 'output'])) + if (!enabled && groups.length > 0) { + if (groups.length > 1) { + const useVars = groups.filter((item, index) => index > 0 && isVarUsedInNodes([id, item.group_name, 'output'])) if (useVars.length > 0) { showRemoveVarConfirm() setRemovedVars(useVars.map(item => [id, item.group_name, 'output'])) @@ -82,7 +89,7 @@ const useConfig = (id: string, payload: VariableAssignerNodeType) => { } } - handleOutVarRenameChange(id, [id, inputs.advanced_settings.groups[0].group_name, 'output'], [id, 'output']) + handleOutVarRenameChange(id, [id, groups[0].group_name, 'output'], [id, 'output']) } setInputs(toggleGroupEnabled({ inputs, enabled })) @@ -110,11 +117,16 @@ const useConfig = (id: string, payload: VariableAssignerNodeType) => { const handleVarGroupNameChange = useCallback((groupId: string) => { return (name: string) => { - const index = inputs.advanced_settings.groups.findIndex(item => item.groupId === groupId) - handleOutVarRenameChange(id, [id, inputs.advanced_settings.groups[index].group_name, 'output'], [id, name, 'output']) + const groups = inputs.advanced_settings?.groups ?? [] + const index = groups.findIndex(item => item.groupId === groupId) + if (index < 0) + return + + const oldName = groups[index].group_name + handleOutVarRenameChange(id, [id, oldName, 'output'], [id, name, 'output']) setInputs(renameGroup(inputs, groupId, name)) if (!(id in oldNameRef.current)) - oldNameRef.current[id] = inputs.advanced_settings.groups[index].group_name + oldNameRef.current[id] = oldName renameInspectNameWithDebounce(id, name) } }, [handleOutVarRenameChange, id, inputs, renameInspectNameWithDebounce, setInputs]) @@ -125,7 +137,8 @@ const useConfig = (id: string, payload: VariableAssignerNodeType) => { }) hideRemoveVarConfirm() if (removeType === 'group') { - setInputs(removeGroupByIndex(inputs, removedGroupIndex)) + if (removedGroupIndex >= 0) + setInputs(removeGroupByIndex(inputs, removedGroupIndex)) } else { // removeType === 'enableChanged' to enabled From beda78e91129874d7a2b5377f71f2cc2ca6dc1bb Mon Sep 17 00:00:00 2001 From: Renzo <170978465+RenzoMXD@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:00:05 +0200 Subject: [PATCH 057/199] refactor: select in 13 small service files (#34371) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/services/audio_service.py | 2 +- api/services/billing_service.py | 7 +++-- api/services/conversation_service.py | 12 ++++---- api/services/credit_pool_service.py | 14 ++++----- .../enterprise/account_deletion_sync.py | 5 +++- .../rag_pipeline/pipeline_generate_service.py | 2 +- .../customized/customized_retrieval.py | 12 ++++---- .../database/database_retrieval.py | 11 +++---- .../database/database_retrieval.py | 8 ++--- api/services/web_conversation_service.py | 12 ++++---- api/services/webapp_auth_service.py | 5 ++-- api/services/workflow/workflow_converter.py | 7 +++-- api/services/workspace_service.py | 7 +++-- .../unit_tests/services/test_audio_service.py | 21 ++++--------- .../services/test_billing_service.py | 30 ++++--------------- .../services/test_conversation_service.py | 19 ++++-------- 16 files changed, 72 insertions(+), 102 deletions(-) diff --git a/api/services/audio_service.py b/api/services/audio_service.py index 90e72d5f34..1c7027efb4 100644 --- a/api/services/audio_service.py +++ b/api/services/audio_service.py @@ -132,7 +132,7 @@ class AudioService: uuid.UUID(message_id) except ValueError: return None - message = db.session.query(Message).where(Message.id == message_id).first() + message = db.session.get(Message, message_id) if message is None: return None if message.answer == "" and message.status in {MessageStatus.NORMAL, MessageStatus.PAUSED}: diff --git a/api/services/billing_service.py b/api/services/billing_service.py index 54c595e0cb..9970b2e604 100644 --- a/api/services/billing_service.py +++ b/api/services/billing_service.py @@ -6,6 +6,7 @@ from typing import Literal import httpx from pydantic import TypeAdapter +from sqlalchemy import select from tenacity import retry, retry_if_exception_type, stop_before_delay, wait_fixed from typing_extensions import TypedDict from werkzeug.exceptions import InternalServerError @@ -158,10 +159,10 @@ class BillingService: def is_tenant_owner_or_admin(current_user: Account): tenant_id = current_user.current_tenant_id - join: TenantAccountJoin | None = ( - db.session.query(TenantAccountJoin) + join: TenantAccountJoin | None = db.session.scalar( + select(TenantAccountJoin) .where(TenantAccountJoin.tenant_id == tenant_id, TenantAccountJoin.account_id == current_user.id) - .first() + .limit(1) ) if not join: diff --git a/api/services/conversation_service.py b/api/services/conversation_service.py index ba1e7bb826..95482a2235 100644 --- a/api/services/conversation_service.py +++ b/api/services/conversation_service.py @@ -137,11 +137,11 @@ class ConversationService: @classmethod def auto_generate_name(cls, app_model: App, conversation: Conversation): # get conversation first message - message = ( - db.session.query(Message) + message = db.session.scalar( + select(Message) .where(Message.app_id == app_model.id, Message.conversation_id == conversation.id) .order_by(Message.created_at.asc()) - .first() + .limit(1) ) if not message: @@ -160,8 +160,8 @@ class ConversationService: @classmethod def get_conversation(cls, app_model: App, conversation_id: str, user: Union[Account, EndUser] | None): - conversation = ( - db.session.query(Conversation) + conversation = db.session.scalar( + select(Conversation) .where( Conversation.id == conversation_id, Conversation.app_id == app_model.id, @@ -170,7 +170,7 @@ class ConversationService: Conversation.from_account_id == (user.id if isinstance(user, Account) else None), Conversation.is_deleted == False, ) - .first() + .limit(1) ) if not conversation: diff --git a/api/services/credit_pool_service.py b/api/services/credit_pool_service.py index 2894826935..7826695366 100644 --- a/api/services/credit_pool_service.py +++ b/api/services/credit_pool_service.py @@ -1,6 +1,6 @@ import logging -from sqlalchemy import update +from sqlalchemy import select, update from sqlalchemy.orm import Session from configs import dify_config @@ -29,13 +29,13 @@ class CreditPoolService: @classmethod def get_pool(cls, tenant_id: str, pool_type: str = "trial") -> TenantCreditPool | None: """get tenant credit pool""" - return ( - db.session.query(TenantCreditPool) - .filter_by( - tenant_id=tenant_id, - pool_type=pool_type, + return db.session.scalar( + select(TenantCreditPool) + .where( + TenantCreditPool.tenant_id == tenant_id, + TenantCreditPool.pool_type == pool_type, ) - .first() + .limit(1) ) @classmethod diff --git a/api/services/enterprise/account_deletion_sync.py b/api/services/enterprise/account_deletion_sync.py index c7ff42894d..b5107fb0f6 100644 --- a/api/services/enterprise/account_deletion_sync.py +++ b/api/services/enterprise/account_deletion_sync.py @@ -4,6 +4,7 @@ import uuid from datetime import UTC, datetime from redis import RedisError +from sqlalchemy import select from configs import dify_config from extensions.ext_database import db @@ -104,7 +105,9 @@ def sync_account_deletion(account_id: str, *, source: str) -> bool: return True # Fetch all workspaces the account belongs to - workspace_joins = db.session.query(TenantAccountJoin).filter_by(account_id=account_id).all() + workspace_joins = db.session.scalars( + select(TenantAccountJoin).where(TenantAccountJoin.account_id == account_id) + ).all() # Queue sync task for each workspace success = True diff --git a/api/services/rag_pipeline/pipeline_generate_service.py b/api/services/rag_pipeline/pipeline_generate_service.py index 07e1b8f20e..10e89b1dba 100644 --- a/api/services/rag_pipeline/pipeline_generate_service.py +++ b/api/services/rag_pipeline/pipeline_generate_service.py @@ -110,7 +110,7 @@ class PipelineGenerateService: Update document status to waiting :param document_id: document id """ - document = db.session.query(Document).where(Document.id == document_id).first() + document = db.session.get(Document, document_id) if document: document.indexing_status = IndexingStatus.WAITING db.session.add(document) diff --git a/api/services/rag_pipeline/pipeline_template/customized/customized_retrieval.py b/api/services/rag_pipeline/pipeline_template/customized/customized_retrieval.py index 4ac2e0792b..2ee871a266 100644 --- a/api/services/rag_pipeline/pipeline_template/customized/customized_retrieval.py +++ b/api/services/rag_pipeline/pipeline_template/customized/customized_retrieval.py @@ -1,4 +1,5 @@ import yaml +from sqlalchemy import select from extensions.ext_database import db from libs.login import current_account_with_tenant @@ -32,12 +33,11 @@ class CustomizedPipelineTemplateRetrieval(PipelineTemplateRetrievalBase): :param language: language :return: """ - pipeline_customized_templates = ( - db.session.query(PipelineCustomizedTemplate) + pipeline_customized_templates = db.session.scalars( + select(PipelineCustomizedTemplate) .where(PipelineCustomizedTemplate.tenant_id == tenant_id, PipelineCustomizedTemplate.language == language) .order_by(PipelineCustomizedTemplate.position.asc(), PipelineCustomizedTemplate.created_at.desc()) - .all() - ) + ).all() recommended_pipelines_results = [] for pipeline_customized_template in pipeline_customized_templates: recommended_pipeline_result = { @@ -59,9 +59,7 @@ class CustomizedPipelineTemplateRetrieval(PipelineTemplateRetrievalBase): :param template_id: Template ID :return: """ - pipeline_template = ( - db.session.query(PipelineCustomizedTemplate).where(PipelineCustomizedTemplate.id == template_id).first() - ) + pipeline_template = db.session.get(PipelineCustomizedTemplate, template_id) if not pipeline_template: return None diff --git a/api/services/rag_pipeline/pipeline_template/database/database_retrieval.py b/api/services/rag_pipeline/pipeline_template/database/database_retrieval.py index 908f9a2684..43b21a7b32 100644 --- a/api/services/rag_pipeline/pipeline_template/database/database_retrieval.py +++ b/api/services/rag_pipeline/pipeline_template/database/database_retrieval.py @@ -1,4 +1,5 @@ import yaml +from sqlalchemy import select from extensions.ext_database import db from models.dataset import PipelineBuiltInTemplate @@ -30,8 +31,10 @@ class DatabasePipelineTemplateRetrieval(PipelineTemplateRetrievalBase): :return: """ - pipeline_built_in_templates: list[PipelineBuiltInTemplate] = ( - db.session.query(PipelineBuiltInTemplate).where(PipelineBuiltInTemplate.language == language).all() + pipeline_built_in_templates = list( + db.session.scalars( + select(PipelineBuiltInTemplate).where(PipelineBuiltInTemplate.language == language) + ).all() ) recommended_pipelines_results = [] @@ -58,9 +61,7 @@ class DatabasePipelineTemplateRetrieval(PipelineTemplateRetrievalBase): :return: """ # is in public recommended list - pipeline_template = ( - db.session.query(PipelineBuiltInTemplate).where(PipelineBuiltInTemplate.id == template_id).first() - ) + pipeline_template = db.session.get(PipelineBuiltInTemplate, template_id) if not pipeline_template: return None diff --git a/api/services/recommend_app/database/database_retrieval.py b/api/services/recommend_app/database/database_retrieval.py index d0c49325dc..6fb90d356d 100644 --- a/api/services/recommend_app/database/database_retrieval.py +++ b/api/services/recommend_app/database/database_retrieval.py @@ -77,17 +77,15 @@ class DatabaseRecommendAppRetrieval(RecommendAppRetrievalBase): :return: """ # is in public recommended list - recommended_app = ( - db.session.query(RecommendedApp) - .where(RecommendedApp.is_listed == True, RecommendedApp.app_id == app_id) - .first() + recommended_app = db.session.scalar( + select(RecommendedApp).where(RecommendedApp.is_listed == True, RecommendedApp.app_id == app_id).limit(1) ) if not recommended_app: return None # get app detail - app_model = db.session.query(App).where(App.id == app_id).first() + app_model = db.session.get(App, app_id) if not app_model or not app_model.is_public: return None diff --git a/api/services/web_conversation_service.py b/api/services/web_conversation_service.py index e028e3e5e3..5ef9e9be61 100644 --- a/api/services/web_conversation_service.py +++ b/api/services/web_conversation_service.py @@ -64,15 +64,15 @@ class WebConversationService: def pin(cls, app_model: App, conversation_id: str, user: Union[Account, EndUser] | None): if not user: return - pinned_conversation = ( - db.session.query(PinnedConversation) + pinned_conversation = db.session.scalar( + select(PinnedConversation) .where( PinnedConversation.app_id == app_model.id, PinnedConversation.conversation_id == conversation_id, PinnedConversation.created_by_role == ("account" if isinstance(user, Account) else "end_user"), PinnedConversation.created_by == user.id, ) - .first() + .limit(1) ) if pinned_conversation: @@ -96,15 +96,15 @@ class WebConversationService: def unpin(cls, app_model: App, conversation_id: str, user: Union[Account, EndUser] | None): if not user: return - pinned_conversation = ( - db.session.query(PinnedConversation) + pinned_conversation = db.session.scalar( + select(PinnedConversation) .where( PinnedConversation.app_id == app_model.id, PinnedConversation.conversation_id == conversation_id, PinnedConversation.created_by_role == ("account" if isinstance(user, Account) else "end_user"), PinnedConversation.created_by == user.id, ) - .first() + .limit(1) ) if not pinned_conversation: diff --git a/api/services/webapp_auth_service.py b/api/services/webapp_auth_service.py index 5ca0b63001..eaea79af2f 100644 --- a/api/services/webapp_auth_service.py +++ b/api/services/webapp_auth_service.py @@ -3,6 +3,7 @@ import secrets from datetime import UTC, datetime, timedelta from typing import Any +from sqlalchemy import select from werkzeug.exceptions import NotFound, Unauthorized from configs import dify_config @@ -92,10 +93,10 @@ class WebAppAuthService: @classmethod def create_end_user(cls, app_code, email) -> EndUser: - site = db.session.query(Site).where(Site.code == app_code).first() + site = db.session.scalar(select(Site).where(Site.code == app_code).limit(1)) if not site: raise NotFound("Site not found.") - app_model = db.session.query(App).where(App.id == site.app_id).first() + app_model = db.session.get(App, site.app_id) if not app_model: raise NotFound("App not found.") end_user = EndUser( diff --git a/api/services/workflow/workflow_converter.py b/api/services/workflow/workflow_converter.py index 31367f72fa..399c82849f 100644 --- a/api/services/workflow/workflow_converter.py +++ b/api/services/workflow/workflow_converter.py @@ -6,6 +6,7 @@ from graphon.model_runtime.entities.llm_entities import LLMMode from graphon.model_runtime.utils.encoders import jsonable_encoder from graphon.nodes import BuiltinNodeTypes from graphon.variables.input_entities import VariableEntity +from sqlalchemy import select from typing_extensions import TypedDict from core.app.app_config.entities import ( @@ -648,10 +649,10 @@ class WorkflowConverter: :param api_based_extension_id: api based extension id :return: """ - api_based_extension = ( - db.session.query(APIBasedExtension) + api_based_extension = db.session.scalar( + select(APIBasedExtension) .where(APIBasedExtension.tenant_id == tenant_id, APIBasedExtension.id == api_based_extension_id) - .first() + .limit(1) ) if not api_based_extension: diff --git a/api/services/workspace_service.py b/api/services/workspace_service.py index 84a8b03329..eb4671cfaa 100644 --- a/api/services/workspace_service.py +++ b/api/services/workspace_service.py @@ -1,4 +1,5 @@ from flask_login import current_user +from sqlalchemy import select from configs import dify_config from enums.cloud_plan import CloudPlan @@ -24,10 +25,10 @@ class WorkspaceService: } # Get role of user - tenant_account_join = ( - db.session.query(TenantAccountJoin) + tenant_account_join = db.session.scalar( + select(TenantAccountJoin) .where(TenantAccountJoin.tenant_id == tenant.id, TenantAccountJoin.account_id == current_user.id) - .first() + .limit(1) ) assert tenant_account_join is not None, "TenantAccountJoin not found" tenant_info["role"] = tenant_account_join.role diff --git a/api/tests/unit_tests/services/test_audio_service.py b/api/tests/unit_tests/services/test_audio_service.py index 175fd3ee01..cede6671ce 100644 --- a/api/tests/unit_tests/services/test_audio_service.py +++ b/api/tests/unit_tests/services/test_audio_service.py @@ -421,11 +421,8 @@ class TestAudioServiceTTS: answer="Message answer text", ) - # Mock database query - mock_query = MagicMock() - mock_db_session.query.return_value = mock_query - mock_query.where.return_value = mock_query - mock_query.first.return_value = message + # Mock database lookup + mock_db_session.get.return_value = message # Mock ModelManager mock_model_manager = mock_model_manager_class.return_value @@ -568,11 +565,8 @@ class TestAudioServiceTTS: # Arrange app = factory.create_app_mock() - # Mock database query returning None - mock_query = MagicMock() - mock_db_session.query.return_value = mock_query - mock_query.where.return_value = mock_query - mock_query.first.return_value = None + # Mock database lookup returning None + mock_db_session.get.return_value = None # Act result = AudioService.transcript_tts( @@ -594,11 +588,8 @@ class TestAudioServiceTTS: status=MessageStatus.NORMAL, ) - # Mock database query - mock_query = MagicMock() - mock_db_session.query.return_value = mock_query - mock_query.where.return_value = mock_query - mock_query.first.return_value = message + # Mock database lookup + mock_db_session.get.return_value = message # Act result = AudioService.transcript_tts( diff --git a/api/tests/unit_tests/services/test_billing_service.py b/api/tests/unit_tests/services/test_billing_service.py index b3d2e60802..168ab6cf0d 100644 --- a/api/tests/unit_tests/services/test_billing_service.py +++ b/api/tests/unit_tests/services/test_billing_service.py @@ -865,16 +865,11 @@ class TestBillingServiceAccountManagement: mock_join = MagicMock(spec=TenantAccountJoin) mock_join.role = TenantAccountRole.OWNER - mock_query = MagicMock() - mock_query.where.return_value.first.return_value = mock_join - mock_db_session.query.return_value = mock_query + mock_db_session.scalar.return_value = mock_join # Act - should not raise exception BillingService.is_tenant_owner_or_admin(current_user) - # Assert - mock_db_session.query.assert_called_once() - def test_is_tenant_owner_or_admin_admin(self, mock_db_session): """Test tenant owner/admin check for admin role.""" # Arrange @@ -885,16 +880,11 @@ class TestBillingServiceAccountManagement: mock_join = MagicMock(spec=TenantAccountJoin) mock_join.role = TenantAccountRole.ADMIN - mock_query = MagicMock() - mock_query.where.return_value.first.return_value = mock_join - mock_db_session.query.return_value = mock_query + mock_db_session.scalar.return_value = mock_join # Act - should not raise exception BillingService.is_tenant_owner_or_admin(current_user) - # Assert - mock_db_session.query.assert_called_once() - def test_is_tenant_owner_or_admin_normal_user_raises_error(self, mock_db_session): """Test tenant owner/admin check raises error for normal user.""" # Arrange @@ -905,9 +895,7 @@ class TestBillingServiceAccountManagement: mock_join = MagicMock(spec=TenantAccountJoin) mock_join.role = TenantAccountRole.NORMAL - mock_query = MagicMock() - mock_query.where.return_value.first.return_value = mock_join - mock_db_session.query.return_value = mock_query + mock_db_session.scalar.return_value = mock_join # Act & Assert with pytest.raises(ValueError) as exc_info: @@ -921,9 +909,7 @@ class TestBillingServiceAccountManagement: current_user.id = "account-123" current_user.current_tenant_id = "tenant-456" - mock_query = MagicMock() - mock_query.where.return_value.first.return_value = None - mock_db_session.query.return_value = mock_query + mock_db_session.scalar.return_value = None # Act & Assert with pytest.raises(ValueError) as exc_info: @@ -1135,9 +1121,7 @@ class TestBillingServiceEdgeCases: mock_join.role = TenantAccountRole.EDITOR # Editor is not privileged with patch("services.billing_service.db.session") as mock_session: - mock_query = MagicMock() - mock_query.where.return_value.first.return_value = mock_join - mock_session.query.return_value = mock_query + mock_session.scalar.return_value = mock_join # Act & Assert with pytest.raises(ValueError) as exc_info: @@ -1155,9 +1139,7 @@ class TestBillingServiceEdgeCases: mock_join.role = TenantAccountRole.DATASET_OPERATOR # Dataset operator is not privileged with patch("services.billing_service.db.session") as mock_session: - mock_query = MagicMock() - mock_query.where.return_value.first.return_value = mock_join - mock_session.query.return_value = mock_query + mock_session.scalar.return_value = mock_join # Act & Assert with pytest.raises(ValueError) as exc_info: diff --git a/api/tests/unit_tests/services/test_conversation_service.py b/api/tests/unit_tests/services/test_conversation_service.py index 1bf4c0e172..a4359f00b8 100644 --- a/api/tests/unit_tests/services/test_conversation_service.py +++ b/api/tests/unit_tests/services/test_conversation_service.py @@ -355,15 +355,13 @@ class TestConversationServiceGetConversation: from_account_id=user.id, from_source=ConversationFromSource.CONSOLE ) - mock_query = mock_db_session.query.return_value - mock_query.where.return_value.first.return_value = conversation + mock_db_session.scalar.return_value = conversation # Act result = ConversationService.get_conversation(app_model, "conv-123", user) # Assert assert result == conversation - mock_db_session.query.assert_called_once_with(Conversation) @patch("services.conversation_service.db.session") def test_get_conversation_success_with_end_user(self, mock_db_session): @@ -379,8 +377,7 @@ class TestConversationServiceGetConversation: from_end_user_id=user.id, from_source=ConversationFromSource.API ) - mock_query = mock_db_session.query.return_value - mock_query.where.return_value.first.return_value = conversation + mock_db_session.scalar.return_value = conversation # Act result = ConversationService.get_conversation(app_model, "conv-123", user) @@ -399,8 +396,7 @@ class TestConversationServiceGetConversation: app_model = ConversationServiceTestDataFactory.create_app_mock() user = ConversationServiceTestDataFactory.create_account_mock() - mock_query = mock_db_session.query.return_value - mock_query.where.return_value.first.return_value = None + mock_db_session.scalar.return_value = None # Act & Assert with pytest.raises(ConversationNotExistsError): @@ -489,8 +485,7 @@ class TestConversationServiceAutoGenerateName: ) # Mock database query to return message - mock_query = mock_db_session.query.return_value - mock_query.where.return_value.order_by.return_value.first.return_value = message + mock_db_session.scalar.return_value = message # Mock LLM generator mock_llm_generator.generate_conversation_name.return_value = "Generated Name" @@ -518,8 +513,7 @@ class TestConversationServiceAutoGenerateName: conversation = ConversationServiceTestDataFactory.create_conversation_mock() # Mock database query to return None - mock_query = mock_db_session.query.return_value - mock_query.where.return_value.order_by.return_value.first.return_value = None + mock_db_session.scalar.return_value = None # Act & Assert with pytest.raises(MessageNotExistsError): @@ -541,8 +535,7 @@ class TestConversationServiceAutoGenerateName: ) # Mock database query to return message - mock_query = mock_db_session.query.return_value - mock_query.where.return_value.order_by.return_value.first.return_value = message + mock_db_session.scalar.return_value = message # Mock LLM generator to raise exception mock_llm_generator.generate_conversation_name.side_effect = Exception("LLM Error") From 09ee8ea1f535fc86a41e8370ef520abbe10ac54f Mon Sep 17 00:00:00 2001 From: Full Stack Engineer <66432853+EndlessLucky@users.noreply.github.com> Date: Wed, 1 Apr 2026 00:22:23 -0400 Subject: [PATCH 058/199] fix: support qa_preview shape in IndexProcessor preview formatting (#34151) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Crazywoola <100913391+crazywoola@users.noreply.github.com> --- api/core/rag/index_processor/index_processor.py | 9 ++++++++- .../core/rag/indexing/test_index_processor.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 api/tests/unit_tests/core/rag/indexing/test_index_processor.py diff --git a/api/core/rag/index_processor/index_processor.py b/api/core/rag/index_processor/index_processor.py index a6d1db214b..825ae01226 100644 --- a/api/core/rag/index_processor/index_processor.py +++ b/api/core/rag/index_processor/index_processor.py @@ -35,7 +35,10 @@ class IndexProcessor: if "parent_mode" in preview: data.parent_mode = preview["parent_mode"] - for item in preview["preview"]: + # Different index processors return different preview shapes: + # - paragraph/parent-child processors: {"preview": [...]} + # - QA processor: {"qa_preview": [...]} (no "preview" key) + for item in preview.get("preview", []): if "content" in item and "child_chunks" in item: data.preview.append( PreviewItem(content=item["content"], child_chunks=item["child_chunks"], summary=None) @@ -44,6 +47,10 @@ class IndexProcessor: data.qa_preview.append(QaPreview(question=item["question"], answer=item["answer"])) elif "content" in item: data.preview.append(PreviewItem(content=item["content"], child_chunks=None, summary=None)) + + for item in preview.get("qa_preview", []): + if "question" in item and "answer" in item: + data.qa_preview.append(QaPreview(question=item["question"], answer=item["answer"])) return data def index_and_clean( diff --git a/api/tests/unit_tests/core/rag/indexing/test_index_processor.py b/api/tests/unit_tests/core/rag/indexing/test_index_processor.py new file mode 100644 index 0000000000..a3f284955b --- /dev/null +++ b/api/tests/unit_tests/core/rag/indexing/test_index_processor.py @@ -0,0 +1,15 @@ +from core.rag.index_processor.index_processor import IndexProcessor + + +class TestIndexProcessor: + def test_format_preview_supports_qa_preview_shape(self) -> None: + preview = IndexProcessor().format_preview( + "qa_model", + {"qa_chunks": [{"question": "Q1", "answer": "A1"}]}, + ) + + assert preview.chunk_structure == "qa_model" + assert preview.total_segments == 1 + assert len(preview.qa_preview) == 1 + assert preview.qa_preview[0].question == "Q1" + assert preview.qa_preview[0].answer == "A1" From c51cd42cb4e21320664b6d0e9efcf2ecbd1ddec5 Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka928@users.noreply.github.com> Date: Wed, 1 Apr 2026 01:41:44 -0400 Subject: [PATCH 059/199] refactor(api): replace json.loads with Pydantic validation in controllers and infra layers (#34277) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/controllers/console/app/workflow.py | 12 ++--- .../rag_pipeline/rag_pipeline_workflow.py | 23 ++-------- .../arize_phoenix_trace.py | 3 +- api/core/ops/mlflow_trace/mlflow_trace.py | 10 ++--- api/core/ops/ops_trace_manager.py | 23 +++++++--- api/core/ops/utils.py | 3 ++ .../alibabacloud_mysql_vector.py | 15 +++---- .../analyticdb/analyticdb_vector_openapi.py | 5 ++- .../rag/datasource/vdb/baidu/baidu_vector.py | 13 ++---- .../vdb/clickzetta/clickzetta_vector.py | 32 ++++++------- api/core/rag/datasource/vdb/field.py | 20 +++++++++ .../vdb/hologres/hologres_vector.py | 7 ++- .../rag/datasource/vdb/iris/iris_vector.py | 5 ++- .../vdb/matrixone/matrixone_vector.py | 7 +-- .../vdb/oceanbase/oceanbase_vector.py | 5 ++- .../vdb/tablestore/tablestore_vector.py | 9 ++-- .../datasource/vdb/tencent/tencent_vector.py | 12 +++-- .../datasource/vdb/tidb_vector/tidb_vector.py | 4 +- .../vdb/vikingdb/vikingdb_vector.py | 7 ++- ...tore_workflow_node_execution_repository.py | 9 ++-- .../clickzetta_volume/file_lifecycle.py | 8 +++- .../storage/google_cloud_storage.py | 7 ++- .../core/rag/datasource/vdb/test_field.py | 45 +++++++++++++++++++ 23 files changed, 170 insertions(+), 114 deletions(-) create mode 100644 api/tests/unit_tests/core/rag/datasource/vdb/test_field.py diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index 6df8f7032e..dcd24d2200 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -9,7 +9,7 @@ from graphon.enums import NodeType from graphon.file import File from graphon.graph_engine.manager import GraphEngineManager from graphon.model_runtime.utils.encoders import jsonable_encoder -from pydantic import BaseModel, Field, field_validator +from pydantic import BaseModel, Field, ValidationError, field_validator from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, Forbidden, InternalServerError, NotFound @@ -268,22 +268,18 @@ class DraftWorkflowApi(Resource): content_type = request.headers.get("Content-Type", "") - payload_data: dict[str, Any] | None = None if "application/json" in content_type: payload_data = request.get_json(silent=True) if not isinstance(payload_data, dict): return {"message": "Invalid JSON data"}, 400 + args_model = SyncDraftWorkflowPayload.model_validate(payload_data) elif "text/plain" in content_type: try: - payload_data = json.loads(request.data.decode("utf-8")) - except json.JSONDecodeError: - return {"message": "Invalid JSON data"}, 400 - if not isinstance(payload_data, dict): + args_model = SyncDraftWorkflowPayload.model_validate_json(request.data) + except (ValueError, ValidationError): return {"message": "Invalid JSON data"}, 400 else: abort(415) - - args_model = SyncDraftWorkflowPayload.model_validate(payload_data) args = args_model.model_dump() workflow_service = WorkflowService() diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py index e08cb155b6..4251e7ebac 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py @@ -5,7 +5,7 @@ from typing import Any, Literal, cast from flask import abort, request from flask_restx import Resource, marshal_with # type: ignore from graphon.model_runtime.utils.encoders import jsonable_encoder -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, ValidationError from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, Forbidden, InternalServerError, NotFound @@ -186,29 +186,14 @@ class DraftRagPipelineApi(Resource): if "application/json" in content_type: payload_dict = console_ns.payload or {} + payload = DraftWorkflowSyncPayload.model_validate(payload_dict) elif "text/plain" in content_type: try: - data = json.loads(request.data.decode("utf-8")) - if "graph" not in data or "features" not in data: - raise ValueError("graph or features not found in data") - - if not isinstance(data.get("graph"), dict): - raise ValueError("graph is not a dict") - - payload_dict = { - "graph": data.get("graph"), - "features": data.get("features"), - "hash": data.get("hash"), - "environment_variables": data.get("environment_variables"), - "conversation_variables": data.get("conversation_variables"), - "rag_pipeline_variables": data.get("rag_pipeline_variables"), - } - except json.JSONDecodeError: + payload = DraftWorkflowSyncPayload.model_validate_json(request.data) + except (ValueError, ValidationError): return {"message": "Invalid JSON data"}, 400 else: abort(415) - - payload = DraftWorkflowSyncPayload.model_validate(payload_dict) rag_pipeline_service = RagPipelineService() try: diff --git a/api/core/ops/arize_phoenix_trace/arize_phoenix_trace.py b/api/core/ops/arize_phoenix_trace/arize_phoenix_trace.py index 902f58e6b7..66933cea28 100644 --- a/api/core/ops/arize_phoenix_trace/arize_phoenix_trace.py +++ b/api/core/ops/arize_phoenix_trace/arize_phoenix_trace.py @@ -38,6 +38,7 @@ from core.ops.entities.trace_entity import ( TraceTaskName, WorkflowTraceInfo, ) +from core.ops.utils import JSON_DICT_ADAPTER from core.repositories import DifyCoreRepositoryFactory from extensions.ext_database import db from models.model import EndUser, MessageFile @@ -469,7 +470,7 @@ class ArizePhoenixDataTrace(BaseTraceInstance): llm_attributes[SpanAttributes.LLM_PROVIDER] = trace_info.message_data.model_provider if trace_info.message_data and trace_info.message_data.message_metadata: - metadata_dict = json.loads(trace_info.message_data.message_metadata) + metadata_dict = JSON_DICT_ADAPTER.validate_json(trace_info.message_data.message_metadata) if model_params := metadata_dict.get("model_parameters"): llm_attributes[SpanAttributes.LLM_INVOCATION_PARAMETERS] = json.dumps(model_params) diff --git a/api/core/ops/mlflow_trace/mlflow_trace.py b/api/core/ops/mlflow_trace/mlflow_trace.py index 946d3cdd47..3d8c1dd038 100644 --- a/api/core/ops/mlflow_trace/mlflow_trace.py +++ b/api/core/ops/mlflow_trace/mlflow_trace.py @@ -1,4 +1,3 @@ -import json import logging import os from datetime import datetime, timedelta @@ -25,6 +24,7 @@ from core.ops.entities.trace_entity import ( TraceTaskName, WorkflowTraceInfo, ) +from core.ops.utils import JSON_DICT_ADAPTER from extensions.ext_database import db from models import EndUser from models.workflow import WorkflowNodeExecutionModel @@ -153,7 +153,7 @@ class MLflowDataTrace(BaseTraceInstance): inputs = node.process_data # contains request URL if not inputs: - inputs = json.loads(node.inputs) if node.inputs else {} + inputs = JSON_DICT_ADAPTER.validate_json(node.inputs) if node.inputs else {} node_span = start_span_no_context( name=node.title, @@ -180,7 +180,7 @@ class MLflowDataTrace(BaseTraceInstance): # End node span finished_at = node.created_at + timedelta(seconds=node.elapsed_time) - outputs = json.loads(node.outputs) if node.outputs else {} + outputs = JSON_DICT_ADAPTER.validate_json(node.outputs) if node.outputs else {} if node.node_type == BuiltinNodeTypes.KNOWLEDGE_RETRIEVAL: outputs = self._parse_knowledge_retrieval_outputs(outputs) elif node.node_type == BuiltinNodeTypes.LLM: @@ -216,8 +216,8 @@ class MLflowDataTrace(BaseTraceInstance): return {}, {} try: - data = json.loads(node.process_data) - except (json.JSONDecodeError, TypeError): + data = JSON_DICT_ADAPTER.validate_json(node.process_data) + except (ValueError, TypeError): return {}, {} inputs = self._parse_prompts(data.get("prompts")) diff --git a/api/core/ops/ops_trace_manager.py b/api/core/ops/ops_trace_manager.py index 9c36d57c6f..c689a86614 100644 --- a/api/core/ops/ops_trace_manager.py +++ b/api/core/ops/ops_trace_manager.py @@ -11,8 +11,10 @@ from uuid import UUID, uuid4 from cachetools import LRUCache from flask import current_app +from pydantic import TypeAdapter from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker +from typing_extensions import TypedDict from core.helper.encrypter import batch_decrypt_token, encrypt_token, obfuscated_token from core.ops.entities.config_entity import ( @@ -33,7 +35,7 @@ from core.ops.entities.trace_entity import ( WorkflowNodeTraceInfo, WorkflowTraceInfo, ) -from core.ops.utils import get_message_data +from core.ops.utils import JSON_DICT_ADAPTER, get_message_data from extensions.ext_database import db from extensions.ext_storage import storage from models.account import Tenant @@ -50,6 +52,14 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) +class _AppTracingConfig(TypedDict, total=False): + enabled: bool + tracing_provider: str | None + + +_app_tracing_config_adapter: TypeAdapter[_AppTracingConfig] = TypeAdapter(_AppTracingConfig) + + def _lookup_app_and_workspace_names(app_id: str | None, tenant_id: str | None) -> tuple[str, str]: """Return (app_name, workspace_name) for the given IDs. Falls back to empty strings.""" app_name = "" @@ -468,7 +478,7 @@ class OpsTraceManager: if app is None: return None - app_ops_trace_config = json.loads(app.tracing) if app.tracing else None + app_ops_trace_config = _app_tracing_config_adapter.validate_json(app.tracing) if app.tracing else None if app_ops_trace_config is None: return None if not app_ops_trace_config.get("enabled"): @@ -560,7 +570,7 @@ class OpsTraceManager: raise ValueError("App not found") if not app.tracing: return {"enabled": False, "tracing_provider": None} - app_trace_config = json.loads(app.tracing) + app_trace_config = _app_tracing_config_adapter.validate_json(app.tracing) return app_trace_config @staticmethod @@ -636,7 +646,6 @@ class TraceTask: carries ``total_tokens``. Projects only the ``outputs`` column to avoid loading large JSON blobs unnecessarily. """ - import json from models.workflow import WorkflowNodeExecutionModel @@ -658,7 +667,7 @@ class TraceTask: if not raw: continue try: - outputs = json.loads(raw) if isinstance(raw, str) else raw + outputs = JSON_DICT_ADAPTER.validate_json(raw) if isinstance(raw, str) else raw except (ValueError, TypeError): continue if not isinstance(outputs, dict): @@ -1420,7 +1429,7 @@ class TraceTask: return {} try: - metadata = json.loads(message_data.message_metadata) + metadata = JSON_DICT_ADAPTER.validate_json(message_data.message_metadata) usage = metadata.get("usage", {}) time_to_first_token = usage.get("time_to_first_token") time_to_generate = usage.get("time_to_generate") @@ -1430,7 +1439,7 @@ class TraceTask: "llm_streaming_time_to_generate": time_to_generate, "is_streaming_request": time_to_first_token is not None, } - except (json.JSONDecodeError, AttributeError): + except (ValueError, AttributeError): return {} diff --git a/api/core/ops/utils.py b/api/core/ops/utils.py index 8b9a2e424a..a6f10c09ac 100644 --- a/api/core/ops/utils.py +++ b/api/core/ops/utils.py @@ -3,11 +3,14 @@ from datetime import datetime from typing import Any, Union from urllib.parse import urlparse +from pydantic import TypeAdapter from sqlalchemy import select from models.engine import db from models.model import Message +JSON_DICT_ADAPTER: TypeAdapter[dict[str, Any]] = TypeAdapter(dict[str, Any]) + def filter_none_values(data: dict[str, Any]) -> dict[str, Any]: new_data = {} diff --git a/api/core/rag/datasource/vdb/alibabacloud_mysql/alibabacloud_mysql_vector.py b/api/core/rag/datasource/vdb/alibabacloud_mysql/alibabacloud_mysql_vector.py index fdb5ffebfc..6e76827a42 100644 --- a/api/core/rag/datasource/vdb/alibabacloud_mysql/alibabacloud_mysql_vector.py +++ b/api/core/rag/datasource/vdb/alibabacloud_mysql/alibabacloud_mysql_vector.py @@ -10,6 +10,7 @@ from mysql.connector import Error as MySQLError from pydantic import BaseModel, model_validator from configs import dify_config +from core.rag.datasource.vdb.field import parse_metadata_json from core.rag.datasource.vdb.vector_base import BaseVector from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory from core.rag.datasource.vdb.vector_type import VectorType @@ -178,9 +179,7 @@ class AlibabaCloudMySQLVector(BaseVector): cur.execute(f"SELECT meta, text FROM {self.table_name} WHERE id IN ({placeholders})", ids) docs = [] for record in cur: - metadata = record["meta"] - if isinstance(metadata, str): - metadata = json.loads(metadata) + metadata = parse_metadata_json(record["meta"]) docs.append(Document(page_content=record["text"], metadata=metadata)) return docs @@ -263,15 +262,13 @@ class AlibabaCloudMySQLVector(BaseVector): # similarity = 1 / (1 + distance) similarity = 1.0 / (1.0 + distance) - metadata = record["meta"] - if isinstance(metadata, str): - metadata = json.loads(metadata) + metadata = parse_metadata_json(record["meta"]) metadata["score"] = similarity metadata["distance"] = distance if similarity >= score_threshold: docs.append(Document(page_content=record["text"], metadata=metadata)) - except (ValueError, json.JSONDecodeError) as e: + except (ValueError, TypeError) as e: logger.warning("Error processing search result: %s", e) continue @@ -306,9 +303,7 @@ class AlibabaCloudMySQLVector(BaseVector): ) docs = [] for record in cur: - metadata = record["meta"] - if isinstance(metadata, str): - metadata = json.loads(metadata) + metadata = parse_metadata_json(record["meta"]) metadata["score"] = float(record["score"]) docs.append(Document(page_content=record["text"], metadata=metadata)) return docs diff --git a/api/core/rag/datasource/vdb/analyticdb/analyticdb_vector_openapi.py b/api/core/rag/datasource/vdb/analyticdb/analyticdb_vector_openapi.py index 702200e0ac..ce626bbd7e 100644 --- a/api/core/rag/datasource/vdb/analyticdb/analyticdb_vector_openapi.py +++ b/api/core/rag/datasource/vdb/analyticdb/analyticdb_vector_openapi.py @@ -8,6 +8,7 @@ _import_err_msg = ( "please run `pip install alibabacloud_gpdb20160503 alibabacloud_tea_openapi`" ) +from core.rag.datasource.vdb.field import parse_metadata_json from core.rag.models.document import Document from extensions.ext_redis import redis_client @@ -257,7 +258,7 @@ class AnalyticdbVectorOpenAPI: documents = [] for match in response.body.matches.match: if match.score >= score_threshold: - metadata = json.loads(match.metadata.get("metadata_")) + metadata = parse_metadata_json(match.metadata.get("metadata_")) metadata["score"] = match.score doc = Document( page_content=match.metadata.get("page_content"), @@ -294,7 +295,7 @@ class AnalyticdbVectorOpenAPI: documents = [] for match in response.body.matches.match: if match.score >= score_threshold: - metadata = json.loads(match.metadata.get("metadata_")) + metadata = parse_metadata_json(match.metadata.get("metadata_")) metadata["score"] = match.score doc = Document( page_content=match.metadata.get("page_content"), diff --git a/api/core/rag/datasource/vdb/baidu/baidu_vector.py b/api/core/rag/datasource/vdb/baidu/baidu_vector.py index 9f5842e449..3173920c9c 100644 --- a/api/core/rag/datasource/vdb/baidu/baidu_vector.py +++ b/api/core/rag/datasource/vdb/baidu/baidu_vector.py @@ -29,6 +29,7 @@ from pymochow.model.table import AnnSearch, BM25SearchRequest, HNSWSearchParams, from configs import dify_config from core.rag.datasource.vdb.field import Field as VDBField +from core.rag.datasource.vdb.field import parse_metadata_json from core.rag.datasource.vdb.vector_base import BaseVector from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory from core.rag.datasource.vdb.vector_type import VectorType @@ -173,15 +174,9 @@ class BaiduVector(BaseVector): score = row.get("score", 0.0) meta = row_data.get(VDBField.METADATA_KEY, {}) - # Handle both JSON string and dict formats for backward compatibility - if isinstance(meta, str): - try: - import json - - meta = json.loads(meta) - except (json.JSONDecodeError, TypeError): - meta = {} - elif not isinstance(meta, dict): + try: + meta = parse_metadata_json(meta) + except (ValueError, TypeError): meta = {} if score >= score_threshold: diff --git a/api/core/rag/datasource/vdb/clickzetta/clickzetta_vector.py b/api/core/rag/datasource/vdb/clickzetta/clickzetta_vector.py index 8e8120fc10..a4dddc68f0 100644 --- a/api/core/rag/datasource/vdb/clickzetta/clickzetta_vector.py +++ b/api/core/rag/datasource/vdb/clickzetta/clickzetta_vector.py @@ -17,7 +17,7 @@ if TYPE_CHECKING: from clickzetta.connector.v0.connection import Connection # type: ignore from configs import dify_config -from core.rag.datasource.vdb.field import Field +from core.rag.datasource.vdb.field import Field, parse_metadata_json from core.rag.datasource.vdb.vector_base import BaseVector from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory from core.rag.embedding.embedding_base import Embeddings @@ -357,18 +357,19 @@ class ClickzettaVector(BaseVector): """ try: if raw_metadata: - metadata = json.loads(raw_metadata) + # First parse may yield a string (double-encoded JSON) so use json.loads + first_pass = json.loads(raw_metadata) # Handle double-encoded JSON - if isinstance(metadata, str): - metadata = json.loads(metadata) - - # Ensure we have a dict - if not isinstance(metadata, dict): + if isinstance(first_pass, str): + metadata = parse_metadata_json(first_pass) + elif isinstance(first_pass, dict): + metadata = first_pass + else: metadata = {} else: metadata = {} - except (json.JSONDecodeError, TypeError): + except (json.JSONDecodeError, ValueError, TypeError): logger.exception("JSON parsing failed for metadata") # Fallback: extract document_id with regex doc_id_match = re.search(r'"document_id":\s*"([^"]+)"', raw_metadata or "") @@ -930,17 +931,18 @@ class ClickzettaVector(BaseVector): # Parse metadata from JSON string (may be double-encoded) try: if row[2]: - metadata = json.loads(row[2]) + # First parse may yield a string (double-encoded JSON) + first_pass = json.loads(row[2]) - # If result is a string, it's double-encoded JSON - parse again - if isinstance(metadata, str): - metadata = json.loads(metadata) - - if not isinstance(metadata, dict): + if isinstance(first_pass, str): + metadata = parse_metadata_json(first_pass) + elif isinstance(first_pass, dict): + metadata = first_pass + else: metadata = {} else: metadata = {} - except (json.JSONDecodeError, TypeError): + except (json.JSONDecodeError, ValueError, TypeError): logger.exception("JSON parsing failed") # Fallback: extract document_id with regex diff --git a/api/core/rag/datasource/vdb/field.py b/api/core/rag/datasource/vdb/field.py index 8fc94be360..5a0fabc572 100644 --- a/api/core/rag/datasource/vdb/field.py +++ b/api/core/rag/datasource/vdb/field.py @@ -1,4 +1,24 @@ from enum import StrEnum, auto +from typing import Any + +from pydantic import TypeAdapter + +_metadata_adapter: TypeAdapter[dict[str, Any]] = TypeAdapter(dict[str, Any]) + + +def parse_metadata_json(raw: Any) -> dict[str, Any]: + """Parse metadata from a JSON string or pass through an existing dict. + + Many VDB drivers return metadata as either a JSON string or an already- + decoded dict depending on the column type and driver version. + """ + if raw is None or raw in ("", b""): + return {} + if isinstance(raw, dict): + return raw + if not isinstance(raw, (str, bytes, bytearray)): + return {} + return _metadata_adapter.validate_json(raw) class Field(StrEnum): diff --git a/api/core/rag/datasource/vdb/hologres/hologres_vector.py b/api/core/rag/datasource/vdb/hologres/hologres_vector.py index 36b259e494..13d48b5668 100644 --- a/api/core/rag/datasource/vdb/hologres/hologres_vector.py +++ b/api/core/rag/datasource/vdb/hologres/hologres_vector.py @@ -9,6 +9,7 @@ from psycopg import sql as psql from pydantic import BaseModel, model_validator from configs import dify_config +from core.rag.datasource.vdb.field import parse_metadata_json from core.rag.datasource.vdb.vector_base import BaseVector from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory from core.rag.datasource.vdb.vector_type import VectorType @@ -217,8 +218,7 @@ class HologresVector(BaseVector): text = row[2] meta = row[3] - if isinstance(meta, str): - meta = json.loads(meta) + meta = parse_metadata_json(meta) # Convert distance to similarity score (consistent with pgvector) score = 1 - distance @@ -265,8 +265,7 @@ class HologresVector(BaseVector): meta = row[2] score = row[-1] # score is the last column from return_score - if isinstance(meta, str): - meta = json.loads(meta) + meta = parse_metadata_json(meta) meta["score"] = score docs.append(Document(page_content=text, metadata=meta)) diff --git a/api/core/rag/datasource/vdb/iris/iris_vector.py b/api/core/rag/datasource/vdb/iris/iris_vector.py index 50bb2429ec..aae445e6ff 100644 --- a/api/core/rag/datasource/vdb/iris/iris_vector.py +++ b/api/core/rag/datasource/vdb/iris/iris_vector.py @@ -15,6 +15,7 @@ from typing import TYPE_CHECKING, Any from configs import dify_config from configs.middleware.vdb.iris_config import IrisVectorConfig +from core.rag.datasource.vdb.field import parse_metadata_json from core.rag.datasource.vdb.vector_base import BaseVector from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory from core.rag.datasource.vdb.vector_type import VectorType @@ -269,7 +270,7 @@ class IrisVector(BaseVector): if len(row) >= 4: text, meta_str, score = row[1], row[2], float(row[3]) if score >= score_threshold: - metadata = json.loads(meta_str) if meta_str else {} + metadata = parse_metadata_json(meta_str) metadata["score"] = score docs.append(Document(page_content=text, metadata=metadata)) return docs @@ -384,7 +385,7 @@ class IrisVector(BaseVector): meta_str = row[2] score_value = row[3] - metadata = json.loads(meta_str) if meta_str else {} + metadata = parse_metadata_json(meta_str) # Add score to metadata for hybrid search compatibility score = float(score_value) if score_value is not None else 0.0 metadata["score"] = score diff --git a/api/core/rag/datasource/vdb/matrixone/matrixone_vector.py b/api/core/rag/datasource/vdb/matrixone/matrixone_vector.py index 14955c8d7c..09ef498715 100644 --- a/api/core/rag/datasource/vdb/matrixone/matrixone_vector.py +++ b/api/core/rag/datasource/vdb/matrixone/matrixone_vector.py @@ -9,6 +9,7 @@ from mo_vector.client import MoVectorClient # type: ignore from pydantic import BaseModel, model_validator from configs import dify_config +from core.rag.datasource.vdb.field import parse_metadata_json from core.rag.datasource.vdb.vector_base import BaseVector from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory from core.rag.datasource.vdb.vector_type import VectorType @@ -196,11 +197,7 @@ class MatrixoneVector(BaseVector): docs = [] for result in results: - metadata = result.metadata - if isinstance(metadata, str): - import json - - metadata = json.loads(metadata) + metadata = parse_metadata_json(result.metadata) score = 1 - result.distance if score >= score_threshold: metadata["score"] = score diff --git a/api/core/rag/datasource/vdb/oceanbase/oceanbase_vector.py b/api/core/rag/datasource/vdb/oceanbase/oceanbase_vector.py index 86c1e65f47..82f419871c 100644 --- a/api/core/rag/datasource/vdb/oceanbase/oceanbase_vector.py +++ b/api/core/rag/datasource/vdb/oceanbase/oceanbase_vector.py @@ -10,6 +10,7 @@ from sqlalchemy.dialects.mysql import LONGTEXT from sqlalchemy.exc import SQLAlchemyError from configs import dify_config +from core.rag.datasource.vdb.field import parse_metadata_json from core.rag.datasource.vdb.vector_base import BaseVector from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory from core.rag.datasource.vdb.vector_type import VectorType @@ -366,8 +367,8 @@ class OceanBaseVector(BaseVector): # Parse metadata JSON try: - metadata = json.loads(metadata_str) if isinstance(metadata_str, str) else metadata_str - except json.JSONDecodeError: + metadata = parse_metadata_json(metadata_str) + except (ValueError, TypeError): logger.warning("Invalid JSON metadata: %s", metadata_str) metadata = {} diff --git a/api/core/rag/datasource/vdb/tablestore/tablestore_vector.py b/api/core/rag/datasource/vdb/tablestore/tablestore_vector.py index f2156afa59..4a734232ec 100644 --- a/api/core/rag/datasource/vdb/tablestore/tablestore_vector.py +++ b/api/core/rag/datasource/vdb/tablestore/tablestore_vector.py @@ -9,7 +9,7 @@ from pydantic import BaseModel, model_validator from tablestore import BatchGetRowRequest, TableInBatchGetRowItem from configs import dify_config -from core.rag.datasource.vdb.field import Field +from core.rag.datasource.vdb.field import Field, parse_metadata_json from core.rag.datasource.vdb.vector_base import BaseVector from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory from core.rag.datasource.vdb.vector_type import VectorType @@ -73,7 +73,8 @@ class TableStoreVector(BaseVector): for item in table_result: if item.is_ok and item.row: kv = {k: v for k, v, _ in item.row.attribute_columns} - docs.append(Document(page_content=kv[Field.CONTENT_KEY], metadata=json.loads(kv[Field.METADATA_KEY]))) + metadata = parse_metadata_json(kv[Field.METADATA_KEY]) + docs.append(Document(page_content=kv[Field.CONTENT_KEY], metadata=metadata)) return docs def get_type(self) -> str: @@ -311,7 +312,7 @@ class TableStoreVector(BaseVector): metadata_str = ots_column_map.get(Field.METADATA_KEY) vector = json.loads(vector_str) if vector_str else None - metadata = json.loads(metadata_str) if metadata_str else {} + metadata = parse_metadata_json(metadata_str) metadata["score"] = search_hit.score @@ -371,7 +372,7 @@ class TableStoreVector(BaseVector): ots_column_map[col[0]] = col[1] metadata_str = ots_column_map.get(Field.METADATA_KEY) - metadata = json.loads(metadata_str) if metadata_str else {} + metadata = parse_metadata_json(metadata_str) vector_str = ots_column_map.get(Field.VECTOR) vector = json.loads(vector_str) if vector_str else None diff --git a/api/core/rag/datasource/vdb/tencent/tencent_vector.py b/api/core/rag/datasource/vdb/tencent/tencent_vector.py index 291d047c04..829db9db20 100644 --- a/api/core/rag/datasource/vdb/tencent/tencent_vector.py +++ b/api/core/rag/datasource/vdb/tencent/tencent_vector.py @@ -11,6 +11,7 @@ from tcvectordb.model import index as vdb_index # type: ignore from tcvectordb.model.document import AnnSearch, Filter, KeywordSearch, WeightedRerank # type: ignore from configs import dify_config +from core.rag.datasource.vdb.field import parse_metadata_json from core.rag.datasource.vdb.vector_base import BaseVector from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory from core.rag.datasource.vdb.vector_type import VectorType @@ -286,13 +287,10 @@ class TencentVector(BaseVector): return docs for result in res[0]: - meta = result.get(self.field_metadata) - if isinstance(meta, str): - # Compatible with version 1.1.3 and below. - meta = json.loads(meta) - score = 1 - result.get("score", 0.0) - else: - score = result.get("score", 0.0) + raw_meta = result.get(self.field_metadata) + # Compatible with version 1.1.3 and below: str means old driver. + score = (1 - result.get("score", 0.0)) if isinstance(raw_meta, str) else result.get("score", 0.0) + meta = parse_metadata_json(raw_meta) if score >= score_threshold: meta["score"] = score doc = Document(page_content=result.get(self.field_text), metadata=meta) diff --git a/api/core/rag/datasource/vdb/tidb_vector/tidb_vector.py b/api/core/rag/datasource/vdb/tidb_vector/tidb_vector.py index 27ae038a06..c948917374 100644 --- a/api/core/rag/datasource/vdb/tidb_vector/tidb_vector.py +++ b/api/core/rag/datasource/vdb/tidb_vector/tidb_vector.py @@ -9,7 +9,7 @@ from sqlalchemy import text as sql_text from sqlalchemy.orm import Session, declarative_base from configs import dify_config -from core.rag.datasource.vdb.field import Field +from core.rag.datasource.vdb.field import Field, parse_metadata_json from core.rag.datasource.vdb.vector_base import BaseVector from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory from core.rag.datasource.vdb.vector_type import VectorType @@ -228,7 +228,7 @@ class TiDBVector(BaseVector): ) results = [(row[0], row[1], row[2]) for row in res] for meta, text, distance in results: - metadata = json.loads(meta) + metadata = parse_metadata_json(meta) metadata["score"] = 1 - distance docs.append(Document(page_content=text, metadata=metadata)) return docs diff --git a/api/core/rag/datasource/vdb/vikingdb/vikingdb_vector.py b/api/core/rag/datasource/vdb/vikingdb/vikingdb_vector.py index e5feecf2bc..83fd3626d9 100644 --- a/api/core/rag/datasource/vdb/vikingdb/vikingdb_vector.py +++ b/api/core/rag/datasource/vdb/vikingdb/vikingdb_vector.py @@ -15,6 +15,7 @@ from volcengine.viking_db import ( # type: ignore from configs import dify_config from core.rag.datasource.vdb.field import Field as vdb_Field +from core.rag.datasource.vdb.field import parse_metadata_json from core.rag.datasource.vdb.vector_base import BaseVector from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory from core.rag.datasource.vdb.vector_type import VectorType @@ -163,7 +164,7 @@ class VikingDBVector(BaseVector): for result in results: metadata = result.fields.get(vdb_Field.METADATA_KEY) if metadata is not None: - metadata = json.loads(metadata) + metadata = parse_metadata_json(metadata) if metadata.get(key) == value: ids.append(result.id) return ids @@ -189,9 +190,7 @@ class VikingDBVector(BaseVector): docs = [] for result in results: - metadata = result.fields.get(vdb_Field.METADATA_KEY) - if metadata is not None: - metadata = json.loads(metadata) + metadata = parse_metadata_json(result.fields.get(vdb_Field.METADATA_KEY)) if result.score >= score_threshold: metadata["score"] = result.score doc = Document(page_content=result.fields.get(vdb_Field.CONTENT_KEY), metadata=metadata) diff --git a/api/extensions/logstore/repositories/logstore_workflow_node_execution_repository.py b/api/extensions/logstore/repositories/logstore_workflow_node_execution_repository.py index b725436681..0e9a19b821 100644 --- a/api/extensions/logstore/repositories/logstore_workflow_node_execution_repository.py +++ b/api/extensions/logstore/repositories/logstore_workflow_node_execution_repository.py @@ -20,6 +20,7 @@ from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from sqlalchemy.engine import Engine from sqlalchemy.orm import sessionmaker +from core.ops.utils import JSON_DICT_ADAPTER from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository from core.repositories.factory import OrderConfig, WorkflowNodeExecutionRepository from extensions.logstore.aliyun_logstore import AliyunLogStore @@ -48,10 +49,10 @@ def _dict_to_workflow_node_execution(data: dict[str, Any]) -> WorkflowNodeExecut """ logger.debug("_dict_to_workflow_node_execution: data keys=%s", list(data.keys())[:5]) # Parse JSON fields - inputs = json.loads(data.get("inputs", "{}")) - process_data = json.loads(data.get("process_data", "{}")) - outputs = json.loads(data.get("outputs", "{}")) - metadata = json.loads(data.get("execution_metadata", "{}")) + inputs = JSON_DICT_ADAPTER.validate_json(data.get("inputs") or "{}") + process_data = JSON_DICT_ADAPTER.validate_json(data.get("process_data") or "{}") + outputs = JSON_DICT_ADAPTER.validate_json(data.get("outputs") or "{}") + metadata = JSON_DICT_ADAPTER.validate_json(data.get("execution_metadata") or "{}") # Convert metadata to domain enum keys domain_metadata = {} diff --git a/api/extensions/storage/clickzetta_volume/file_lifecycle.py b/api/extensions/storage/clickzetta_volume/file_lifecycle.py index 1d9911465b..483bd6bbf6 100644 --- a/api/extensions/storage/clickzetta_volume/file_lifecycle.py +++ b/api/extensions/storage/clickzetta_volume/file_lifecycle.py @@ -15,8 +15,12 @@ from datetime import datetime from enum import StrEnum, auto from typing import Any +from pydantic import TypeAdapter + logger = logging.getLogger(__name__) +_metadata_adapter: TypeAdapter[dict[str, Any]] = TypeAdapter(dict[str, Any]) + class FileStatus(StrEnum): """File status enumeration""" @@ -455,8 +459,8 @@ class FileLifecycleManager: try: if self._storage.exists(self._metadata_file): metadata_content = self._storage.load_once(self._metadata_file) - result = json.loads(metadata_content.decode("utf-8")) - return dict(result) if result else {} + result = _metadata_adapter.validate_json(metadata_content) + return result or {} else: return {} except Exception as e: diff --git a/api/extensions/storage/google_cloud_storage.py b/api/extensions/storage/google_cloud_storage.py index 4ad7e2d159..00f7289aa4 100644 --- a/api/extensions/storage/google_cloud_storage.py +++ b/api/extensions/storage/google_cloud_storage.py @@ -1,13 +1,16 @@ import base64 import io -import json from collections.abc import Generator +from typing import Any from google.cloud import storage as google_cloud_storage # type: ignore +from pydantic import TypeAdapter from configs import dify_config from extensions.storage.base_storage import BaseStorage +_service_account_adapter: TypeAdapter[dict[str, Any]] = TypeAdapter(dict[str, Any]) + class GoogleCloudStorage(BaseStorage): """Implementation for Google Cloud storage.""" @@ -21,7 +24,7 @@ class GoogleCloudStorage(BaseStorage): if service_account_json_str: service_account_json = base64.b64decode(service_account_json_str).decode("utf-8") # convert str to object - service_account_obj = json.loads(service_account_json) + service_account_obj = _service_account_adapter.validate_json(service_account_json) self.client = google_cloud_storage.Client.from_service_account_info(service_account_obj) else: self.client = google_cloud_storage.Client() diff --git a/api/tests/unit_tests/core/rag/datasource/vdb/test_field.py b/api/tests/unit_tests/core/rag/datasource/vdb/test_field.py new file mode 100644 index 0000000000..d68c93b021 --- /dev/null +++ b/api/tests/unit_tests/core/rag/datasource/vdb/test_field.py @@ -0,0 +1,45 @@ +import pytest + +from core.rag.datasource.vdb.field import parse_metadata_json + + +class TestParseMetadataJson: + def test_none_returns_empty_dict(self): + assert parse_metadata_json(None) == {} + + def test_empty_string_returns_empty_dict(self): + assert parse_metadata_json("") == {} + + def test_valid_json_string(self): + result = parse_metadata_json('{"doc_id": "abc", "score": 0.9}') + assert result == {"doc_id": "abc", "score": 0.9} + + def test_dict_passthrough(self): + original = {"doc_id": "abc", "document_id": "123"} + result = parse_metadata_json(original) + assert result == original + + def test_empty_json_object(self): + assert parse_metadata_json("{}") == {} + + def test_invalid_json_raises_value_error(self): + with pytest.raises(ValueError): + parse_metadata_json("{invalid json") + + def test_nested_metadata(self): + result = parse_metadata_json('{"doc_id": "1", "extra": {"nested": true}}') + assert result["extra"]["nested"] is True + + def test_non_str_non_dict_returns_empty_dict(self): + assert parse_metadata_json(123) == {} + assert parse_metadata_json([1, 2]) == {} + + def test_bytes_input(self): + result = parse_metadata_json(b'{"key": "value"}') + assert result == {"key": "value"} + + def test_empty_bytes_returns_empty_dict(self): + assert parse_metadata_json(b"") == {} + + def test_empty_bytearray_returns_empty_dict(self): + assert parse_metadata_json(bytearray(b"")) == {} From b23ea0397a756d7b6f267c5789a292eabbb1c502 Mon Sep 17 00:00:00 2001 From: jimmyzhuu Date: Wed, 1 Apr 2026 14:16:09 +0800 Subject: [PATCH 060/199] fix: apply Baidu Vector DB connection timeout when initializing Mochow client (#34328) --- api/core/rag/datasource/vdb/baidu/baidu_vector.py | 6 +++++- .../rag/datasource/vdb/baidu/test_baidu_vector.py | 13 +++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/api/core/rag/datasource/vdb/baidu/baidu_vector.py b/api/core/rag/datasource/vdb/baidu/baidu_vector.py index 3173920c9c..2b220fc04d 100644 --- a/api/core/rag/datasource/vdb/baidu/baidu_vector.py +++ b/api/core/rag/datasource/vdb/baidu/baidu_vector.py @@ -195,7 +195,11 @@ class BaiduVector(BaseVector): raise def _init_client(self, config) -> MochowClient: - config = Configuration(credentials=BceCredentials(config.account, config.api_key), endpoint=config.endpoint) + config = Configuration( + credentials=BceCredentials(config.account, config.api_key), + endpoint=config.endpoint, + connection_timeout_in_mills=config.connection_timeout_in_mills, + ) client = MochowClient(config) return client diff --git a/api/tests/unit_tests/core/rag/datasource/vdb/baidu/test_baidu_vector.py b/api/tests/unit_tests/core/rag/datasource/vdb/baidu/test_baidu_vector.py index c46c3d5e4b..487d021697 100644 --- a/api/tests/unit_tests/core/rag/datasource/vdb/baidu/test_baidu_vector.py +++ b/api/tests/unit_tests/core/rag/datasource/vdb/baidu/test_baidu_vector.py @@ -381,13 +381,22 @@ def test_init_client_constructs_configuration_and_client(baidu_module, monkeypat monkeypatch.setattr(baidu_module, "MochowClient", client_cls) vector = baidu_module.BaiduVector.__new__(baidu_module.BaiduVector) - config = SimpleNamespace(account="account", api_key="key", endpoint="https://endpoint") + config = SimpleNamespace( + account="account", + api_key="key", + endpoint="https://endpoint", + connection_timeout_in_mills=12_345, + ) client = vector._init_client(config) assert client == "client" credentials.assert_called_once_with("account", "key") - configuration.assert_called_once_with(credentials="credentials", endpoint="https://endpoint") + configuration.assert_called_once_with( + credentials="credentials", + endpoint="https://endpoint", + connection_timeout_in_mills=12_345, + ) client_cls.assert_called_once_with("configuration") From 31f7752ba9479e69753867cab8e3feafe7c101eb Mon Sep 17 00:00:00 2001 From: Renzo <170978465+RenzoMXD@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:03:49 +0200 Subject: [PATCH 061/199] refactor: select in 10 service files (#34373) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Asuka Minato --- api/services/agent_service.py | 24 +++---- api/services/api_based_extension_service.py | 48 ++++++++------ api/services/app_service.py | 9 ++- api/services/feedback_service.py | 22 +++---- .../rag_pipeline_transform_service.py | 7 +- api/services/recommended_app_service.py | 12 ++-- api/services/saved_message_service.py | 21 +++--- .../tools/builtin_tools_manage_service.py | 9 ++- api/services/vector_service.py | 15 ++--- api/services/workflow_service.py | 24 +++---- .../services/test_feedback_service.py | 21 +++--- .../services/test_vector_service.py | 47 ++++++-------- .../services/test_workflow_service.py | 64 +++++++------------ .../test_builtin_tools_manage_service.py | 4 +- 14 files changed, 147 insertions(+), 180 deletions(-) diff --git a/api/services/agent_service.py b/api/services/agent_service.py index 2b8a3ee594..d8f4e11e75 100644 --- a/api/services/agent_service.py +++ b/api/services/agent_service.py @@ -2,6 +2,7 @@ import threading from typing import Any import pytz +from sqlalchemy import select import contexts from core.app.app_config.easy_ui_based_app.agent.manager import AgentConfigManager @@ -23,25 +24,25 @@ class AgentService: contexts.plugin_tool_providers.set({}) contexts.plugin_tool_providers_lock.set(threading.Lock()) - conversation: Conversation | None = ( - db.session.query(Conversation) + conversation: Conversation | None = db.session.scalar( + select(Conversation) .where( Conversation.id == conversation_id, Conversation.app_id == app_model.id, ) - .first() + .limit(1) ) if not conversation: raise ValueError(f"Conversation not found: {conversation_id}") - message: Message | None = ( - db.session.query(Message) + message: Message | None = db.session.scalar( + select(Message) .where( Message.id == message_id, Message.conversation_id == conversation_id, ) - .first() + .limit(1) ) if not message: @@ -51,16 +52,11 @@ class AgentService: if conversation.from_end_user_id: # only select name field - executor = ( - db.session.query(EndUser, EndUser.name).where(EndUser.id == conversation.from_end_user_id).first() - ) + executor_name = db.session.scalar(select(EndUser.name).where(EndUser.id == conversation.from_end_user_id)) else: - executor = db.session.query(Account, Account.name).where(Account.id == conversation.from_account_id).first() + executor_name = db.session.scalar(select(Account.name).where(Account.id == conversation.from_account_id)) - if executor: - executor = executor.name - else: - executor = "Unknown" + executor = executor_name or "Unknown" assert isinstance(current_user, Account) assert current_user.timezone is not None timezone = pytz.timezone(current_user.timezone) diff --git a/api/services/api_based_extension_service.py b/api/services/api_based_extension_service.py index 3a0ed41be0..fdb377694b 100644 --- a/api/services/api_based_extension_service.py +++ b/api/services/api_based_extension_service.py @@ -1,3 +1,5 @@ +from sqlalchemy import select + from core.extension.api_based_extension_requestor import APIBasedExtensionRequestor from core.helper.encrypter import decrypt_token, encrypt_token from extensions.ext_database import db @@ -7,11 +9,12 @@ from models.api_based_extension import APIBasedExtension, APIBasedExtensionPoint class APIBasedExtensionService: @staticmethod def get_all_by_tenant_id(tenant_id: str) -> list[APIBasedExtension]: - extension_list = ( - db.session.query(APIBasedExtension) - .filter_by(tenant_id=tenant_id) - .order_by(APIBasedExtension.created_at.desc()) - .all() + extension_list = list( + db.session.scalars( + select(APIBasedExtension) + .where(APIBasedExtension.tenant_id == tenant_id) + .order_by(APIBasedExtension.created_at.desc()) + ).all() ) for extension in extension_list: @@ -36,11 +39,10 @@ class APIBasedExtensionService: @staticmethod def get_with_tenant_id(tenant_id: str, api_based_extension_id: str) -> APIBasedExtension: - extension = ( - db.session.query(APIBasedExtension) - .filter_by(tenant_id=tenant_id) - .filter_by(id=api_based_extension_id) - .first() + extension = db.session.scalar( + select(APIBasedExtension) + .where(APIBasedExtension.tenant_id == tenant_id, APIBasedExtension.id == api_based_extension_id) + .limit(1) ) if not extension: @@ -58,23 +60,27 @@ class APIBasedExtensionService: if not extension_data.id: # case one: check new data, name must be unique - is_name_existed = ( - db.session.query(APIBasedExtension) - .filter_by(tenant_id=extension_data.tenant_id) - .filter_by(name=extension_data.name) - .first() + is_name_existed = db.session.scalar( + select(APIBasedExtension) + .where( + APIBasedExtension.tenant_id == extension_data.tenant_id, + APIBasedExtension.name == extension_data.name, + ) + .limit(1) ) if is_name_existed: raise ValueError("name must be unique, it is already existed") else: # case two: check existing data, name must be unique - is_name_existed = ( - db.session.query(APIBasedExtension) - .filter_by(tenant_id=extension_data.tenant_id) - .filter_by(name=extension_data.name) - .where(APIBasedExtension.id != extension_data.id) - .first() + is_name_existed = db.session.scalar( + select(APIBasedExtension) + .where( + APIBasedExtension.tenant_id == extension_data.tenant_id, + APIBasedExtension.name == extension_data.name, + APIBasedExtension.id != extension_data.id, + ) + .limit(1) ) if is_name_existed: diff --git a/api/services/app_service.py b/api/services/app_service.py index e9aeb6c43d..87d52a3159 100644 --- a/api/services/app_service.py +++ b/api/services/app_service.py @@ -6,6 +6,7 @@ import sqlalchemy as sa from flask_sqlalchemy.pagination import Pagination from graphon.model_runtime.entities.model_entities import ModelPropertyKey, ModelType from graphon.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel +from sqlalchemy import select from configs import dify_config from constants.model_template import default_app_templates @@ -433,9 +434,7 @@ class AppService: meta["tool_icons"][tool_name] = url_prefix + provider_id + "/icon" elif provider_type == "api": try: - provider: ApiToolProvider | None = ( - db.session.query(ApiToolProvider).where(ApiToolProvider.id == provider_id).first() - ) + provider: ApiToolProvider | None = db.session.get(ApiToolProvider, provider_id) if provider is None: raise ValueError(f"provider not found for tool {tool_name}") meta["tool_icons"][tool_name] = json.loads(provider.icon) @@ -451,7 +450,7 @@ class AppService: :param app_id: app id :return: app code """ - site = db.session.query(Site).where(Site.app_id == app_id).first() + site = db.session.scalar(select(Site).where(Site.app_id == app_id).limit(1)) if not site: raise ValueError(f"App with id {app_id} not found") return str(site.code) @@ -463,7 +462,7 @@ class AppService: :param app_code: app code :return: app id """ - site = db.session.query(Site).where(Site.code == app_code).first() + site = db.session.scalar(select(Site).where(Site.code == app_code).limit(1)) if not site: raise ValueError(f"App with code {app_code} not found") return str(site.app_id) diff --git a/api/services/feedback_service.py b/api/services/feedback_service.py index e7473d371b..d6c338a830 100644 --- a/api/services/feedback_service.py +++ b/api/services/feedback_service.py @@ -4,7 +4,7 @@ import json from datetime import datetime from flask import Response -from sqlalchemy import or_ +from sqlalchemy import or_, select from extensions.ext_database import db from models.enums import FeedbackRating @@ -41,8 +41,8 @@ class FeedbackService: raise ValueError(f"Unsupported format: {format_type}") # Build base query - query = ( - db.session.query(MessageFeedback, Message, Conversation, App, Account) + stmt = ( + select(MessageFeedback, Message, Conversation, App, Account) .join(Message, MessageFeedback.message_id == Message.id) .join(Conversation, MessageFeedback.conversation_id == Conversation.id) .join(App, MessageFeedback.app_id == App.id) @@ -52,36 +52,36 @@ class FeedbackService: # Apply filters if from_source: - query = query.filter(MessageFeedback.from_source == from_source) + stmt = stmt.where(MessageFeedback.from_source == from_source) if rating: - query = query.filter(MessageFeedback.rating == rating) + stmt = stmt.where(MessageFeedback.rating == rating) if has_comment is not None: if has_comment: - query = query.filter(MessageFeedback.content.isnot(None), MessageFeedback.content != "") + stmt = stmt.where(MessageFeedback.content.isnot(None), MessageFeedback.content != "") else: - query = query.filter(or_(MessageFeedback.content.is_(None), MessageFeedback.content == "")) + stmt = stmt.where(or_(MessageFeedback.content.is_(None), MessageFeedback.content == "")) if start_date: try: start_dt = datetime.strptime(start_date, "%Y-%m-%d") - query = query.filter(MessageFeedback.created_at >= start_dt) + stmt = stmt.where(MessageFeedback.created_at >= start_dt) except ValueError: raise ValueError(f"Invalid start_date format: {start_date}. Use YYYY-MM-DD") if end_date: try: end_dt = datetime.strptime(end_date, "%Y-%m-%d") - query = query.filter(MessageFeedback.created_at <= end_dt) + stmt = stmt.where(MessageFeedback.created_at <= end_dt) except ValueError: raise ValueError(f"Invalid end_date format: {end_date}. Use YYYY-MM-DD") # Order by creation date (newest first) - query = query.order_by(MessageFeedback.created_at.desc()) + stmt = stmt.order_by(MessageFeedback.created_at.desc()) # Execute query - results = query.all() + results = db.session.execute(stmt).all() # Prepare data for export export_data = [] diff --git a/api/services/rag_pipeline/rag_pipeline_transform_service.py b/api/services/rag_pipeline/rag_pipeline_transform_service.py index 215a8c8528..c3b00fe109 100644 --- a/api/services/rag_pipeline/rag_pipeline_transform_service.py +++ b/api/services/rag_pipeline/rag_pipeline_transform_service.py @@ -6,6 +6,7 @@ from uuid import uuid4 import yaml from flask_login import current_user +from sqlalchemy import select from constants import DOCUMENT_EXTENSIONS from core.plugin.impl.plugin import PluginInstaller @@ -26,7 +27,7 @@ logger = logging.getLogger(__name__) class RagPipelineTransformService: def transform_dataset(self, dataset_id: str): - dataset = db.session.query(Dataset).where(Dataset.id == dataset_id).first() + dataset = db.session.get(Dataset, dataset_id) if not dataset: raise ValueError("Dataset not found") if dataset.pipeline_id and dataset.runtime_mode == DatasetRuntimeMode.RAG_PIPELINE: @@ -306,7 +307,7 @@ class RagPipelineTransformService: jina_node_id = "1752491761974" firecrawl_node_id = "1752565402678" - documents = db.session.query(Document).where(Document.dataset_id == dataset.id).all() + documents = db.session.scalars(select(Document).where(Document.dataset_id == dataset.id)).all() for document in documents: data_source_info_dict = document.data_source_info_dict @@ -316,7 +317,7 @@ class RagPipelineTransformService: document.data_source_type = DataSourceType.LOCAL_FILE file_id = data_source_info_dict.get("upload_file_id") if file_id: - file = db.session.query(UploadFile).where(UploadFile.id == file_id).first() + file = db.session.get(UploadFile, file_id) if file: data_source_info = json.dumps( { diff --git a/api/services/recommended_app_service.py b/api/services/recommended_app_service.py index 6b211a5632..9819822103 100644 --- a/api/services/recommended_app_service.py +++ b/api/services/recommended_app_service.py @@ -1,3 +1,5 @@ +from sqlalchemy import select + from configs import dify_config from extensions.ext_database import db from models.model import AccountTrialAppRecord, TrialApp @@ -27,7 +29,7 @@ class RecommendedAppService: apps = result["recommended_apps"] for app in apps: app_id = app["app_id"] - trial_app_model = db.session.query(TrialApp).where(TrialApp.app_id == app_id).first() + trial_app_model = db.session.scalar(select(TrialApp).where(TrialApp.app_id == app_id).limit(1)) if trial_app_model: app["can_trial"] = True else: @@ -46,7 +48,7 @@ class RecommendedAppService: result: dict = retrieval_instance.get_recommend_app_detail(app_id) if FeatureService.get_system_features().enable_trial_app: app_id = result["id"] - trial_app_model = db.session.query(TrialApp).where(TrialApp.app_id == app_id).first() + trial_app_model = db.session.scalar(select(TrialApp).where(TrialApp.app_id == app_id).limit(1)) if trial_app_model: result["can_trial"] = True else: @@ -60,10 +62,10 @@ class RecommendedAppService: :param app_id: app id :return: """ - account_trial_app_record = ( - db.session.query(AccountTrialAppRecord) + account_trial_app_record = db.session.scalar( + select(AccountTrialAppRecord) .where(AccountTrialAppRecord.app_id == app_id, AccountTrialAppRecord.account_id == account_id) - .first() + .limit(1) ) if account_trial_app_record: account_trial_app_record.count += 1 diff --git a/api/services/saved_message_service.py b/api/services/saved_message_service.py index d0f4f27968..77d1767c4b 100644 --- a/api/services/saved_message_service.py +++ b/api/services/saved_message_service.py @@ -1,5 +1,7 @@ from typing import Union +from sqlalchemy import select + from extensions.ext_database import db from libs.infinite_scroll_pagination import InfiniteScrollPagination from models import Account @@ -16,16 +18,15 @@ class SavedMessageService: ) -> InfiniteScrollPagination: if not user: raise ValueError("User is required") - saved_messages = ( - db.session.query(SavedMessage) + saved_messages = db.session.scalars( + select(SavedMessage) .where( SavedMessage.app_id == app_model.id, SavedMessage.created_by_role == ("account" if isinstance(user, Account) else "end_user"), SavedMessage.created_by == user.id, ) .order_by(SavedMessage.created_at.desc()) - .all() - ) + ).all() message_ids = [sm.message_id for sm in saved_messages] return MessageService.pagination_by_last_id( @@ -36,15 +37,15 @@ class SavedMessageService: def save(cls, app_model: App, user: Union[Account, EndUser] | None, message_id: str): if not user: return - saved_message = ( - db.session.query(SavedMessage) + saved_message = db.session.scalar( + select(SavedMessage) .where( SavedMessage.app_id == app_model.id, SavedMessage.message_id == message_id, SavedMessage.created_by_role == ("account" if isinstance(user, Account) else "end_user"), SavedMessage.created_by == user.id, ) - .first() + .limit(1) ) if saved_message: @@ -66,15 +67,15 @@ class SavedMessageService: def delete(cls, app_model: App, user: Union[Account, EndUser] | None, message_id: str): if not user: return - saved_message = ( - db.session.query(SavedMessage) + saved_message = db.session.scalar( + select(SavedMessage) .where( SavedMessage.app_id == app_model.id, SavedMessage.message_id == message_id, SavedMessage.created_by_role == ("account" if isinstance(user, Account) else "end_user"), SavedMessage.created_by == user.id, ) - .first() + .limit(1) ) if not saved_message: diff --git a/api/services/tools/builtin_tools_manage_service.py b/api/services/tools/builtin_tools_manage_service.py index 8e3c36e099..f7447d3c10 100644 --- a/api/services/tools/builtin_tools_manage_service.py +++ b/api/services/tools/builtin_tools_manage_service.py @@ -332,12 +332,11 @@ class BuiltinToolManageService: get builtin tool provider credentials """ with db.session.no_autoflush: - providers = ( - db.session.query(BuiltinToolProvider) - .filter_by(tenant_id=tenant_id, provider=provider_name) + providers = db.session.scalars( + select(BuiltinToolProvider) + .where(BuiltinToolProvider.tenant_id == tenant_id, BuiltinToolProvider.provider == provider_name) .order_by(BuiltinToolProvider.is_default.desc(), BuiltinToolProvider.created_at.asc()) - .all() - ) + ).all() if len(providers) == 0: return [] diff --git a/api/services/vector_service.py b/api/services/vector_service.py index 3f78b823a6..e7266cb8e9 100644 --- a/api/services/vector_service.py +++ b/api/services/vector_service.py @@ -1,6 +1,7 @@ import logging from graphon.model_runtime.entities.model_entities import ModelType +from sqlalchemy import delete, select from core.model_manager import ModelInstance, ModelManager from core.rag.datasource.keyword.keyword_factory import Keyword @@ -29,7 +30,7 @@ class VectorService: for segment in segments: if doc_form == IndexStructureType.PARENT_CHILD_INDEX: - dataset_document = db.session.query(DatasetDocument).filter_by(id=segment.document_id).first() + dataset_document = db.session.get(DatasetDocument, segment.document_id) if not dataset_document: logger.warning( "Expected DatasetDocument record to exist, but none was found, document_id=%s, segment_id=%s", @@ -38,11 +39,7 @@ class VectorService: ) continue # get the process rule - processing_rule = ( - db.session.query(DatasetProcessRule) - .where(DatasetProcessRule.id == dataset_document.dataset_process_rule_id) - .first() - ) + processing_rule = db.session.get(DatasetProcessRule, dataset_document.dataset_process_rule_id) if not processing_rule: raise ValueError("No processing rule found.") # get embedding model instance @@ -271,8 +268,8 @@ class VectorService: vector.delete_by_ids(old_attachment_ids) # Delete existing segment attachment bindings in one operation - db.session.query(SegmentAttachmentBinding).where(SegmentAttachmentBinding.segment_id == segment.id).delete( - synchronize_session=False + db.session.execute( + delete(SegmentAttachmentBinding).where(SegmentAttachmentBinding.segment_id == segment.id) ) if not attachment_ids: @@ -280,7 +277,7 @@ class VectorService: return # Bulk fetch upload files - only fetch needed fields - upload_file_list = db.session.query(UploadFile).where(UploadFile.id.in_(attachment_ids)).all() + upload_file_list = db.session.scalars(select(UploadFile).where(UploadFile.id.in_(attachment_ids))).all() if not upload_file_list: db.session.commit() diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index 3b3ee6dd92..8f365c7c51 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -138,14 +138,14 @@ class WorkflowService: if workflow_id: return self.get_published_workflow_by_id(app_model, workflow_id) # fetch draft workflow by app_model - workflow = ( - db.session.query(Workflow) + workflow = db.session.scalar( + select(Workflow) .where( Workflow.tenant_id == app_model.tenant_id, Workflow.app_id == app_model.id, Workflow.version == Workflow.VERSION_DRAFT, ) - .first() + .limit(1) ) # return draft workflow @@ -155,14 +155,14 @@ class WorkflowService: """ fetch published workflow by workflow_id """ - workflow = ( - db.session.query(Workflow) + workflow = db.session.scalar( + select(Workflow) .where( Workflow.tenant_id == app_model.tenant_id, Workflow.app_id == app_model.id, Workflow.id == workflow_id, ) - .first() + .limit(1) ) if not workflow: return None @@ -182,14 +182,14 @@ class WorkflowService: return None # fetch published workflow by workflow_id - workflow = ( - db.session.query(Workflow) + workflow = db.session.scalar( + select(Workflow) .where( Workflow.tenant_id == app_model.tenant_id, Workflow.app_id == app_model.id, Workflow.id == app_model.workflow_id, ) - .first() + .limit(1) ) return workflow @@ -544,14 +544,14 @@ class WorkflowService: # Use the same fallback logic as runtime: get the first available credential # ordered by is_default DESC, created_at ASC (same as tool_manager.py) - default_provider = ( - db.session.query(BuiltinToolProvider) + default_provider = db.session.scalar( + select(BuiltinToolProvider) .where( BuiltinToolProvider.tenant_id == tenant_id, BuiltinToolProvider.provider == provider, ) .order_by(BuiltinToolProvider.is_default.desc(), BuiltinToolProvider.created_at.asc()) - .first() + .limit(1) ) if not default_provider: diff --git a/api/tests/test_containers_integration_tests/services/test_feedback_service.py b/api/tests/test_containers_integration_tests/services/test_feedback_service.py index 771f406775..d82933ccb9 100644 --- a/api/tests/test_containers_integration_tests/services/test_feedback_service.py +++ b/api/tests/test_containers_integration_tests/services/test_feedback_service.py @@ -99,7 +99,7 @@ class TestFeedbackService: ) ] - mock_db_session.query.return_value = mock_query + mock_db_session.execute.return_value = mock_query # Test CSV export result = FeedbackService.export_feedbacks(app_id=sample_data["app"].id, format_type="csv") @@ -138,7 +138,7 @@ class TestFeedbackService: ) ] - mock_db_session.query.return_value = mock_query + mock_db_session.execute.return_value = mock_query # Test JSON export result = FeedbackService.export_feedbacks(app_id=sample_data["app"].id, format_type="json") @@ -175,7 +175,7 @@ class TestFeedbackService: ) ] - mock_db_session.query.return_value = mock_query + mock_db_session.execute.return_value = mock_query # Test with filters result = FeedbackService.export_feedbacks( @@ -188,11 +188,8 @@ class TestFeedbackService: format_type="csv", ) - # Verify filters were applied - assert mock_query.filter.called - filter_calls = mock_query.filter.call_args_list - # At least three filter invocations are expected (source, rating, comment) - assert len(filter_calls) >= 3 + # Verify query was executed (filters are baked into the select statement) + assert mock_db_session.execute.called def test_export_feedbacks_no_data(self, mock_db_session, sample_data): """Test exporting feedback when no data exists.""" @@ -206,7 +203,7 @@ class TestFeedbackService: mock_query.order_by.return_value = mock_query mock_query.all.return_value = [] - mock_db_session.query.return_value = mock_query + mock_db_session.execute.return_value = mock_query result = FeedbackService.export_feedbacks(app_id=sample_data["app"].id, format_type="csv") @@ -271,7 +268,7 @@ class TestFeedbackService: ) ] - mock_db_session.query.return_value = mock_query + mock_db_session.execute.return_value = mock_query # Test export result = FeedbackService.export_feedbacks(app_id=sample_data["app"].id, format_type="json") @@ -329,7 +326,7 @@ class TestFeedbackService: ) ] - mock_db_session.query.return_value = mock_query + mock_db_session.execute.return_value = mock_query # Test export result = FeedbackService.export_feedbacks(app_id=sample_data["app"].id, format_type="csv") @@ -367,7 +364,7 @@ class TestFeedbackService: ), ] - mock_db_session.query.return_value = mock_query + mock_db_session.execute.return_value = mock_query # Test export result = FeedbackService.export_feedbacks(app_id=sample_data["app"].id, format_type="json") diff --git a/api/tests/unit_tests/services/test_vector_service.py b/api/tests/unit_tests/services/test_vector_service.py index 598ff3fc3a..a78a033f4d 100644 --- a/api/tests/unit_tests/services/test_vector_service.py +++ b/api/tests/unit_tests/services/test_vector_service.py @@ -77,22 +77,12 @@ def _make_segment( def _mock_db_session_for_update_multimodel(*, upload_files: list[_UploadFileStub] | None) -> MagicMock: session = MagicMock(name="session") - binding_query = MagicMock(name="binding_query") - binding_query.where.return_value = binding_query - binding_query.delete.return_value = 1 + # db.session.execute() is used for delete(SegmentAttachmentBinding).where(...) + session.execute = MagicMock(name="execute") - upload_query = MagicMock(name="upload_query") - upload_query.where.return_value = upload_query - upload_query.all.return_value = upload_files or [] + # db.session.scalars(select(UploadFile).where(...)).all() returns upload files + session.scalars.return_value.all.return_value = upload_files or [] - def query_side_effect(model: object) -> MagicMock: - if model is vector_service_module.SegmentAttachmentBinding: - return binding_query - if model is vector_service_module.UploadFile: - return upload_query - return MagicMock(name=f"query({model})") - - session.query.side_effect = query_side_effect db_mock = MagicMock(name="db") db_mock.session = session return db_mock @@ -165,22 +155,15 @@ def _mock_parent_child_queries( ) -> MagicMock: session = MagicMock(name="session") - doc_query = MagicMock(name="doc_query") - doc_query.filter_by.return_value = doc_query - doc_query.first.return_value = dataset_document + get_dispatch: dict[object, object | None] = { + vector_service_module.DatasetDocument: dataset_document, + vector_service_module.DatasetProcessRule: processing_rule, + } - rule_query = MagicMock(name="rule_query") - rule_query.where.return_value = rule_query - rule_query.first.return_value = processing_rule + def get_side_effect(model: object, pk: object) -> object | None: + return get_dispatch.get(model) - def query_side_effect(model: object) -> MagicMock: - if model is vector_service_module.DatasetDocument: - return doc_query - if model is vector_service_module.DatasetProcessRule: - return rule_query - return MagicMock(name=f"query({model})") - - session.query.side_effect = query_side_effect + session.get.side_effect = get_side_effect db_mock = MagicMock(name="db") db_mock.session = session return db_mock @@ -609,7 +592,7 @@ def test_update_multimodel_vector_deletes_bindings_and_commits_on_empty_new_ids( vector_cls.assert_called_once_with(dataset=dataset) vector_instance.delete_by_ids.assert_called_once_with(["old-1", "old-2"]) - db_mock.session.query.assert_called_once_with(vector_service_module.SegmentAttachmentBinding) + db_mock.session.execute.assert_called_once() db_mock.session.commit.assert_called_once() db_mock.session.add_all.assert_not_called() vector_instance.add_texts.assert_not_called() @@ -644,6 +627,8 @@ def test_update_multimodel_vector_adds_bindings_and_vectors_and_skips_missing_up binding_ctor = MagicMock(side_effect=lambda **kwargs: kwargs) monkeypatch.setattr(vector_service_module, "SegmentAttachmentBinding", binding_ctor) + monkeypatch.setattr(vector_service_module, "delete", MagicMock()) + monkeypatch.setattr(vector_service_module, "select", MagicMock()) logger_mock = MagicMock() monkeypatch.setattr(vector_service_module, "logger", logger_mock) @@ -677,6 +662,8 @@ def test_update_multimodel_vector_updates_bindings_without_multimodal_vector_ops monkeypatch.setattr( vector_service_module, "SegmentAttachmentBinding", MagicMock(side_effect=lambda **kwargs: kwargs) ) + monkeypatch.setattr(vector_service_module, "delete", MagicMock()) + monkeypatch.setattr(vector_service_module, "select", MagicMock()) VectorService.update_multimodel_vector(segment=segment, attachment_ids=["file-1"], dataset=dataset) @@ -698,6 +685,8 @@ def test_update_multimodel_vector_rolls_back_and_reraises_on_error(monkeypatch: monkeypatch.setattr( vector_service_module, "SegmentAttachmentBinding", MagicMock(side_effect=lambda **kwargs: kwargs) ) + monkeypatch.setattr(vector_service_module, "delete", MagicMock()) + monkeypatch.setattr(vector_service_module, "select", MagicMock()) logger_mock = MagicMock() monkeypatch.setattr(vector_service_module, "logger", logger_mock) diff --git a/api/tests/unit_tests/services/test_workflow_service.py b/api/tests/unit_tests/services/test_workflow_service.py index cd71981bcf..1b253eb2f1 100644 --- a/api/tests/unit_tests/services/test_workflow_service.py +++ b/api/tests/unit_tests/services/test_workflow_service.py @@ -268,7 +268,7 @@ class TestWorkflowService: Provides mock implementations of: - session.add(): Adding new records - session.commit(): Committing transactions - - session.query(): Querying database + - session.scalar(): Scalar queries - session.execute(): Executing SQL statements """ with patch("services.workflow_service.db") as mock_db: @@ -276,7 +276,7 @@ class TestWorkflowService: mock_db.session = mock_session mock_session.add = MagicMock() mock_session.commit = MagicMock() - mock_session.query = MagicMock() + mock_session.scalar = MagicMock() mock_session.execute = MagicMock() yield mock_db @@ -338,10 +338,8 @@ class TestWorkflowService: app = TestWorkflowAssociatedDataFactory.create_app_mock() mock_workflow = TestWorkflowAssociatedDataFactory.create_workflow_mock() - # Mock database query - mock_query = MagicMock() - mock_db_session.session.query.return_value = mock_query - mock_query.where.return_value.first.return_value = mock_workflow + # Mock db.session.scalar() used by get_draft_workflow + mock_db_session.session.scalar.return_value = mock_workflow result = workflow_service.get_draft_workflow(app) @@ -351,10 +349,8 @@ class TestWorkflowService: """Test get_draft_workflow returns None when no draft exists.""" app = TestWorkflowAssociatedDataFactory.create_app_mock() - # Mock database query to return None - mock_query = MagicMock() - mock_db_session.session.query.return_value = mock_query - mock_query.where.return_value.first.return_value = None + # Mock db.session.scalar() to return None + mock_db_session.session.scalar.return_value = None result = workflow_service.get_draft_workflow(app) @@ -366,10 +362,8 @@ class TestWorkflowService: workflow_id = "workflow-123" mock_workflow = TestWorkflowAssociatedDataFactory.create_workflow_mock(version="v1") - # Mock database query - mock_query = MagicMock() - mock_db_session.session.query.return_value = mock_query - mock_query.where.return_value.first.return_value = mock_workflow + # Mock db.session.scalar() used by get_published_workflow_by_id + mock_db_session.session.scalar.return_value = mock_workflow result = workflow_service.get_draft_workflow(app, workflow_id=workflow_id) @@ -384,10 +378,8 @@ class TestWorkflowService: workflow_id = "workflow-123" mock_workflow = TestWorkflowAssociatedDataFactory.create_workflow_mock(workflow_id=workflow_id, version="v1") - # Mock database query - mock_query = MagicMock() - mock_db_session.session.query.return_value = mock_query - mock_query.where.return_value.first.return_value = mock_workflow + # Mock db.session.scalar() used by get_published_workflow_by_id + mock_db_session.session.scalar.return_value = mock_workflow result = workflow_service.get_published_workflow_by_id(app, workflow_id) @@ -406,10 +398,8 @@ class TestWorkflowService: workflow_id=workflow_id, version=Workflow.VERSION_DRAFT ) - # Mock database query - mock_query = MagicMock() - mock_db_session.session.query.return_value = mock_query - mock_query.where.return_value.first.return_value = mock_workflow + # Mock db.session.scalar() used by get_published_workflow_by_id + mock_db_session.session.scalar.return_value = mock_workflow with pytest.raises(IsDraftWorkflowError): workflow_service.get_published_workflow_by_id(app, workflow_id) @@ -419,10 +409,8 @@ class TestWorkflowService: app = TestWorkflowAssociatedDataFactory.create_app_mock() workflow_id = "nonexistent-workflow" - # Mock database query to return None - mock_query = MagicMock() - mock_db_session.session.query.return_value = mock_query - mock_query.where.return_value.first.return_value = None + # Mock db.session.scalar() to return None + mock_db_session.session.scalar.return_value = None result = workflow_service.get_published_workflow_by_id(app, workflow_id) @@ -434,10 +422,8 @@ class TestWorkflowService: app = TestWorkflowAssociatedDataFactory.create_app_mock(workflow_id=workflow_id) mock_workflow = TestWorkflowAssociatedDataFactory.create_workflow_mock(workflow_id=workflow_id, version="v1") - # Mock database query - mock_query = MagicMock() - mock_db_session.session.query.return_value = mock_query - mock_query.where.return_value.first.return_value = mock_workflow + # Mock db.session.scalar() used by get_published_workflow + mock_db_session.session.scalar.return_value = mock_workflow result = workflow_service.get_published_workflow(app) @@ -466,11 +452,9 @@ class TestWorkflowService: graph = TestWorkflowAssociatedDataFactory.create_valid_workflow_graph() features = {"file_upload": {"enabled": False}} - # Mock get_draft_workflow to return None (no existing draft) + # Mock db.session.scalar() to return None (no existing draft) # This simulates the first time a workflow is created for an app - mock_query = MagicMock() - mock_db_session.session.query.return_value = mock_query - mock_query.where.return_value.first.return_value = None + mock_db_session.session.scalar.return_value = None with ( patch.object(workflow_service, "validate_features_structure"), @@ -504,12 +488,10 @@ class TestWorkflowService: features = {"file_upload": {"enabled": False}} unique_hash = "test-hash-123" - # Mock existing draft workflow + # Mock existing draft workflow via db.session.scalar() mock_workflow = TestWorkflowAssociatedDataFactory.create_workflow_mock(unique_hash=unique_hash) - mock_query = MagicMock() - mock_db_session.session.query.return_value = mock_query - mock_query.where.return_value.first.return_value = mock_workflow + mock_db_session.session.scalar.return_value = mock_workflow with ( patch.object(workflow_service, "validate_features_structure"), @@ -545,12 +527,10 @@ class TestWorkflowService: graph = TestWorkflowAssociatedDataFactory.create_valid_workflow_graph() features = {} - # Mock existing draft workflow with different hash + # Mock existing draft workflow with different hash via db.session.scalar() mock_workflow = TestWorkflowAssociatedDataFactory.create_workflow_mock(unique_hash="old-hash") - mock_query = MagicMock() - mock_db_session.session.query.return_value = mock_query - mock_query.where.return_value.first.return_value = mock_workflow + mock_db_session.session.scalar.return_value = mock_workflow with pytest.raises(WorkflowHashNotEqualError): workflow_service.sync_draft_workflow( diff --git a/api/tests/unit_tests/services/tools/test_builtin_tools_manage_service.py b/api/tests/unit_tests/services/tools/test_builtin_tools_manage_service.py index 439d203c58..175900071b 100644 --- a/api/tests/unit_tests/services/tools/test_builtin_tools_manage_service.py +++ b/api/tests/unit_tests/services/tools/test_builtin_tools_manage_service.py @@ -347,7 +347,7 @@ class TestGetBuiltinToolProviderCredentials: def test_returns_empty_when_no_providers(self, mock_db): mock_db.session.no_autoflush.__enter__ = MagicMock(return_value=None) mock_db.session.no_autoflush.__exit__ = MagicMock(return_value=False) - mock_db.session.query.return_value.filter_by.return_value.order_by.return_value.all.return_value = [] + mock_db.session.scalars.return_value.all.return_value = [] result = BuiltinToolManageService.get_builtin_tool_provider_credentials("t", "google") @@ -362,7 +362,7 @@ class TestGetBuiltinToolProviderCredentials: mock_db.session.no_autoflush.__exit__ = MagicMock(return_value=False) provider = MagicMock(provider="google", is_default=False) - mock_db.session.query.return_value.filter_by.return_value.order_by.return_value.all.return_value = [provider] + mock_db.session.scalars.return_value.all.return_value = [provider] mock_encrypter = MagicMock() mock_encrypter.decrypt.return_value = {"key": "decrypted"} From 2b9eb065552e4bae8ff14b00640d30e2d2257d78 Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Wed, 1 Apr 2026 19:02:53 +0800 Subject: [PATCH 062/199] chore: move commit hook to root (#34404) --- .gitignore | 1 + {web/.husky => .vite-hooks}/pre-commit | 2 +- package.json | 12 +- pnpm-lock.yaml | 155 +------------------------ pnpm-workspace.yaml | 2 - vite.config.ts | 5 + web/Dockerfile | 2 +- web/Dockerfile.dockerignore | 1 - web/package.json | 6 - web/vite.config.ts | 3 + 10 files changed, 22 insertions(+), 167 deletions(-) rename {web/.husky => .vite-hooks}/pre-commit (99%) mode change 100644 => 100755 create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore index d7698fe3fd..f703fc02e9 100644 --- a/.gitignore +++ b/.gitignore @@ -213,6 +213,7 @@ api/.vscode # pnpm /.pnpm-store /node_modules +.vite-hooks/_ # plugin migrate plugins.jsonl diff --git a/web/.husky/pre-commit b/.vite-hooks/pre-commit old mode 100644 new mode 100755 similarity index 99% rename from web/.husky/pre-commit rename to .vite-hooks/pre-commit index 3f25de256f..54e09f80d6 --- a/web/.husky/pre-commit +++ b/.vite-hooks/pre-commit @@ -77,7 +77,7 @@ if $web_modified; then fi cd ./web || exit 1 - lint-staged + vp staged if $web_ts_modified; then echo "Running TypeScript type-check:tsgo" diff --git a/package.json b/package.json index 07f1e16153..48c3acef02 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,15 @@ { "name": "dify", "private": true, + "scripts": { + "prepare": "vp config" + }, + "devDependencies": { + "taze": "catalog:", + "vite-plus": "catalog:" + }, "engines": { "node": "^22.22.1" }, - "packageManager": "pnpm@10.33.0", - "devDependencies": { - "taze": "catalog:" - } + "packageManager": "pnpm@10.33.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb45ea0ef8..baa4ed6c34 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -345,9 +345,6 @@ catalogs: html-to-image: specifier: 1.11.13 version: 1.11.13 - husky: - specifier: 9.1.7 - version: 9.1.7 i18next: specifier: 25.10.10 version: 25.10.10 @@ -390,9 +387,6 @@ catalogs: lexical: specifier: 0.42.0 version: 0.42.0 - lint-staged: - specifier: 16.4.0 - version: 16.4.0 mermaid: specifier: 11.13.0 version: 11.13.0 @@ -624,6 +618,9 @@ importers: taze: specifier: 'catalog:' version: 19.10.0 + vite-plus: + specifier: 'catalog:' + version: 0.1.14(@types/node@25.5.0)(esbuild@0.27.2)(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) e2e: devDependencies: @@ -1165,18 +1162,12 @@ importers: hono: specifier: 'catalog:' version: 4.12.9 - husky: - specifier: 'catalog:' - version: 9.1.7 iconify-import-svg: specifier: 'catalog:' version: 0.1.2 knip: specifier: 'catalog:' version: 6.1.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) - lint-staged: - specifier: 'catalog:' - version: 16.4.0 postcss: specifier: 'catalog:' version: 8.5.8 @@ -4751,10 +4742,6 @@ packages: ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} - ansi-escapes@7.3.0: - resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} - engines: {node: '>=18'} - ansi-regex@4.1.1: resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} engines: {node: '>=6'} @@ -4775,10 +4762,6 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} - ansi-styles@6.2.3: - resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} - engines: {node: '>=12'} - ansis@4.2.0: resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} engines: {node: '>=14'} @@ -5066,18 +5049,10 @@ packages: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} - cli-cursor@5.0.0: - resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} - engines: {node: '>=18'} - cli-table3@0.6.5: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} engines: {node: 10.* || >= 12.*} - cli-truncate@5.2.0: - resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} - engines: {node: '>=20'} - client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -5104,9 +5079,6 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - colorette@2.0.20: - resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - comma-separated-tokens@1.0.8: resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} @@ -5572,10 +5544,6 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} - environment@1.1.0: - resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} - engines: {node: '>=18'} - error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} @@ -5965,9 +5933,6 @@ packages: event-target-bus@1.0.0: resolution: {integrity: sha512-uPcWKbj/BJU3Tbw9XqhHqET4/LBOhvv3/SJWr7NksxA6TC5YqBpaZgawE9R+WpYFCBFSAE4Vun+xQS6w4ABdlA==} - eventemitter3@5.0.4: - resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -6289,11 +6254,6 @@ packages: htmlparser2@10.1.0: resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} - husky@9.1.7: - resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} - engines: {node: '>=18'} - hasBin: true - i18next-resources-to-backend@1.2.1: resolution: {integrity: sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw==} @@ -6419,10 +6379,6 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - is-fullwidth-code-point@5.1.0: - resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} - engines: {node: '>=18'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -6730,15 +6686,6 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - lint-staged@16.4.0: - resolution: {integrity: sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==} - engines: {node: '>=20.17'} - hasBin: true - - listr2@9.0.5: - resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} - engines: {node: '>=20.0.0'} - load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -6770,10 +6717,6 @@ packages: lodash@4.17.23: resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} - log-update@6.1.0: - resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} - engines: {node: '>=18'} - longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -7920,9 +7863,6 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rfdc@1.4.1: - resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} @@ -8043,14 +7983,6 @@ packages: size-sensor@1.0.3: resolution: {integrity: sha512-+k9mJ2/rQMiRmQUcjn+qznch260leIXY8r4FyYKKyRBO/s5UoeMAHGkCJyE1R/4wrIhTJONfyloY55SkE7ve3A==} - slice-ansi@7.1.2: - resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} - engines: {node: '>=18'} - - slice-ansi@8.0.0: - resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} - engines: {node: '>=20'} - smol-toml@1.6.1: resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} engines: {node: '>= 18'} @@ -8134,10 +8066,6 @@ packages: resolution: {integrity: sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==} engines: {node: '>=0.6.19'} - string-argv@0.3.2: - resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} - engines: {node: '>=0.6.19'} - string-ts@2.3.1: resolution: {integrity: sha512-xSJq+BS52SaFFAVxuStmx6n5aYZU571uYUnUrPXkPFCfdHyZMMlbP2v2Wx5sNBnAVzq/2+0+mcBLBa3Xa5ubYw==} @@ -8874,10 +8802,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wrap-ansi@9.0.2: - resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} - engines: {node: '>=18'} - wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -12658,10 +12582,6 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - ansi-escapes@7.3.0: - dependencies: - environment: 1.1.0 - ansi-regex@4.1.1: {} ansi-regex@5.0.1: {} @@ -12674,8 +12594,6 @@ snapshots: ansi-styles@5.2.0: {} - ansi-styles@6.2.3: {} - ansis@4.2.0: {} any-promise@1.3.0: {} @@ -12947,21 +12865,12 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 - cli-cursor@5.0.0: - dependencies: - restore-cursor: 5.1.0 - cli-table3@0.6.5: dependencies: string-width: 8.2.0 optionalDependencies: '@colors/colors': 1.5.0 - cli-truncate@5.2.0: - dependencies: - slice-ansi: 8.0.0 - string-width: 8.2.0 - client-only@0.0.1: {} clsx@2.1.1: {} @@ -12998,8 +12907,6 @@ snapshots: color-name@1.1.4: {} - colorette@2.0.20: {} - comma-separated-tokens@1.0.8: {} comma-separated-tokens@2.0.3: {} @@ -13443,8 +13350,6 @@ snapshots: entities@7.0.1: {} - environment@1.1.0: {} - error-stack-parser-es@1.0.5: {} error-stack-parser@2.1.4: @@ -14106,8 +14011,6 @@ snapshots: event-target-bus@1.0.0: {} - eventemitter3@5.0.4: {} - events@3.3.0: {} expand-template@2.0.3: @@ -14496,8 +14399,6 @@ snapshots: domutils: 3.2.2 entities: 7.0.1 - husky@9.1.7: {} - i18next-resources-to-backend@1.2.1: dependencies: '@babel/runtime': 7.29.2 @@ -14596,10 +14497,6 @@ snapshots: is-extglob@2.1.1: {} - is-fullwidth-code-point@5.1.0: - dependencies: - get-east-asian-width: 1.5.0 - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -14853,24 +14750,6 @@ snapshots: lines-and-columns@1.2.4: {} - lint-staged@16.4.0: - dependencies: - commander: 14.0.3 - listr2: 9.0.5 - picomatch: 4.0.4 - string-argv: 0.3.2 - tinyexec: 1.0.4 - yaml: 2.8.3 - - listr2@9.0.5: - dependencies: - cli-truncate: 5.2.0 - colorette: 2.0.20 - eventemitter3: 5.0.4 - log-update: 6.1.0 - rfdc: 1.4.1 - wrap-ansi: 9.0.2 - load-tsconfig@0.2.5: {} loader-runner@4.3.1: {} @@ -14895,14 +14774,6 @@ snapshots: lodash@4.17.23: {} - log-update@6.1.0: - dependencies: - ansi-escapes: 7.3.0 - cli-cursor: 5.0.0 - slice-ansi: 7.1.2 - strip-ansi: 7.2.0 - wrap-ansi: 9.0.2 - longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -16539,8 +16410,6 @@ snapshots: reusify@1.1.0: {} - rfdc@1.4.1: {} - robust-predicates@3.0.3: {} rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): @@ -16734,16 +16603,6 @@ snapshots: size-sensor@1.0.3: {} - slice-ansi@7.1.2: - dependencies: - ansi-styles: 6.2.3 - is-fullwidth-code-point: 5.1.0 - - slice-ansi@8.0.0: - dependencies: - ansi-styles: 6.2.3 - is-fullwidth-code-point: 5.1.0 - smol-toml@1.6.1: {} solid-js@1.9.11: @@ -16844,8 +16703,6 @@ snapshots: string-argv@0.3.1: {} - string-argv@0.3.2: {} - string-ts@2.3.1: {} string-width@8.2.0: @@ -17690,12 +17547,6 @@ snapshots: word-wrap@1.2.5: {} - wrap-ansi@9.0.2: - dependencies: - ansi-styles: 6.2.3 - string-width: 8.2.0 - strip-ansi: 7.2.0 - wrappy@1.0.2: {} ws@8.20.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b11cca6642..77451f6dfc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -183,7 +183,6 @@ catalog: hono: 4.12.9 html-entities: 2.6.0 html-to-image: 1.11.13 - husky: 9.1.7 i18next: 25.10.10 i18next-resources-to-backend: 1.2.1 iconify-import-svg: 0.1.2 @@ -198,7 +197,6 @@ catalog: ky: 1.14.3 lamejs: 1.2.1 lexical: 0.42.0 - lint-staged: 16.4.0 mermaid: 11.13.0 mime: 4.1.0 mitt: 3.0.1 diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000000..a34932a4ef --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from 'vite-plus' + +export default defineConfig({ + staged: {}, +}) diff --git a/web/Dockerfile b/web/Dockerfile index 75024db4f3..dc23416842 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -31,7 +31,7 @@ RUN corepack install # Install only the web workspace to keep image builds from pulling in # unrelated workspace dependencies such as e2e tooling. -RUN pnpm install --filter ./web... --frozen-lockfile +RUN VITE_GIT_HOOKS=0 pnpm install --filter ./web... --frozen-lockfile # build resources FROM base AS builder diff --git a/web/Dockerfile.dockerignore b/web/Dockerfile.dockerignore index 9801003d89..b572bd863e 100644 --- a/web/Dockerfile.dockerignore +++ b/web/Dockerfile.dockerignore @@ -22,7 +22,6 @@ web/node_modules web/dist web/build web/coverage -web/.husky web/.next web/.pnpm-store web/.vscode diff --git a/web/package.json b/web/package.json index 9ed21fdb22..08c10b12ad 100644 --- a/web/package.json +++ b/web/package.json @@ -40,7 +40,6 @@ "lint:quiet": "vp run lint --quiet", "lint:tss": "tsslint --project tsconfig.json", "preinstall": "npx only-allow pnpm", - "prepare": "cd ../ && node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || husky ./web/.husky", "refactor-component": "node ./scripts/refactor-component.js", "start": "node ./scripts/copy-and-start.mjs", "start:vinext": "vinext start", @@ -218,10 +217,8 @@ "eslint-plugin-storybook": "catalog:", "happy-dom": "catalog:", "hono": "catalog:", - "husky": "catalog:", "iconify-import-svg": "catalog:", "knip": "catalog:", - "lint-staged": "catalog:", "postcss": "catalog:", "postcss-js": "catalog:", "react-server-dom-webpack": "catalog:", @@ -237,8 +234,5 @@ "vite-plus": "catalog:", "vitest": "catalog:", "vitest-canvas-mock": "catalog:" - }, - "lint-staged": { - "*": "eslint --fix --pass-on-unpruned-suppressions" } } diff --git a/web/vite.config.ts b/web/vite.config.ts index 28746f81ca..92762676d1 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -18,6 +18,9 @@ export default defineConfig(({ mode }) => { || process.argv.some(arg => arg.toLowerCase().includes('storybook')) return { + staged: { + '*': 'eslint --fix --pass-on-unpruned-suppressions', + }, plugins: isTest ? [ nextStaticImageTestPlugin({ projectRoot }), From e41965061ceb78bb574d1eac60fa18fb7b3e0e98 Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Wed, 1 Apr 2026 21:15:36 +0800 Subject: [PATCH 063/199] =?UTF-8?q?fix:=20sqlalchemy.exc.InvalidRequestErr?= =?UTF-8?q?or:=20Can't=20operate=20on=20closed=20tran=E2=80=A6=20(#34407)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rag_pipeline/rag_pipeline_workflow.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py index 4251e7ebac..70dfe47d7f 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py @@ -593,17 +593,15 @@ class PublishedRagPipelineApi(Resource): # The role of the current user in the ta table must be admin, owner, or editor current_user, _ = current_account_with_tenant() rag_pipeline_service = RagPipelineService() - with sessionmaker(db.engine).begin() as session: - pipeline = session.merge(pipeline) - workflow = rag_pipeline_service.publish_workflow( - session=session, - pipeline=pipeline, - account=current_user, - ) - pipeline.is_published = True - pipeline.workflow_id = workflow.id - session.add(pipeline) - workflow_created_at = TimestampField().format(workflow.created_at) + workflow = rag_pipeline_service.publish_workflow( + session=db.session, # type: ignore[reportArgumentType,arg-type] + pipeline=pipeline, + account=current_user, + ) + pipeline.is_published = True + pipeline.workflow_id = workflow.id + db.session.commit() + workflow_created_at = TimestampField().format(workflow.created_at) return { "result": "success", From 391007d02e60e69962df2cc0ebcbac740cd1a1de Mon Sep 17 00:00:00 2001 From: Tim Ren <137012659+xr843@users.noreply.github.com> Date: Wed, 1 Apr 2026 22:53:41 +0800 Subject: [PATCH 064/199] refactor: migrate service_api and inner_api to sessionmaker pattern (#34379) Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/controllers/inner_api/plugin/wraps.py | 6 ++-- .../service_api/app/conversation.py | 4 +-- api/controllers/service_api/app/workflow.py | 4 +-- .../inner_api/plugin/test_plugin_wraps.py | 33 +++++++++---------- .../service_api/app/test_conversation.py | 11 +++++-- .../service_api/app/test_workflow.py | 22 +++++++++---- 6 files changed, 47 insertions(+), 33 deletions(-) diff --git a/api/controllers/inner_api/plugin/wraps.py b/api/controllers/inner_api/plugin/wraps.py index d6e3ebfbcd..ed0d490aad 100644 --- a/api/controllers/inner_api/plugin/wraps.py +++ b/api/controllers/inner_api/plugin/wraps.py @@ -6,7 +6,7 @@ from flask import current_app, request from flask_login import user_logged_in from pydantic import BaseModel from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from extensions.ext_database import db from libs.login import current_user @@ -33,7 +33,7 @@ def get_user(tenant_id: str, user_id: str | None) -> EndUser: user_id = DefaultEndUserSessionID.DEFAULT_SESSION_ID is_anonymous = user_id == DefaultEndUserSessionID.DEFAULT_SESSION_ID try: - with Session(db.engine) as session: + with sessionmaker(db.engine, expire_on_commit=False).begin() as session: user_model = None if is_anonymous: @@ -56,7 +56,7 @@ def get_user(tenant_id: str, user_id: str | None) -> EndUser: session_id=user_id, ) session.add(user_model) - session.commit() + session.flush() session.refresh(user_model) except Exception: diff --git a/api/controllers/service_api/app/conversation.py b/api/controllers/service_api/app/conversation.py index edbf011656..8c9a3eb5e9 100644 --- a/api/controllers/service_api/app/conversation.py +++ b/api/controllers/service_api/app/conversation.py @@ -3,7 +3,7 @@ from typing import Any, Literal from flask import request from flask_restx import Resource from pydantic import BaseModel, Field, TypeAdapter, field_validator, model_validator -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, NotFound import services @@ -116,7 +116,7 @@ class ConversationApi(Resource): last_id = str(query_args.last_id) if query_args.last_id else None try: - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: pagination = ConversationService.pagination_by_last_id( session=session, app_model=app_model, diff --git a/api/controllers/service_api/app/workflow.py b/api/controllers/service_api/app/workflow.py index 1759075139..d7992a2a3a 100644 --- a/api/controllers/service_api/app/workflow.py +++ b/api/controllers/service_api/app/workflow.py @@ -8,7 +8,7 @@ from graphon.enums import WorkflowExecutionStatus from graphon.graph_engine.manager import GraphEngineManager from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel, Field -from sqlalchemy.orm import Session, sessionmaker +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, InternalServerError, NotFound from controllers.common.schema import register_schema_models @@ -314,7 +314,7 @@ class WorkflowAppLogApi(Resource): # get paginate workflow app logs workflow_app_service = WorkflowAppService() - with Session(db.engine) as session: + with sessionmaker(db.engine).begin() as session: workflow_app_log_pagination = workflow_app_service.get_paginate_workflow_app_logs( session=session, app_model=app_model, diff --git a/api/tests/unit_tests/controllers/inner_api/plugin/test_plugin_wraps.py b/api/tests/unit_tests/controllers/inner_api/plugin/test_plugin_wraps.py index eac57fe4b7..957d7fbd9b 100644 --- a/api/tests/unit_tests/controllers/inner_api/plugin/test_plugin_wraps.py +++ b/api/tests/unit_tests/controllers/inner_api/plugin/test_plugin_wraps.py @@ -41,15 +41,15 @@ class TestGetUser: """Test get_user function""" @patch("controllers.inner_api.plugin.wraps.EndUser") - @patch("controllers.inner_api.plugin.wraps.Session") + @patch("controllers.inner_api.plugin.wraps.sessionmaker") @patch("controllers.inner_api.plugin.wraps.db") - def test_should_return_existing_user_by_id(self, mock_db, mock_session_class, mock_enduser_class, app: Flask): + def test_should_return_existing_user_by_id(self, mock_db, mock_sessionmaker, mock_enduser_class, app: Flask): """Test returning existing user when found by ID""" # Arrange mock_user = MagicMock() mock_user.id = "user123" mock_session = MagicMock() - mock_session_class.return_value.__enter__.return_value = mock_session + mock_sessionmaker.return_value.begin.return_value.__enter__.return_value = mock_session mock_session.get.return_value = mock_user # Act @@ -61,17 +61,17 @@ class TestGetUser: mock_session.get.assert_called_once() @patch("controllers.inner_api.plugin.wraps.EndUser") - @patch("controllers.inner_api.plugin.wraps.Session") + @patch("controllers.inner_api.plugin.wraps.sessionmaker") @patch("controllers.inner_api.plugin.wraps.db") def test_should_return_existing_anonymous_user_by_session_id( - self, mock_db, mock_session_class, mock_enduser_class, app: Flask + self, mock_db, mock_sessionmaker, mock_enduser_class, app: Flask ): """Test returning existing anonymous user by session_id""" # Arrange mock_user = MagicMock() mock_user.session_id = "anonymous_session" mock_session = MagicMock() - mock_session_class.return_value.__enter__.return_value = mock_session + mock_sessionmaker.return_value.begin.return_value.__enter__.return_value = mock_session # non-anonymous path uses session.get(); anonymous uses session.scalar() mock_session.get.return_value = mock_user @@ -83,13 +83,13 @@ class TestGetUser: assert result == mock_user @patch("controllers.inner_api.plugin.wraps.EndUser") - @patch("controllers.inner_api.plugin.wraps.Session") + @patch("controllers.inner_api.plugin.wraps.sessionmaker") @patch("controllers.inner_api.plugin.wraps.db") - def test_should_create_new_user_when_not_found(self, mock_db, mock_session_class, mock_enduser_class, app: Flask): + def test_should_create_new_user_when_not_found(self, mock_db, mock_sessionmaker, mock_enduser_class, app: Flask): """Test creating new user when not found in database""" # Arrange mock_session = MagicMock() - mock_session_class.return_value.__enter__.return_value = mock_session + mock_sessionmaker.return_value.begin.return_value.__enter__.return_value = mock_session mock_session.get.return_value = None mock_new_user = MagicMock() mock_enduser_class.return_value = mock_new_user @@ -101,21 +101,20 @@ class TestGetUser: # Assert assert result == mock_new_user mock_session.add.assert_called_once() - mock_session.commit.assert_called_once() mock_session.refresh.assert_called_once() @patch("controllers.inner_api.plugin.wraps.select") @patch("controllers.inner_api.plugin.wraps.EndUser") - @patch("controllers.inner_api.plugin.wraps.Session") + @patch("controllers.inner_api.plugin.wraps.sessionmaker") @patch("controllers.inner_api.plugin.wraps.db") def test_should_use_default_session_id_when_user_id_none( - self, mock_db, mock_session_class, mock_enduser_class, mock_select, app: Flask + self, mock_db, mock_sessionmaker, mock_enduser_class, mock_select, app: Flask ): """Test using default session ID when user_id is None""" # Arrange mock_user = MagicMock() mock_session = MagicMock() - mock_session_class.return_value.__enter__.return_value = mock_session + mock_sessionmaker.return_value.begin.return_value.__enter__.return_value = mock_session # When user_id is None, is_anonymous=True, so session.scalar() is used mock_session.scalar.return_value = mock_user @@ -127,15 +126,13 @@ class TestGetUser: assert result == mock_user @patch("controllers.inner_api.plugin.wraps.EndUser") - @patch("controllers.inner_api.plugin.wraps.Session") + @patch("controllers.inner_api.plugin.wraps.sessionmaker") @patch("controllers.inner_api.plugin.wraps.db") - def test_should_raise_error_on_database_exception( - self, mock_db, mock_session_class, mock_enduser_class, app: Flask - ): + def test_should_raise_error_on_database_exception(self, mock_db, mock_sessionmaker, mock_enduser_class, app: Flask): """Test raising ValueError when database operation fails""" # Arrange mock_session = MagicMock() - mock_session_class.return_value.__enter__.return_value = mock_session + mock_sessionmaker.return_value.begin.return_value.__enter__.return_value = mock_session mock_session.get.side_effect = Exception("Database error") # Act & Assert diff --git a/api/tests/unit_tests/controllers/service_api/app/test_conversation.py b/api/tests/unit_tests/controllers/service_api/app/test_conversation.py index 81c45dcdb7..dbd06677d8 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_conversation.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_conversation.py @@ -433,13 +433,20 @@ class TestConversationApiController: handler(api, app_model=app_model, end_user=end_user) def test_list_last_not_found(self, app, monkeypatch: pytest.MonkeyPatch) -> None: - class _SessionStub: + class _BeginStub: def __enter__(self): return SimpleNamespace() def __exit__(self, exc_type, exc, tb): return False + class _SessionMakerStub: + def __init__(self, *args, **kwargs): + pass + + def begin(self): + return _BeginStub() + monkeypatch.setattr( ConversationService, "pagination_by_last_id", @@ -447,7 +454,7 @@ class TestConversationApiController: ) conversation_module = sys.modules["controllers.service_api.app.conversation"] monkeypatch.setattr(conversation_module, "db", SimpleNamespace(engine=object())) - monkeypatch.setattr(conversation_module, "Session", lambda *_args, **_kwargs: _SessionStub()) + monkeypatch.setattr(conversation_module, "sessionmaker", _SessionMakerStub) api = ConversationApi() handler = _unwrap(api.get) diff --git a/api/tests/unit_tests/controllers/service_api/app/test_workflow.py b/api/tests/unit_tests/controllers/service_api/app/test_workflow.py index b1f036c6f3..cfa21bf2dd 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_workflow.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_workflow.py @@ -470,16 +470,23 @@ class TestWorkflowTaskStopApi: class TestWorkflowAppLogApi: def test_success(self, app, monkeypatch: pytest.MonkeyPatch) -> None: - class _SessionStub: + class _BeginStub: def __enter__(self): return SimpleNamespace() def __exit__(self, exc_type, exc, tb): return False + class _SessionMakerStub: + def __init__(self, *args, **kwargs): + pass + + def begin(self): + return _BeginStub() + workflow_module = sys.modules["controllers.service_api.app.workflow"] monkeypatch.setattr(workflow_module, "db", SimpleNamespace(engine=object())) - monkeypatch.setattr(workflow_module, "Session", lambda *_args, **_kwargs: _SessionStub()) + monkeypatch.setattr(workflow_module, "sessionmaker", _SessionMakerStub) monkeypatch.setattr( WorkflowAppService, "get_paginate_workflow_app_logs", @@ -635,11 +642,14 @@ class TestWorkflowAppLogApiGet: mock_svc_instance.get_paginate_workflow_app_logs.return_value = mock_pagination mock_wf_svc_cls.return_value = mock_svc_instance - # Mock Session context manager + # Mock sessionmaker(...).begin() context manager mock_session = Mock() mock_db.engine = Mock() - mock_session.__enter__ = Mock(return_value=mock_session) - mock_session.__exit__ = Mock(return_value=False) + mock_begin = Mock() + mock_begin.__enter__ = Mock(return_value=mock_session) + mock_begin.__exit__ = Mock(return_value=False) + mock_session_factory = Mock() + mock_session_factory.begin.return_value = mock_begin from controllers.service_api.app.workflow import WorkflowAppLogApi @@ -647,7 +657,7 @@ class TestWorkflowAppLogApiGet: "/workflows/logs?page=1&limit=20", method="GET", ): - with patch("controllers.service_api.app.workflow.Session", return_value=mock_session): + with patch("controllers.service_api.app.workflow.sessionmaker", return_value=mock_session_factory): api = WorkflowAppLogApi() result = _unwrap(api.get)(api, app_model=mock_workflow_app) From 4e1d0604391e2df11c6df7b3864b9121e9304fe8 Mon Sep 17 00:00:00 2001 From: Renzo <170978465+RenzoMXD@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:37:27 +0200 Subject: [PATCH 065/199] refactor: select in message_service and ops_service (#34414) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/services/message_service.py | 57 ++++--- api/services/ops_service.py | 32 ++-- .../services/test_message_service.py | 147 +++--------------- .../unit_tests/services/test_ops_service.py | 53 ++++--- 4 files changed, 94 insertions(+), 195 deletions(-) diff --git a/api/services/message_service.py b/api/services/message_service.py index a04f9cbe01..5c2978db21 100644 --- a/api/services/message_service.py +++ b/api/services/message_service.py @@ -3,6 +3,7 @@ from typing import Union from graphon.model_runtime.entities.model_entities import ModelType from pydantic import TypeAdapter +from sqlalchemy import select from sqlalchemy.orm import sessionmaker from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfigManager @@ -75,17 +76,15 @@ class MessageService: fetch_limit = limit + 1 if first_id: - first_message = ( - db.session.query(Message) - .where(Message.conversation_id == conversation.id, Message.id == first_id) - .first() + first_message = db.session.scalar( + select(Message).where(Message.conversation_id == conversation.id, Message.id == first_id).limit(1) ) if not first_message: raise FirstMessageNotExistsError() - history_messages = ( - db.session.query(Message) + history_messages = db.session.scalars( + select(Message) .where( Message.conversation_id == conversation.id, Message.created_at < first_message.created_at, @@ -93,16 +92,14 @@ class MessageService: ) .order_by(Message.created_at.desc()) .limit(fetch_limit) - .all() - ) + ).all() else: - history_messages = ( - db.session.query(Message) + history_messages = db.session.scalars( + select(Message) .where(Message.conversation_id == conversation.id) .order_by(Message.created_at.desc()) .limit(fetch_limit) - .all() - ) + ).all() has_more = False if len(history_messages) > limit: @@ -129,7 +126,7 @@ class MessageService: if not user: return InfiniteScrollPagination(data=[], limit=limit, has_more=False) - base_query = db.session.query(Message) + stmt = select(Message) fetch_limit = limit + 1 @@ -138,28 +135,27 @@ class MessageService: app_model=app_model, user=user, conversation_id=conversation_id ) - base_query = base_query.where(Message.conversation_id == conversation.id) + stmt = stmt.where(Message.conversation_id == conversation.id) # Check if include_ids is not None and not empty to avoid WHERE false condition if include_ids is not None: if len(include_ids) == 0: return InfiniteScrollPagination(data=[], limit=limit, has_more=False) - base_query = base_query.where(Message.id.in_(include_ids)) + stmt = stmt.where(Message.id.in_(include_ids)) if last_id: - last_message = base_query.where(Message.id == last_id).first() + last_message = db.session.scalar(stmt.where(Message.id == last_id).limit(1)) if not last_message: raise LastMessageNotExistsError() - history_messages = ( - base_query.where(Message.created_at < last_message.created_at, Message.id != last_message.id) + history_messages = db.session.scalars( + stmt.where(Message.created_at < last_message.created_at, Message.id != last_message.id) .order_by(Message.created_at.desc()) .limit(fetch_limit) - .all() - ) + ).all() else: - history_messages = base_query.order_by(Message.created_at.desc()).limit(fetch_limit).all() + history_messages = db.session.scalars(stmt.order_by(Message.created_at.desc()).limit(fetch_limit)).all() has_more = False if len(history_messages) > limit: @@ -214,21 +210,20 @@ class MessageService: def get_all_messages_feedbacks(cls, app_model: App, page: int, limit: int): """Get all feedbacks of an app""" offset = (page - 1) * limit - feedbacks = ( - db.session.query(MessageFeedback) + feedbacks = db.session.scalars( + select(MessageFeedback) .where(MessageFeedback.app_id == app_model.id) .order_by(MessageFeedback.created_at.desc(), MessageFeedback.id.desc()) .limit(limit) .offset(offset) - .all() - ) + ).all() return [record.to_dict() for record in feedbacks] @classmethod def get_message(cls, app_model: App, user: Union[Account, EndUser] | None, message_id: str): - message = ( - db.session.query(Message) + message = db.session.scalar( + select(Message) .where( Message.id == message_id, Message.app_id == app_model.id, @@ -236,7 +231,7 @@ class MessageService: Message.from_end_user_id == (user.id if isinstance(user, EndUser) else None), Message.from_account_id == (user.id if isinstance(user, Account) else None), ) - .first() + .limit(1) ) if not message: @@ -282,10 +277,10 @@ class MessageService: ) else: if not conversation.override_model_configs: - app_model_config = ( - db.session.query(AppModelConfig) + app_model_config = db.session.scalar( + select(AppModelConfig) .where(AppModelConfig.id == conversation.app_model_config_id, AppModelConfig.app_id == app_model.id) - .first() + .limit(1) ) else: conversation_override_model_configs = _app_model_config_adapter.validate_json( diff --git a/api/services/ops_service.py b/api/services/ops_service.py index 50ea832085..2a64088dd6 100644 --- a/api/services/ops_service.py +++ b/api/services/ops_service.py @@ -1,5 +1,7 @@ from typing import Any +from sqlalchemy import select + from core.ops.entities.config_entity import BaseTracingConfig from core.ops.ops_trace_manager import OpsTraceManager, provider_config_map from extensions.ext_database import db @@ -15,17 +17,17 @@ class OpsService: :param tracing_provider: tracing provider :return: """ - trace_config_data: TraceAppConfig | None = ( - db.session.query(TraceAppConfig) + trace_config_data: TraceAppConfig | None = db.session.scalar( + select(TraceAppConfig) .where(TraceAppConfig.app_id == app_id, TraceAppConfig.tracing_provider == tracing_provider) - .first() + .limit(1) ) if not trace_config_data: return None # decrypt_token and obfuscated_token - app = db.session.query(App).where(App.id == app_id).first() + app = db.session.get(App, app_id) if not app: return None tenant_id = app.tenant_id @@ -182,17 +184,17 @@ class OpsService: project_url = None # check if trace config already exists - trace_config_data: TraceAppConfig | None = ( - db.session.query(TraceAppConfig) + trace_config_data: TraceAppConfig | None = db.session.scalar( + select(TraceAppConfig) .where(TraceAppConfig.app_id == app_id, TraceAppConfig.tracing_provider == tracing_provider) - .first() + .limit(1) ) if trace_config_data: return None # get tenant id - app = db.session.query(App).where(App.id == app_id).first() + app = db.session.get(App, app_id) if not app: return None tenant_id = app.tenant_id @@ -224,17 +226,17 @@ class OpsService: raise ValueError(f"Invalid tracing provider: {tracing_provider}") # check if trace config already exists - current_trace_config = ( - db.session.query(TraceAppConfig) + current_trace_config = db.session.scalar( + select(TraceAppConfig) .where(TraceAppConfig.app_id == app_id, TraceAppConfig.tracing_provider == tracing_provider) - .first() + .limit(1) ) if not current_trace_config: return None # get tenant id - app = db.session.query(App).where(App.id == app_id).first() + app = db.session.get(App, app_id) if not app: return None tenant_id = app.tenant_id @@ -261,10 +263,10 @@ class OpsService: :param tracing_provider: tracing provider :return: """ - trace_config = ( - db.session.query(TraceAppConfig) + trace_config = db.session.scalar( + select(TraceAppConfig) .where(TraceAppConfig.app_id == app_id, TraceAppConfig.tracing_provider == tracing_provider) - .first() + .limit(1) ) if not trace_config: diff --git a/api/tests/unit_tests/services/test_message_service.py b/api/tests/unit_tests/services/test_message_service.py index 101b9bff24..b6e990ebe0 100644 --- a/api/tests/unit_tests/services/test_message_service.py +++ b/api/tests/unit_tests/services/test_message_service.py @@ -151,12 +151,7 @@ class TestMessageServicePaginationByFirstId: for i in range(5) ] - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.where.return_value = mock_query - mock_query.order_by.return_value = mock_query - mock_query.limit.return_value = mock_query - mock_query.all.return_value = messages + mock_db.session.scalars.return_value.all.return_value = messages # Act result = MessageService.pagination_by_first_id( @@ -196,12 +191,7 @@ class TestMessageServicePaginationByFirstId: for i in range(5) ] - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.where.return_value = mock_query - mock_query.order_by.return_value = mock_query - mock_query.limit.return_value = mock_query - mock_query.all.return_value = messages + mock_db.session.scalars.return_value.all.return_value = messages # Act result = MessageService.pagination_by_first_id( @@ -246,31 +236,8 @@ class TestMessageServicePaginationByFirstId: for i in range(5) ] - # Setup query mocks - mock_query_first = MagicMock() - mock_query_history = MagicMock() - - query_calls = [] - - def query_side_effect(*args): - if args[0] == Message: - query_calls.append(args) - if len(query_calls) == 1: - return mock_query_first - else: - return mock_query_history - - mock_db.session.query.side_effect = [mock_query_first, mock_query_history] - - # Setup first message query - mock_query_first.where.return_value = mock_query_first - mock_query_first.first.return_value = first_message - - # Setup history messages query - mock_query_history.where.return_value = mock_query_history - mock_query_history.order_by.return_value = mock_query_history - mock_query_history.limit.return_value = mock_query_history - mock_query_history.all.return_value = history_messages + mock_db.session.scalar.return_value = first_message + mock_db.session.scalars.return_value.all.return_value = history_messages # Act result = MessageService.pagination_by_first_id( @@ -285,8 +252,6 @@ class TestMessageServicePaginationByFirstId: # Assert assert len(result.data) == 5 assert result.has_more is False - mock_query_first.where.assert_called_once() - mock_query_history.where.assert_called_once() # Test 06: First message not found @patch("services.message_service.db") @@ -300,10 +265,7 @@ class TestMessageServicePaginationByFirstId: mock_conversation_service.get_conversation.return_value = conversation - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.where.return_value = mock_query - mock_query.first.return_value = None # Message not found + mock_db.session.scalar.return_value = None # Message not found # Act & Assert with pytest.raises(FirstMessageNotExistsError): @@ -336,12 +298,7 @@ class TestMessageServicePaginationByFirstId: for i in range(11) ] - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.where.return_value = mock_query - mock_query.order_by.return_value = mock_query - mock_query.limit.return_value = mock_query - mock_query.all.return_value = messages + mock_db.session.scalars.return_value.all.return_value = messages # Act result = MessageService.pagination_by_first_id( @@ -369,12 +326,7 @@ class TestMessageServicePaginationByFirstId: mock_conversation_service.get_conversation.return_value = conversation - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.where.return_value = mock_query - mock_query.order_by.return_value = mock_query - mock_query.limit.return_value = mock_query - mock_query.all.return_value = [] + mock_db.session.scalars.return_value.all.return_value = [] # Act result = MessageService.pagination_by_first_id( @@ -443,12 +395,7 @@ class TestMessageServicePaginationByLastId: for i in range(5) ] - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.where.return_value = mock_query - mock_query.order_by.return_value = mock_query - mock_query.limit.return_value = mock_query - mock_query.all.return_value = messages + mock_db.session.scalars.return_value.all.return_value = messages # Act result = MessageService.pagination_by_last_id( @@ -485,22 +432,8 @@ class TestMessageServicePaginationByLastId: for i in range(6, 10) ] - # Setup base query mock that returns itself for chaining - mock_base_query = MagicMock() - mock_db.session.query.return_value = mock_base_query - - # First where() call for last_id lookup - mock_query_last = MagicMock() - mock_query_last.first.return_value = last_message - - # Second where() call for history messages - mock_query_history = MagicMock() - mock_query_history.order_by.return_value = mock_query_history - mock_query_history.limit.return_value = mock_query_history - mock_query_history.all.return_value = new_messages - - # Setup where() to return different mocks on consecutive calls - mock_base_query.where.side_effect = [mock_query_last, mock_query_history] + mock_db.session.scalar.return_value = last_message + mock_db.session.scalars.return_value.all.return_value = new_messages # Act result = MessageService.pagination_by_last_id( @@ -522,10 +455,7 @@ class TestMessageServicePaginationByLastId: app = factory.create_app_mock() user = factory.create_end_user_mock() - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.where.return_value = mock_query - mock_query.first.return_value = None # Message not found + mock_db.session.scalar.return_value = None # Message not found # Act & Assert with pytest.raises(LastMessageNotExistsError): @@ -557,12 +487,7 @@ class TestMessageServicePaginationByLastId: for i in range(5) ] - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.where.return_value = mock_query - mock_query.order_by.return_value = mock_query - mock_query.limit.return_value = mock_query - mock_query.all.return_value = messages + mock_db.session.scalars.return_value.all.return_value = messages # Act result = MessageService.pagination_by_last_id( @@ -576,8 +501,6 @@ class TestMessageServicePaginationByLastId: # Assert assert len(result.data) == 5 assert result.has_more is False - # Verify conversation_id was used in query - mock_query.where.assert_called() mock_conversation_service.get_conversation.assert_called_once() # Test 14: Pagination with include_ids filter @@ -594,12 +517,7 @@ class TestMessageServicePaginationByLastId: factory.create_message_mock(message_id="msg-003"), ] - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.where.return_value = mock_query - mock_query.order_by.return_value = mock_query - mock_query.limit.return_value = mock_query - mock_query.all.return_value = messages + mock_db.session.scalars.return_value.all.return_value = messages # Act result = MessageService.pagination_by_last_id( @@ -632,12 +550,7 @@ class TestMessageServicePaginationByLastId: for i in range(11) ] - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.where.return_value = mock_query - mock_query.order_by.return_value = mock_query - mock_query.limit.return_value = mock_query - mock_query.all.return_value = messages + mock_db.session.scalars.return_value.all.return_value = messages # Act result = MessageService.pagination_by_last_id( @@ -743,17 +656,13 @@ class TestMessageServiceGetMessage: user = factory.create_end_user_mock(user_id="end-user-123") message = factory.create_message_mock() - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.where.return_value = mock_query - mock_query.first.return_value = message + mock_db.session.scalar.return_value = message # Act result = MessageService.get_message(app_model=app, user=user, message_id="msg-123") # Assert assert result == message - mock_query.where.assert_called_once() # Test 21: get_message success for Account (Admin) @patch("services.message_service.db") @@ -767,10 +676,7 @@ class TestMessageServiceGetMessage: user.id = "account-123" message = factory.create_message_mock() - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.where.return_value = mock_query - mock_query.first.return_value = message + mock_db.session.scalar.return_value = message # Act result = MessageService.get_message(app_model=app, user=user, message_id="msg-123") @@ -786,10 +692,7 @@ class TestMessageServiceGetMessage: app = factory.create_app_mock() user = factory.create_end_user_mock() - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.where.return_value = mock_query - mock_query.first.return_value = None + mock_db.session.scalar.return_value = None # Act & Assert with pytest.raises(MessageNotExistsError): @@ -899,21 +802,13 @@ class TestMessageServiceFeedback: feedback = MagicMock() feedback.to_dict.return_value = {"id": "fb-1"} - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.where.return_value = mock_query - mock_query.order_by.return_value = mock_query - mock_query.limit.return_value = mock_query - mock_query.offset.return_value = mock_query - mock_query.all.return_value = [feedback] + mock_db.session.scalars.return_value.all.return_value = [feedback] # Act result = MessageService.get_all_messages_feedbacks(app_model=app, page=1, limit=10) # Assert assert result == [{"id": "fb-1"}] - mock_query.limit.assert_called_with(10) - mock_query.offset.assert_called_with(0) class TestMessageServiceSuggestedQuestions: @@ -1015,10 +910,7 @@ class TestMessageServiceSuggestedQuestions: app_model_config.suggested_questions_after_answer_dict = {"enabled": True} app_model_config.model_dict = {"provider": "openai", "name": "gpt-4"} - mock_query = MagicMock() - mock_db.session.query.return_value = mock_query - mock_query.where.return_value = mock_query - mock_query.first.return_value = app_model_config + mock_db.session.scalar.return_value = app_model_config mock_llm_gen.generate_suggested_questions_after_answer.return_value = ["Q1?"] @@ -1029,7 +921,6 @@ class TestMessageServiceSuggestedQuestions: # Assert assert result == ["Q1?"] - mock_query.first.assert_called_once() mock_llm_gen.generate_suggested_questions_after_answer.assert_called_once() # Test 30: get_suggested_questions_after_answer - Disabled Error diff --git a/api/tests/unit_tests/services/test_ops_service.py b/api/tests/unit_tests/services/test_ops_service.py index ab7b473790..7067e3b3dd 100644 --- a/api/tests/unit_tests/services/test_ops_service.py +++ b/api/tests/unit_tests/services/test_ops_service.py @@ -12,28 +12,27 @@ class TestOpsService: @patch("services.ops_service.OpsTraceManager") def test_get_tracing_app_config_no_config(self, mock_ops_trace_manager, mock_db): # Arrange - mock_db.session.query.return_value.where.return_value.first.return_value = None + mock_db.session.scalar.return_value = None # Act result = OpsService.get_tracing_app_config("app_id", "arize") # Assert assert result is None - mock_db.session.query.assert_called_with(TraceAppConfig) @patch("services.ops_service.db") @patch("services.ops_service.OpsTraceManager") def test_get_tracing_app_config_no_app(self, mock_ops_trace_manager, mock_db): # Arrange trace_config = MagicMock(spec=TraceAppConfig) - mock_db.session.query.return_value.where.return_value.first.side_effect = [trace_config, None] + mock_db.session.scalar.return_value = trace_config + mock_db.session.get.return_value = None # Act result = OpsService.get_tracing_app_config("app_id", "arize") # Assert assert result is None - assert mock_db.session.query.call_count == 2 @patch("services.ops_service.db") @patch("services.ops_service.OpsTraceManager") @@ -43,7 +42,8 @@ class TestOpsService: trace_config.tracing_config = None app = MagicMock(spec=App) app.tenant_id = "tenant_id" - mock_db.session.query.return_value.where.return_value.first.side_effect = [trace_config, app] + mock_db.session.scalar.return_value = trace_config + mock_db.session.get.return_value = app # Act & Assert with pytest.raises(ValueError, match="Tracing config cannot be None."): @@ -72,7 +72,8 @@ class TestOpsService: trace_config.to_dict.return_value = {"tracing_config": {"project_url": default_url}} app = MagicMock(spec=App) app.tenant_id = "tenant_id" - mock_db.session.query.return_value.where.return_value.first.side_effect = [trace_config, app] + mock_db.session.scalar.return_value = trace_config + mock_db.session.get.return_value = app mock_ops_trace_manager.decrypt_tracing_config.return_value = {} mock_ops_trace_manager.obfuscated_decrypt_token.return_value = {} @@ -97,7 +98,8 @@ class TestOpsService: trace_config.to_dict.return_value = {"tracing_config": {"project_url": "success_url"}} app = MagicMock(spec=App) app.tenant_id = "tenant_id" - mock_db.session.query.return_value.where.return_value.first.side_effect = [trace_config, app] + mock_db.session.scalar.return_value = trace_config + mock_db.session.get.return_value = app mock_ops_trace_manager.decrypt_tracing_config.return_value = {} mock_ops_trace_manager.obfuscated_decrypt_token.return_value = {} @@ -118,7 +120,8 @@ class TestOpsService: trace_config.to_dict.return_value = {"tracing_config": {"project_url": "https://api.langfuse.com/project/key"}} app = MagicMock(spec=App) app.tenant_id = "tenant_id" - mock_db.session.query.return_value.where.return_value.first.side_effect = [trace_config, app] + mock_db.session.scalar.return_value = trace_config + mock_db.session.get.return_value = app mock_ops_trace_manager.decrypt_tracing_config.return_value = {"host": "https://api.langfuse.com"} mock_ops_trace_manager.obfuscated_decrypt_token.return_value = {"host": "https://api.langfuse.com"} @@ -139,7 +142,8 @@ class TestOpsService: trace_config.to_dict.return_value = {"tracing_config": {"project_url": "https://api.langfuse.com/"}} app = MagicMock(spec=App) app.tenant_id = "tenant_id" - mock_db.session.query.return_value.where.return_value.first.side_effect = [trace_config, app] + mock_db.session.scalar.return_value = trace_config + mock_db.session.get.return_value = app mock_ops_trace_manager.decrypt_tracing_config.return_value = {"host": "https://api.langfuse.com"} mock_ops_trace_manager.obfuscated_decrypt_token.return_value = {"host": "https://api.langfuse.com"} @@ -189,7 +193,7 @@ class TestOpsService: mock_ops_trace_manager.check_trace_config_is_effective.return_value = True mock_ops_trace_manager.get_trace_config_project_url.side_effect = Exception("error") mock_ops_trace_manager.get_trace_config_project_key.side_effect = Exception("error") - mock_db.session.query.return_value.where.return_value.first.return_value = MagicMock(spec=TraceAppConfig) + mock_db.session.scalar.return_value = MagicMock(spec=TraceAppConfig) # Act result = OpsService.create_tracing_app_config("app_id", provider, config) @@ -206,7 +210,8 @@ class TestOpsService: mock_ops_trace_manager.get_trace_config_project_key.return_value = "key" app = MagicMock(spec=App) app.tenant_id = "tenant_id" - mock_db.session.query.return_value.where.return_value.first.side_effect = [None, app] + mock_db.session.scalar.return_value = None + mock_db.session.get.return_value = app mock_ops_trace_manager.encrypt_tracing_config.return_value = {} # Act @@ -223,7 +228,7 @@ class TestOpsService: # Arrange provider = TracingProviderEnum.ARIZE mock_ops_trace_manager.check_trace_config_is_effective.return_value = True - mock_db.session.query.return_value.where.return_value.first.return_value = MagicMock(spec=TraceAppConfig) + mock_db.session.scalar.return_value = MagicMock(spec=TraceAppConfig) # Act result = OpsService.create_tracing_app_config("app_id", provider, {}) @@ -237,7 +242,8 @@ class TestOpsService: # Arrange provider = TracingProviderEnum.ARIZE mock_ops_trace_manager.check_trace_config_is_effective.return_value = True - mock_db.session.query.return_value.where.return_value.first.side_effect = [None, None] + mock_db.session.scalar.return_value = None + mock_db.session.get.return_value = None # Act result = OpsService.create_tracing_app_config("app_id", provider, {}) @@ -253,7 +259,8 @@ class TestOpsService: mock_ops_trace_manager.check_trace_config_is_effective.return_value = True app = MagicMock(spec=App) app.tenant_id = "tenant_id" - mock_db.session.query.return_value.where.return_value.first.side_effect = [None, app] + mock_db.session.scalar.return_value = None + mock_db.session.get.return_value = app mock_ops_trace_manager.encrypt_tracing_config.return_value = {} # Act @@ -274,7 +281,8 @@ class TestOpsService: mock_ops_trace_manager.get_trace_config_project_url.return_value = "http://project_url" app = MagicMock(spec=App) app.tenant_id = "tenant_id" - mock_db.session.query.return_value.where.return_value.first.side_effect = [None, app] + mock_db.session.scalar.return_value = None + mock_db.session.get.return_value = app mock_ops_trace_manager.encrypt_tracing_config.return_value = {"encrypted": "config"} # Act @@ -297,7 +305,7 @@ class TestOpsService: def test_update_tracing_app_config_no_config(self, mock_ops_trace_manager, mock_db): # Arrange provider = TracingProviderEnum.ARIZE - mock_db.session.query.return_value.where.return_value.first.return_value = None + mock_db.session.scalar.return_value = None # Act result = OpsService.update_tracing_app_config("app_id", provider, {}) @@ -311,7 +319,8 @@ class TestOpsService: # Arrange provider = TracingProviderEnum.ARIZE current_config = MagicMock(spec=TraceAppConfig) - mock_db.session.query.return_value.where.return_value.first.side_effect = [current_config, None] + mock_db.session.scalar.return_value = current_config + mock_db.session.get.return_value = None # Act result = OpsService.update_tracing_app_config("app_id", provider, {}) @@ -327,7 +336,8 @@ class TestOpsService: current_config = MagicMock(spec=TraceAppConfig) app = MagicMock(spec=App) app.tenant_id = "tenant_id" - mock_db.session.query.return_value.where.return_value.first.side_effect = [current_config, app] + mock_db.session.scalar.return_value = current_config + mock_db.session.get.return_value = app mock_ops_trace_manager.decrypt_tracing_config.return_value = {} mock_ops_trace_manager.check_trace_config_is_effective.return_value = False @@ -344,7 +354,8 @@ class TestOpsService: current_config.to_dict.return_value = {"some": "data"} app = MagicMock(spec=App) app.tenant_id = "tenant_id" - mock_db.session.query.return_value.where.return_value.first.side_effect = [current_config, app] + mock_db.session.scalar.return_value = current_config + mock_db.session.get.return_value = app mock_ops_trace_manager.decrypt_tracing_config.return_value = {} mock_ops_trace_manager.check_trace_config_is_effective.return_value = True @@ -358,7 +369,7 @@ class TestOpsService: @patch("services.ops_service.db") def test_delete_tracing_app_config_no_config(self, mock_db): # Arrange - mock_db.session.query.return_value.where.return_value.first.return_value = None + mock_db.session.scalar.return_value = None # Act result = OpsService.delete_tracing_app_config("app_id", "arize") @@ -370,7 +381,7 @@ class TestOpsService: def test_delete_tracing_app_config_success(self, mock_db): # Arrange trace_config = MagicMock(spec=TraceAppConfig) - mock_db.session.query.return_value.where.return_value.first.return_value = trace_config + mock_db.session.scalar.return_value = trace_config # Act result = OpsService.delete_tracing_app_config("app_id", "arize") From 725f9e3dc4d38b04cfa6493108fc074fb84b8ab0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 09:33:09 +0900 Subject: [PATCH 066/199] chore(deps): bump aiohttp from 3.13.3 to 3.13.4 in /api (#34425) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- api/uv.lock | 72 ++++++++++++++++++++++++++--------------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/api/uv.lock b/api/uv.lock index 39c362eda0..9ec408d380 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -60,7 +60,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.13.3" +version = "3.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -71,42 +71,42 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/4a/064321452809dae953c1ed6e017504e72551a26b6f5708a5a80e4bf556ff/aiohttp-3.13.4.tar.gz", hash = "sha256:d97a6d09c66087890c2ab5d49069e1e570583f7ac0314ecf98294c1b6aaebd38", size = 7859748, upload-time = "2026-03-28T17:19:40.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, - { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, - { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, - { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, - { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, - { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, - { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, - { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, - { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, - { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, - { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, - { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, - { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, - { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, - { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, - { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, - { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, - { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, - { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, - { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, - { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, - { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, - { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, - { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, - { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7e/cb94129302d78c46662b47f9897d642fd0b33bdfef4b73b20c6ced35aa4c/aiohttp-3.13.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8ea0c64d1bcbf201b285c2246c51a0c035ba3bbd306640007bc5844a3b4658c1", size = 760027, upload-time = "2026-03-28T17:15:33.022Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cd/2db3c9397c3bd24216b203dd739945b04f8b87bb036c640da7ddb63c75ef/aiohttp-3.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6f742e1fa45c0ed522b00ede565e18f97e4cf8d1883a712ac42d0339dfb0cce7", size = 508325, upload-time = "2026-03-28T17:15:34.714Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/d28b2722ec13107f2e37a86b8a169897308bab6a3b9e071ecead9d67bd9b/aiohttp-3.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dcfb50ee25b3b7a1222a9123be1f9f89e56e67636b561441f0b304e25aaef8f", size = 502402, upload-time = "2026-03-28T17:15:36.409Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d6/acd47b5f17c4430e555590990a4746efbcb2079909bb865516892bf85f37/aiohttp-3.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3262386c4ff370849863ea93b9ea60fd59c6cf56bf8f93beac625cf4d677c04d", size = 1771224, upload-time = "2026-03-28T17:15:38.223Z" }, + { url = "https://files.pythonhosted.org/packages/98/af/af6e20113ba6a48fd1cd9e5832c4851e7613ef50c7619acdaee6ec5f1aff/aiohttp-3.13.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:473bb5aa4218dd254e9ae4834f20e31f5a0083064ac0136a01a62ddbae2eaa42", size = 1731530, upload-time = "2026-03-28T17:15:39.988Z" }, + { url = "https://files.pythonhosted.org/packages/81/16/78a2f5d9c124ad05d5ce59a9af94214b6466c3491a25fb70760e98e9f762/aiohttp-3.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56423766399b4c77b965f6aaab6c9546617b8994a956821cc507d00b91d978c", size = 1827925, upload-time = "2026-03-28T17:15:41.944Z" }, + { url = "https://files.pythonhosted.org/packages/2a/1f/79acf0974ced805e0e70027389fccbb7d728e6f30fcac725fb1071e63075/aiohttp-3.13.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8af249343fafd5ad90366a16d230fc265cf1149f26075dc9fe93cfd7c7173942", size = 1923579, upload-time = "2026-03-28T17:15:44.071Z" }, + { url = "https://files.pythonhosted.org/packages/af/53/29f9e2054ea6900413f3b4c3eb9d8331f60678ec855f13ba8714c47fd48d/aiohttp-3.13.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bc0a5cf4f10ef5a2c94fdde488734b582a3a7a000b131263e27c9295bd682d9", size = 1767655, upload-time = "2026-03-28T17:15:45.911Z" }, + { url = "https://files.pythonhosted.org/packages/f3/57/462fe1d3da08109ba4aa8590e7aed57c059af2a7e80ec21f4bac5cfe1094/aiohttp-3.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5c7ff1028e3c9fc5123a865ce17df1cb6424d180c503b8517afbe89aa566e6be", size = 1630439, upload-time = "2026-03-28T17:15:48.11Z" }, + { url = "https://files.pythonhosted.org/packages/d7/4b/4813344aacdb8127263e3eec343d24e973421143826364fa9fc847f6283f/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ba5cf98b5dcb9bddd857da6713a503fa6d341043258ca823f0f5ab7ab4a94ee8", size = 1745557, upload-time = "2026-03-28T17:15:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/d4/01/1ef1adae1454341ec50a789f03cfafe4c4ac9c003f6a64515ecd32fe4210/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d85965d3ba21ee4999e83e992fecb86c4614d6920e40705501c0a1f80a583c12", size = 1741796, upload-time = "2026-03-28T17:15:52.351Z" }, + { url = "https://files.pythonhosted.org/packages/22/04/8cdd99af988d2aa6922714d957d21383c559835cbd43fbf5a47ddf2e0f05/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:49f0b18a9b05d79f6f37ddd567695943fcefb834ef480f17a4211987302b2dc7", size = 1805312, upload-time = "2026-03-28T17:15:54.407Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/b48d5577338d4b25bbdbae35c75dbfd0493cb8886dc586fbfb2e90862239/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7f78cb080c86fbf765920e5f1ef35af3f24ec4314d6675d0a21eaf41f6f2679c", size = 1621751, upload-time = "2026-03-28T17:15:56.564Z" }, + { url = "https://files.pythonhosted.org/packages/bc/89/4eecad8c1858e6d0893c05929e22343e0ebe3aec29a8a399c65c3cc38311/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:67a3ec705534a614b68bbf1c70efa777a21c3da3895d1c44510a41f5a7ae0453", size = 1826073, upload-time = "2026-03-28T17:15:58.489Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5c/9dc8293ed31b46c39c9c513ac7ca152b3c3d38e0ea111a530ad12001b827/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6630ec917e85c5356b2295744c8a97d40f007f96a1c76bf1928dc2e27465393", size = 1760083, upload-time = "2026-03-28T17:16:00.677Z" }, + { url = "https://files.pythonhosted.org/packages/1e/19/8bbf6a4994205d96831f97b7d21a0feed120136e6267b5b22d229c6dc4dc/aiohttp-3.13.4-cp311-cp311-win32.whl", hash = "sha256:54049021bc626f53a5394c29e8c444f726ee5a14b6e89e0ad118315b1f90f5e3", size = 439690, upload-time = "2026-03-28T17:16:02.902Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f5/ac409ecd1007528d15c3e8c3a57d34f334c70d76cfb7128a28cffdebd4c1/aiohttp-3.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:c033f2bc964156030772d31cbf7e5defea181238ce1f87b9455b786de7d30145", size = 463824, upload-time = "2026-03-28T17:16:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bd/ede278648914cabbabfdf95e436679b5d4156e417896a9b9f4587169e376/aiohttp-3.13.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ee62d4471ce86b108b19c3364db4b91180d13fe3510144872d6bad5401957360", size = 752158, upload-time = "2026-03-28T17:16:06.901Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/581c053253c07b480b03785196ca5335e3c606a37dc73e95f6527f1591fe/aiohttp-3.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c0fd8f41b54b58636402eb493afd512c23580456f022c1ba2db0f810c959ed0d", size = 501037, upload-time = "2026-03-28T17:16:08.82Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/a5ede193c08f13cc42c0a5b50d1e246ecee9115e4cf6e900d8dbd8fd6acb/aiohttp-3.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4baa48ce49efd82d6b1a0be12d6a36b35e5594d1dd42f8bfba96ea9f8678b88c", size = 501556, upload-time = "2026-03-28T17:16:10.63Z" }, + { url = "https://files.pythonhosted.org/packages/d6/10/88ff67cd48a6ec36335b63a640abe86135791544863e0cfe1f065d6cef7a/aiohttp-3.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d738ebab9f71ee652d9dbd0211057690022201b11197f9a7324fd4dba128aa97", size = 1757314, upload-time = "2026-03-28T17:16:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/8b/15/fdb90a5cf5a1f52845c276e76298c75fbbcc0ac2b4a86551906d54529965/aiohttp-3.13.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0ce692c3468fa831af7dceed52edf51ac348cebfc8d3feb935927b63bd3e8576", size = 1731819, upload-time = "2026-03-28T17:16:14.558Z" }, + { url = "https://files.pythonhosted.org/packages/ec/df/28146785a007f7820416be05d4f28cc207493efd1e8c6c1068e9bdc29198/aiohttp-3.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e08abcfe752a454d2cb89ff0c08f2d1ecd057ae3e8cc6d84638de853530ebab", size = 1793279, upload-time = "2026-03-28T17:16:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/10/47/689c743abf62ea7a77774d5722f220e2c912a77d65d368b884d9779ef41b/aiohttp-3.13.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5977f701b3fff36367a11087f30ea73c212e686d41cd363c50c022d48b011d8d", size = 1891082, upload-time = "2026-03-28T17:16:18.71Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/f7f4f318c7e58c23b761c9b13b9a3c9b394e0f9d5d76fbc6622fa98509f6/aiohttp-3.13.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54203e10405c06f8b6020bd1e076ae0fe6c194adcee12a5a78af3ffa3c57025e", size = 1773938, upload-time = "2026-03-28T17:16:21.125Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/f207cb3121852c989586a6fc16ff854c4fcc8651b86c5d3bd1fc83057650/aiohttp-3.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:358a6af0145bc4dda037f13167bef3cce54b132087acc4c295c739d05d16b1c3", size = 1579548, upload-time = "2026-03-28T17:16:23.588Z" }, + { url = "https://files.pythonhosted.org/packages/6c/58/e1289661a32161e24c1fe479711d783067210d266842523752869cc1d9c2/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:898ea1850656d7d61832ef06aa9846ab3ddb1621b74f46de78fbc5e1a586ba83", size = 1714669, upload-time = "2026-03-28T17:16:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/96/0a/3e86d039438a74a86e6a948a9119b22540bae037d6ba317a042ae3c22711/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7bc30cceb710cf6a44e9617e43eebb6e3e43ad855a34da7b4b6a73537d8a6763", size = 1754175, upload-time = "2026-03-28T17:16:28.18Z" }, + { url = "https://files.pythonhosted.org/packages/f4/30/e717fc5df83133ba467a560b6d8ef20197037b4bb5d7075b90037de1018e/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4a31c0c587a8a038f19a4c7e60654a6c899c9de9174593a13e7cc6e15ff271f9", size = 1762049, upload-time = "2026-03-28T17:16:30.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/28/8f7a2d4492e336e40005151bdd94baf344880a4707573378579f833a64c1/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2062f675f3fe6e06d6113eb74a157fb9df58953ffed0cdb4182554b116545758", size = 1570861, upload-time = "2026-03-28T17:16:32.953Z" }, + { url = "https://files.pythonhosted.org/packages/78/45/12e1a3d0645968b1c38de4b23fdf270b8637735ea057d4f84482ff918ad9/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d1ba8afb847ff80626d5e408c1fdc99f942acc877d0702fe137015903a220a9", size = 1790003, upload-time = "2026-03-28T17:16:35.468Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/60374e18d590de16dcb39d6ff62f39c096c1b958e6f37727b5870026ea30/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b08149419994cdd4d5eecf7fd4bc5986b5a9380285bcd01ab4c0d6bfca47b79d", size = 1737289, upload-time = "2026-03-28T17:16:38.187Z" }, + { url = "https://files.pythonhosted.org/packages/02/bf/535e58d886cfbc40a8b0013c974afad24ef7632d645bca0b678b70033a60/aiohttp-3.13.4-cp312-cp312-win32.whl", hash = "sha256:fc432f6a2c4f720180959bc19aa37259651c1a4ed8af8afc84dd41c60f15f791", size = 434185, upload-time = "2026-03-28T17:16:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1a/d92e3325134ebfff6f4069f270d3aac770d63320bd1fcd0eca023e74d9a8/aiohttp-3.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:6148c9ae97a3e8bff9a1fc9c757fa164116f86c100468339730e717590a3fb77", size = 461285, upload-time = "2026-03-28T17:16:42.713Z" }, ] [[package]] From 2d29345f2631c33b6ca55d00af4ac668378ff691 Mon Sep 17 00:00:00 2001 From: YBoy Date: Thu, 2 Apr 2026 03:47:08 +0200 Subject: [PATCH 067/199] =?UTF-8?q?refactor(api):=20type=20OpsTraceProvide?= =?UTF-8?q?rConfigMap=20with=20TracingProviderCon=E2=80=A6=20(#34424)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/core/ops/ops_trace_manager.py | 24 ++++++++++++++++-------- api/services/ops_service.py | 6 ++---- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/api/core/ops/ops_trace_manager.py b/api/core/ops/ops_trace_manager.py index c689a86614..aa39e6b681 100644 --- a/api/core/ops/ops_trace_manager.py +++ b/api/core/ops/ops_trace_manager.py @@ -19,6 +19,7 @@ from typing_extensions import TypedDict from core.helper.encrypter import batch_decrypt_token, encrypt_token, obfuscated_token from core.ops.entities.config_entity import ( OPS_FILE_PATH, + BaseTracingConfig, TracingProviderEnum, ) from core.ops.entities.trace_entity import ( @@ -195,8 +196,15 @@ def _lookup_llm_credential_info( return None, "" -class OpsTraceProviderConfigMap(collections.UserDict[str, dict[str, Any]]): - def __getitem__(self, provider: str) -> dict[str, Any]: +class TracingProviderConfigEntry(TypedDict): + config_class: type[BaseTracingConfig] + secret_keys: list[str] + other_keys: list[str] + trace_instance: type[Any] + + +class OpsTraceProviderConfigMap(collections.UserDict[str, TracingProviderConfigEntry]): + def __getitem__(self, provider: str) -> TracingProviderConfigEntry: match provider: case TracingProviderEnum.LANGFUSE: from core.ops.entities.config_entity import LangfuseConfig @@ -585,8 +593,8 @@ class OpsTraceManager: provider_config_map[tracing_provider]["config_class"], provider_config_map[tracing_provider]["trace_instance"], ) - tracing_config = config_type(**tracing_config) - return trace_instance(tracing_config).api_check() + config = config_type(**tracing_config) + return trace_instance(config).api_check() @staticmethod def get_trace_config_project_key(tracing_config: dict, tracing_provider: str): @@ -600,8 +608,8 @@ class OpsTraceManager: provider_config_map[tracing_provider]["config_class"], provider_config_map[tracing_provider]["trace_instance"], ) - tracing_config = config_type(**tracing_config) - return trace_instance(tracing_config).get_project_key() + config = config_type(**tracing_config) + return trace_instance(config).get_project_key() @staticmethod def get_trace_config_project_url(tracing_config: dict, tracing_provider: str): @@ -615,8 +623,8 @@ class OpsTraceManager: provider_config_map[tracing_provider]["config_class"], provider_config_map[tracing_provider]["trace_instance"], ) - tracing_config = config_type(**tracing_config) - return trace_instance(tracing_config).get_project_url() + config = config_type(**tracing_config) + return trace_instance(config).get_project_url() class TraceTask: diff --git a/api/services/ops_service.py b/api/services/ops_service.py index 2a64088dd6..0db3d3efec 100644 --- a/api/services/ops_service.py +++ b/api/services/ops_service.py @@ -1,9 +1,7 @@ -from typing import Any - from sqlalchemy import select from core.ops.entities.config_entity import BaseTracingConfig -from core.ops.ops_trace_manager import OpsTraceManager, provider_config_map +from core.ops.ops_trace_manager import OpsTraceManager, TracingProviderConfigEntry, provider_config_map from extensions.ext_database import db from models.model import App, TraceAppConfig @@ -150,7 +148,7 @@ class OpsService: except KeyError: return {"error": f"Invalid tracing provider: {tracing_provider}"} - provider_config: dict[str, Any] = provider_config_map[tracing_provider] + provider_config: TracingProviderConfigEntry = provider_config_map[tracing_provider] config_class: type[BaseTracingConfig] = provider_config["config_class"] other_keys: list[str] = provider_config["other_keys"] From f9d9ad7a3817653f5eeb84880a07175242ebdfbe Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:16:50 +0800 Subject: [PATCH 068/199] refactor(web): migrate remaining toast usage (#34433) --- .../references/runtime-rules.md | 7 +- web/.storybook/preview.tsx | 7 +- .../apps/app-card-operations-flow.test.tsx | 40 +- .../datasets/create-dataset-flow.test.tsx | 8 +- .../dsl-export-import-flow.test.ts | 17 +- .../tools/tool-provider-detail-flow.test.tsx | 8 +- .../[appId]/overview/card-view.tsx | 9 +- .../[appId]/overview/tracing/panel.tsx | 7 +- .../tracing/provider-config-modal.tsx | 17 +- .../account-page/AvatarWithEdit.tsx | 80 ++- .../account-page/email-change-modal.tsx | 382 +++++++-------- .../(commonLayout)/account-page/index.tsx | 216 ++++----- .../delete-account/components/feed-back.tsx | 4 +- .../__tests__/use-app-info-actions.spec.ts | 43 +- .../app-info/use-app-info-actions.ts | 33 +- .../app-sidebar/dataset-info/dropdown.tsx | 8 +- .../add-annotation-modal/index.spec.tsx | 8 +- .../annotation/add-annotation-modal/index.tsx | 7 +- .../csv-uploader.spec.tsx | 28 +- .../csv-uploader.tsx | 6 +- .../batch-add-annotation-modal/index.spec.tsx | 20 +- .../batch-add-annotation-modal/index.tsx | 11 +- .../edit-annotation-modal/index.spec.tsx | 37 +- .../edit-annotation-modal/index.tsx | 12 +- .../components/app/annotation/index.spec.tsx | 24 +- web/app/components/app/annotation/index.tsx | 22 +- .../access-control.spec.tsx | 6 +- .../app/app-access-control/index.tsx | 4 +- .../components/app/app-publisher/index.tsx | 4 +- .../app/app-publisher/version-info-modal.tsx | 12 +- .../config-prompt/advanced-prompt-input.tsx | 24 +- .../config-prompt/simple-prompt-input.tsx | 24 +- .../config-var/config-modal/index.tsx | 26 +- .../configuration/config-var/index.spec.tsx | 18 +- .../app/configuration/config-var/index.tsx | 9 +- .../config/agent/prompt-editor.tsx | 8 +- .../config/automatic/get-automatic-res.tsx | 25 +- .../configuration/config/automatic/result.tsx | 4 +- .../code-generator/get-code-generator-res.tsx | 18 +- .../params-config/config-content.spec.tsx | 18 +- .../params-config/config-content.tsx | 6 +- .../settings-modal/index.spec.tsx | 41 +- .../dataset-config/settings-modal/index.tsx | 11 +- .../debug-with-single-model/index.spec.tsx | 7 - .../app/configuration/debug/index.spec.tsx | 54 ++- .../app/configuration/debug/index.tsx | 50 +- .../components/app/configuration/index.tsx | 19 +- .../tools/external-data-tool-modal.tsx | 248 +++++----- .../app/configuration/tools/index.tsx | 52 +- .../app/create-app-modal/index.spec.tsx | 23 +- .../components/app/create-app-modal/index.tsx | 17 +- .../app/create-from-dsl-modal/index.tsx | 24 +- .../app/create-from-dsl-modal/uploader.tsx | 6 +- .../app/duplicate-modal/index.spec.tsx | 6 +- .../components/app/duplicate-modal/index.tsx | 4 +- web/app/components/app/log/list.tsx | 27 +- .../app/overview/settings/index.spec.tsx | 34 +- .../app/overview/settings/index.tsx | 9 +- .../app/switch-app-modal/index.spec.tsx | 40 +- .../components/app/switch-app-modal/index.tsx | 8 +- .../app/text-generate/item/index.tsx | 6 +- .../text-generate/saved-items/index.spec.tsx | 8 +- .../app/text-generate/saved-items/index.tsx | 4 +- .../apps/__tests__/app-card.spec.tsx | 38 +- web/app/components/apps/app-card.tsx | 41 +- .../agent-log-modal/__tests__/detail.spec.tsx | 35 +- .../agent-log-modal/__tests__/index.spec.tsx | 42 +- .../base/agent-log-modal/detail.tsx | 66 +-- .../base/agent-log-modal/index.stories.tsx | 7 +- .../base/audio-btn/__tests__/audio.spec.ts | 6 +- web/app/components/base/audio-btn/audio.ts | 20 +- .../base/audio-gallery/AudioPlayer.tsx | 88 +--- .../__tests__/AudioPlayer.spec.tsx | 20 +- .../base/block-input/__tests__/index.spec.tsx | 6 +- web/app/components/base/block-input/index.tsx | 47 +- .../__tests__/hooks.spec.tsx | 5 +- .../base/chat/chat-with-history/hooks.tsx | 166 ++----- .../check-input-forms-hooks.spec.tsx | 12 +- .../base/chat/chat/__tests__/hooks.spec.tsx | 10 +- .../chat/chat/__tests__/question.spec.tsx | 4 +- .../chat/answer/__tests__/operation.spec.tsx | 2 +- .../base/chat/chat/answer/operation.tsx | 4 +- .../chat-input-area/__tests__/index.spec.tsx | 12 +- .../base/chat/chat/chat-input-area/index.tsx | 164 +------ .../base/chat/chat/check-input-forms-hooks.ts | 18 +- web/app/components/base/chat/chat/hooks.ts | 6 +- .../components/base/chat/chat/question.tsx | 119 +---- .../embedded-chatbot/__tests__/hooks.spec.tsx | 5 +- .../base/chat/embedded-chatbot/hooks.tsx | 115 +---- .../inputs-form/__tests__/content.spec.tsx | 4 +- .../__tests__/annotation-ctrl-button.spec.tsx | 8 +- .../__tests__/config-param-modal.spec.tsx | 13 +- .../annotation-ctrl-button.tsx | 32 +- .../annotation-reply/config-param-modal.tsx | 47 +- .../moderation-setting-modal.spec.tsx | 6 +- .../moderation/moderation-setting-modal.tsx | 18 +- .../file-uploader/__tests__/hooks.spec.ts | 9 +- .../index.stories.tsx | 7 +- .../index.stories.tsx | 7 +- .../components/base/file-uploader/hooks.ts | 77 ++- .../__tests__/use-check-validated.spec.ts | 14 +- .../base/form/hooks/use-check-validated.ts | 17 +- .../image-uploader/__tests__/hooks.spec.ts | 6 +- .../__tests__/image-preview.spec.tsx | 8 +- .../components/base/image-uploader/hooks.ts | 112 ++--- .../base/image-uploader/image-preview.tsx | 24 +- .../base/tag-input/__tests__/index.spec.tsx | 14 +- .../base/tag-input/__tests__/interop.spec.tsx | 32 +- web/app/components/base/tag-input/index.tsx | 112 ++--- .../tag-management/__tests__/index.spec.tsx | 28 +- .../tag-management/__tests__/panel.spec.tsx | 42 +- .../__tests__/selector.spec.tsx | 31 +- .../__tests__/tag-item-editor.spec.tsx | 71 +-- .../base/tag-management/index.stories.tsx | 7 +- .../components/base/tag-management/index.tsx | 43 +- .../components/base/tag-management/panel.tsx | 85 +--- .../base/tag-management/tag-item-editor.tsx | 45 +- .../text-generation/__tests__/hooks.spec.ts | 14 +- .../components/base/text-generation/hooks.ts | 57 +-- .../base/toast/__tests__/index.spec.tsx | 349 -------------- web/app/components/base/toast/context.ts | 33 -- .../components/base/toast/index.stories.tsx | 105 ---- web/app/components/base/toast/index.tsx | 173 ------- .../components/base/toast/style.module.css | 44 -- .../custom-page/__tests__/index.spec.tsx | 22 +- .../__tests__/use-web-app-brand.spec.tsx | 22 +- .../hooks/use-web-app-brand.ts | 32 +- .../common/retrieval-param-config/index.tsx | 2 +- .../__tests__/index.spec.tsx | 43 +- .../__tests__/uploader.spec.tsx | 24 +- .../hooks/__tests__/use-dsl-import.spec.tsx | 39 +- .../hooks/use-dsl-import.ts | 67 +-- .../create-from-dsl-modal/uploader.tsx | 44 +- .../__tests__/index.spec.tsx | 28 +- .../empty-dataset-creation-modal/index.tsx | 26 +- .../hooks/__tests__/use-file-upload.spec.tsx | 58 ++- .../file-uploader/hooks/use-file-upload.ts | 18 +- .../__tests__/use-document-creation.spec.ts | 8 +- .../step-two/hooks/use-document-creation.ts | 107 +--- .../datasets/create/step-two/index.tsx | 4 +- .../create/website/firecrawl/index.tsx | 67 +-- .../create/website/jina-reader/index.tsx | 64 +-- .../create/website/watercrawl/index.tsx | 78 +-- .../components/__tests__/operations.spec.tsx | 39 +- .../documents/components/operations.tsx | 121 +---- .../__tests__/use-local-file-upload.spec.tsx | 57 +-- .../documents/detail/__tests__/index.spec.tsx | 2 +- .../__tests__/csv-uploader.spec.tsx | 42 +- .../detail/batch-modal/csv-uploader.tsx | 55 +-- .../detail/completed/__tests__/index.spec.tsx | 17 +- .../__tests__/use-child-segment-data.spec.ts | 7 +- .../__tests__/use-segment-list-data.spec.ts | 7 +- .../completed/hooks/use-child-segment-data.ts | 160 ++---- .../completed/hooks/use-segment-list-data.ts | 120 +---- .../detail/embedding/__tests__/index.spec.tsx | 33 +- .../documents/detail/embedding/index.tsx | 74 +-- .../datasets/documents/detail/index.tsx | 8 +- .../detail/metadata/__tests__/index.spec.tsx | 27 +- .../__tests__/use-metadata-state.spec.ts | 23 +- .../metadata/hooks/use-metadata-state.ts | 29 +- .../status-item/__tests__/index.spec.tsx | 27 +- .../datasets/documents/status-item/index.tsx | 70 +-- .../__tests__/index.spec.tsx | 14 +- .../external-api/external-api-modal/index.tsx | 85 +--- .../__tests__/modify-retrieval-modal.spec.tsx | 21 +- .../hit-testing/modify-retrieval-modal.tsx | 58 +-- .../__tests__/modal.spec.tsx | 8 +- .../metadata/edit-metadata-batch/modal.tsx | 81 +--- .../use-batch-edit-document-metadata.spec.ts | 7 +- .../use-edit-dataset-metadata.spec.ts | 8 +- .../__tests__/use-metadata-document.spec.ts | 8 +- .../hooks/use-batch-edit-document-metadata.ts | 38 +- .../hooks/use-edit-dataset-metadata.ts | 38 +- .../metadata/hooks/use-metadata-document.ts | 41 +- .../dataset-metadata-drawer.spec.tsx | 8 +- .../dataset-metadata-drawer.tsx | 17 +- .../rename-modal/__tests__/index.spec.tsx | 68 ++- .../datasets/rename-modal/index.tsx | 75 +-- .../create-app-modal/__tests__/index.spec.tsx | 2 - .../explore/create-app-modal/index.tsx | 4 +- .../__tests__/compliance.spec.tsx | 18 +- .../header/account-dropdown/compliance.tsx | 97 +--- .../__tests__/index.spec.tsx | 22 +- .../workplace-selector/index.tsx | 99 ++-- .../__tests__/modal.spec.tsx | 35 +- .../api-based-extension-page/modal.tsx | 79 +-- .../language-page/__tests__/index.spec.tsx | 7 +- .../account-setting/language-page/index.tsx | 37 +- .../__tests__/dialog.spec.tsx | 5 +- .../__tests__/index.spec.tsx | 22 +- .../edit-workspace-modal/index.tsx | 55 +-- .../operation/__tests__/index.spec.tsx | 5 +- .../members-page/operation/index.tsx | 58 +-- .../__tests__/index.spec.tsx | 22 +- .../transfer-ownership-modal/index.tsx | 146 +----- .../hooks/__tests__/use-auth.spec.tsx | 19 +- .../model-auth/hooks/use-auth.ts | 98 ++-- .../__tests__/credential-panel.spec.tsx | 8 +- .../model-load-balancing-modal.spec.tsx | 21 +- .../use-change-provider-priority.spec.ts | 8 +- .../use-activate-credential.spec.tsx | 18 +- .../use-activate-credential.ts | 35 +- .../model-load-balancing-modal.tsx | 269 +++-------- .../use-change-provider-priority.ts | 44 +- .../plugin-page/SerpapiPlugin.tsx | 16 +- .../__tests__/SerpapiPlugin.spec.tsx | 27 +- .../plugin-page/__tests__/index.spec.tsx | 8 +- .../__tests__/authorized-in-node.spec.tsx | 4 - .../__tests__/plugin-auth-in-agent.spec.tsx | 4 - .../__tests__/api-key-modal.spec.tsx | 19 +- .../__tests__/authorize-components.spec.tsx | 16 +- .../__tests__/oauth-client-settings.spec.tsx | 19 +- .../plugin-auth/authorize/api-key-modal.tsx | 10 +- .../authorize/oauth-client-settings.tsx | 17 +- .../authorized/__tests__/index.spec.tsx | 40 +- .../plugins/plugin-auth/authorized/index.tsx | 24 +- .../__tests__/use-plugin-auth-action.spec.ts | 28 +- .../hooks/use-plugin-auth-action.ts | 28 +- .../datasource-action-list.tsx | 5 - .../edit/__tests__/apikey-edit-modal.spec.tsx | 29 +- .../edit/__tests__/manual-edit-modal.spec.tsx | 29 +- .../edit/__tests__/oauth-edit-modal.spec.tsx | 29 +- .../__tests__/use-reference-setting.spec.ts | 14 +- .../plugin-page/use-reference-setting.ts | 7 +- .../update-plugin/__tests__/index.spec.tsx | 25 +- .../update-plugin/from-market-place.tsx | 9 +- .../components/__tests__/conversion.spec.tsx | 34 +- .../__tests__/update-dsl-modal.spec.tsx | 45 +- .../rag-pipeline/components/conversion.tsx | 40 +- .../editor/form/__tests__/index.spec.tsx | 13 +- .../panel/input-field/editor/form/index.tsx | 35 +- .../field-list/__tests__/hooks.spec.ts | 8 +- .../field-list/__tests__/index.spec.tsx | 10 +- .../panel/input-field/field-list/hooks.ts | 41 +- .../__tests__/index.spec.tsx | 6 +- .../document-processing/options.tsx | 22 +- .../__tests__/index.spec.tsx | 21 +- .../publisher/__tests__/index.spec.tsx | 38 +- .../publisher/__tests__/popup.spec.tsx | 23 +- .../rag-pipeline-header/publisher/popup.tsx | 215 ++------- .../hooks/__tests__/index.spec.ts | 12 +- .../hooks/__tests__/use-DSL.spec.ts | 23 +- .../__tests__/use-update-dsl-modal.spec.ts | 49 +- .../components/rag-pipeline/hooks/use-DSL.ts | 25 +- .../hooks/use-update-dsl-modal.ts | 79 +-- .../use-text-generation-app-state.spec.ts | 8 +- .../hooks/use-text-generation-app-state.ts | 45 +- .../share/text-generation/index.tsx | 123 +---- .../result/__tests__/index.spec.tsx | 2 +- .../share/text-generation/result/index.tsx | 106 ++-- .../__tests__/index.spec.tsx | 14 +- .../edit-custom-collection-modal/index.tsx | 7 +- .../tools/mcp/hooks/use-mcp-modal-form.ts | 22 +- .../__tests__/config-credentials.spec.tsx | 2 +- .../setting/build-in/config-credentials.tsx | 4 +- .../__tests__/features-trigger.spec.tsx | 33 +- .../workflow-header/features-trigger.tsx | 9 +- .../hooks/__tests__/use-DSL.spec.ts | 27 +- .../components/workflow-app/hooks/use-DSL.ts | 11 +- .../nodes/_base/hooks/use-one-step-run.ts | 2 +- .../workflow/nodes/trigger-webhook/panel.tsx | 2 +- .../plugins/link-editor-plugin/hooks.ts | 121 ++--- .../__tests__/value-content-sections.spec.tsx | 36 +- .../education-apply/education-apply-page.tsx | 8 +- web/app/init/InitPasswordPopup.tsx | 8 +- web/app/layout.tsx | 13 +- web/app/signin/invite-settings/page.tsx | 4 +- web/app/signin/one-more-step.tsx | 4 +- web/docs/overlay-migration.md | 17 +- web/eslint-suppressions.json | 456 ++---------------- web/eslint.constants.mjs | 9 - web/hooks/use-import-dsl.ts | 34 +- web/service/base.ts | 18 +- 273 files changed, 3491 insertions(+), 6996 deletions(-) delete mode 100644 web/app/components/base/toast/__tests__/index.spec.tsx delete mode 100644 web/app/components/base/toast/context.ts delete mode 100644 web/app/components/base/toast/index.stories.tsx delete mode 100644 web/app/components/base/toast/index.tsx delete mode 100644 web/app/components/base/toast/style.module.css diff --git a/.agents/skills/frontend-query-mutation/references/runtime-rules.md b/.agents/skills/frontend-query-mutation/references/runtime-rules.md index 02e8b9c2b6..73d6fbdded 100644 --- a/.agents/skills/frontend-query-mutation/references/runtime-rules.md +++ b/.agents/skills/frontend-query-mutation/references/runtime-rules.md @@ -64,7 +64,7 @@ export const useUpdateAccessMode = () => { // Component only adds UI behavior. updateAccessMode({ appId, mode }, { - onSuccess: () => Toast.notify({ type: 'success', message: '...' }), + onSuccess: () => toast.success('...'), }) // Avoid putting invalidation knowledge in the component. @@ -114,10 +114,7 @@ try { router.push(`/orders/${order.id}`) } catch (error) { - Toast.notify({ - type: 'error', - message: error instanceof Error ? error.message : 'Unknown error', - }) + toast.error(error instanceof Error ? error.message : 'Unknown error') } ``` diff --git a/web/.storybook/preview.tsx b/web/.storybook/preview.tsx index 072244c33f..a9144e7128 100644 --- a/web/.storybook/preview.tsx +++ b/web/.storybook/preview.tsx @@ -2,7 +2,7 @@ import type { Preview } from '@storybook/react' import type { Resource } from 'i18next' import { withThemeByDataAttribute } from '@storybook/addon-themes' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { ToastProvider } from '../app/components/base/toast' +import { ToastHost } from '../app/components/base/ui/toast' import { I18nClientProvider as I18N } from '../app/components/provider/i18n' import commonEnUS from '../i18n/en-US/common.json' @@ -39,9 +39,10 @@ export const decorators = [ return ( - + <> + - + ) diff --git a/web/__tests__/apps/app-card-operations-flow.test.tsx b/web/__tests__/apps/app-card-operations-flow.test.tsx index c5766878a1..765c7045e5 100644 --- a/web/__tests__/apps/app-card-operations-flow.test.tsx +++ b/web/__tests__/apps/app-card-operations-flow.test.tsx @@ -23,8 +23,25 @@ let mockSystemFeatures = { webapp_auth: { enabled: false }, } +const toastMocks = vi.hoisted(() => ({ + mockNotify: vi.fn(), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), +})) const mockRouterPush = vi.fn() -const mockNotify = vi.fn() + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + success: (message: string, options?: Record) => toastMocks.mockNotify({ type: 'success', message, ...options }), + error: (message: string, options?: Record) => toastMocks.mockNotify({ type: 'error', message, ...options }), + warning: (message: string, options?: Record) => toastMocks.mockNotify({ type: 'warning', message, ...options }), + info: (message: string, options?: Record) => toastMocks.mockNotify({ type: 'info', message, ...options }), + dismiss: toastMocks.dismiss, + update: toastMocks.update, + promise: toastMocks.promise, + }, +})) const mockOnPlanInfoChanged = vi.fn() const mockDeleteAppMutation = vi.fn().mockResolvedValue(undefined) let mockDeleteMutationPending = false @@ -94,27 +111,6 @@ vi.mock('@/context/provider-context', () => ({ }), })) -// Mock the ToastContext used via useContext from use-context-selector -vi.mock('use-context-selector', async () => { - const actual = await vi.importActual('use-context-selector') - return { - ...actual, - useContext: () => ({ notify: mockNotify }), - } -}) - -vi.mock('@/app/components/base/tag-management/store', () => ({ - useStore: (selector: (state: Record) => unknown) => { - const state = { - tagList: [], - showTagManagementModal: false, - setTagList: vi.fn(), - setShowTagManagementModal: vi.fn(), - } - return selector(state) - }, -})) - vi.mock('@/service/tag', () => ({ fetchTagList: vi.fn().mockResolvedValue([]), })) diff --git a/web/__tests__/datasets/create-dataset-flow.test.tsx b/web/__tests__/datasets/create-dataset-flow.test.tsx index e3a59edde6..34d64d8c43 100644 --- a/web/__tests__/datasets/create-dataset-flow.test.tsx +++ b/web/__tests__/datasets/create-dataset-flow.test.tsx @@ -33,8 +33,14 @@ vi.mock('@/service/knowledge/use-dataset', () => ({ useInvalidDatasetList: () => vi.fn(), })) -vi.mock('@/app/components/base/toast', () => ({ +vi.mock('@/app/components/base/ui/toast', () => ({ default: { notify: vi.fn() }, + toast: { + success: vi.fn(), + error: vi.fn(), + warning: vi.fn(), + info: vi.fn(), + }, })) vi.mock('@/app/components/base/amplitude', () => ({ diff --git a/web/__tests__/rag-pipeline/dsl-export-import-flow.test.ts b/web/__tests__/rag-pipeline/dsl-export-import-flow.test.ts index dc5ab3fc86..cdf7aba4f6 100644 --- a/web/__tests__/rag-pipeline/dsl-export-import-flow.test.ts +++ b/web/__tests__/rag-pipeline/dsl-export-import-flow.test.ts @@ -10,6 +10,19 @@ import { describe, expect, it, vi } from 'vitest' const mockDoSyncWorkflowDraft = vi.fn().mockResolvedValue(undefined) const mockExportPipelineConfig = vi.fn().mockResolvedValue({ data: 'yaml-content' }) const mockNotify = vi.fn() +const mockToast = { + success: (message: string, options?: Record) => mockNotify({ type: 'success', message, ...options }), + error: (message: string, options?: Record) => mockNotify({ type: 'error', message, ...options }), + warning: (message: string, options?: Record) => mockNotify({ type: 'warning', message, ...options }), + info: (message: string, options?: Record) => mockNotify({ type: 'info', message, ...options }), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), +} + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: mockToast, +})) const mockEventEmitter = { emit: vi.fn() } const mockDownloadBlob = vi.fn() @@ -19,10 +32,6 @@ vi.mock('react-i18next', () => ({ }), })) -vi.mock('@/app/components/base/toast/context', () => ({ - useToastContext: () => ({ notify: mockNotify }), -})) - vi.mock('@/app/components/workflow/constants', () => ({ DSL_EXPORT_CHECK: 'DSL_EXPORT_CHECK', })) diff --git a/web/__tests__/tools/tool-provider-detail-flow.test.tsx b/web/__tests__/tools/tool-provider-detail-flow.test.tsx index 0101f83f22..3d66467695 100644 --- a/web/__tests__/tools/tool-provider-detail-flow.test.tsx +++ b/web/__tests__/tools/tool-provider-detail-flow.test.tsx @@ -153,8 +153,14 @@ vi.mock('@/app/components/base/confirm', () => ({ ), })) -vi.mock('@/app/components/base/toast', () => ({ +vi.mock('@/app/components/base/ui/toast', () => ({ default: { notify: vi.fn() }, + toast: { + success: vi.fn(), + error: vi.fn(), + warning: vi.fn(), + info: vi.fn(), + }, })) vi.mock('@/app/components/base/icons/src/vender/line/general', () => ({ diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx index 8c1df8d63d..26373bd42a 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx @@ -7,12 +7,11 @@ import type { App } from '@/types/app' import type { I18nKeysByPrefix } from '@/types/i18n' import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import AppCard from '@/app/components/app/overview/app-card' import TriggerCard from '@/app/components/app/overview/trigger-card' import { useStore as useAppStore } from '@/app/components/app/store' import Loading from '@/app/components/base/loading' -import { ToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card' import { isTriggerNode } from '@/app/components/workflow/types' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' @@ -34,7 +33,6 @@ export type ICardViewProps = { const CardView: FC = ({ appId, isInPanel, className }) => { const { t } = useTranslation() - const { notify } = useContext(ToastContext) const appDetail = useAppStore(state => state.appDetail) const setAppDetail = useAppStore(state => state.setAppDetail) @@ -90,10 +88,7 @@ const CardView: FC = ({ appId, isInPanel, className }) => { if (type === 'success') updateAppDetail() - notify({ - type, - message: t(`actionMsg.${message}`, { ns: 'common' }) as string, - }) + toast(t(`actionMsg.${message}`, { ns: 'common' }) as string, { type }) } const onChangeSiteStatus = async (value: boolean) => { diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx index 4201d11490..239427159c 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx @@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' import { AliyunIcon, ArizeIcon, DatabricksIcon, LangfuseIcon, LangsmithIcon, MlflowIcon, OpikIcon, PhoenixIcon, TencentIcon, WeaveIcon } from '@/app/components/base/icons/src/public/tracing' import Loading from '@/app/components/base/loading' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import Indicator from '@/app/components/header/indicator' import { useAppContext } from '@/context/app-context' import { usePathname } from '@/next/navigation' @@ -43,10 +43,7 @@ const Panel: FC = () => { await updateTracingStatus({ appId, body: tracingStatus }) setTracingStatus(tracingStatus) if (!noToast) { - Toast.notify({ - type: 'success', - message: t('api.success', { ns: 'common' }), - }) + toast(t('api.success', { ns: 'common' }), { type: 'success' }) } } diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx index ff78712c3c..cc2143faac 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx @@ -14,7 +14,7 @@ import { PortalToFollowElem, PortalToFollowElemContent, } from '@/app/components/base/portal-to-follow-elem' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { addTracingConfig, removeTracingConfig, updateTracingConfig } from '@/service/apps' import { docURL } from './config' import Field from './field' @@ -155,10 +155,7 @@ const ProviderConfigModal: FC = ({ appId, provider: type, }) - Toast.notify({ - type: 'success', - message: t('api.remove', { ns: 'common' }), - }) + toast(t('api.remove', { ns: 'common' }), { type: 'success' }) onRemoved() hideRemoveConfirm() }, [hideRemoveConfirm, appId, type, t, onRemoved]) @@ -264,10 +261,7 @@ const ProviderConfigModal: FC = ({ return const errorMessage = checkValid() if (errorMessage) { - Toast.notify({ - type: 'error', - message: errorMessage, - }) + toast(errorMessage, { type: 'error' }) return } const action = isEdit ? updateTracingConfig : addTracingConfig @@ -279,10 +273,7 @@ const ProviderConfigModal: FC = ({ tracing_config: config, }, }) - Toast.notify({ - type: 'success', - message: t('api.success', { ns: 'common' }), - }) + toast(t('api.success', { ns: 'common' }), { type: 'success' }) onSaved(config) if (isAdd) onChosen(type) diff --git a/web/app/account/(commonLayout)/account-page/AvatarWithEdit.tsx b/web/app/account/(commonLayout)/account-page/AvatarWithEdit.tsx index 3fc677d8d8..25e529a221 100644 --- a/web/app/account/(commonLayout)/account-page/AvatarWithEdit.tsx +++ b/web/app/account/(commonLayout)/account-page/AvatarWithEdit.tsx @@ -8,15 +8,14 @@ import { RiDeleteBin5Line, RiPencilLine } from '@remixicon/react' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import ImageInput from '@/app/components/base/app-icon-picker/ImageInput' import getCroppedImg from '@/app/components/base/app-icon-picker/utils' import { Avatar } from '@/app/components/base/avatar' import Button from '@/app/components/base/button' import Divider from '@/app/components/base/divider' import { useLocalFileUploader } from '@/app/components/base/image-uploader/hooks' -import Modal from '@/app/components/base/modal' -import { ToastContext } from '@/app/components/base/toast/context' +import { Dialog, DialogContent } from '@/app/components/base/ui/dialog' +import { toast } from '@/app/components/base/ui/toast' import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config' import { updateUserProfile } from '@/service/common' @@ -25,7 +24,6 @@ type AvatarWithEditProps = AvatarProps & { onSave?: () => void } const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => { const { t } = useTranslation() - const { notify } = useContext(ToastContext) const [inputImageInfo, setInputImageInfo] = useState() const [isShowAvatarPicker, setIsShowAvatarPicker] = useState(false) @@ -48,24 +46,24 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => { await updateUserProfile({ url: 'account/avatar', body: { avatar: uploadedFileId } }) setIsShowAvatarPicker(false) onSave?.() - notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) } catch (e) { - notify({ type: 'error', message: (e as Error).message }) + toast.error((e as Error).message) } - }, [notify, onSave, t]) + }, [onSave, t]) const handleDeleteAvatar = useCallback(async () => { try { await updateUserProfile({ url: 'account/avatar', body: { avatar: '' } }) - notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) setIsShowDeleteConfirm(false) onSave?.() } catch (e) { - notify({ type: 'error', message: (e as Error).message }) + toast.error((e as Error).message) } - }, [notify, onSave, t]) + }, [onSave, t]) const { handleLocalFileUpload } = useLocalFileUploader({ limit: 3, @@ -134,45 +132,39 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => { - setIsShowAvatarPicker(false)} - > - - + !open && setIsShowAvatarPicker(false)}> + + + -
- +
+ - -
- + +
+
+
- setIsShowDeleteConfirm(false)} - > -
{t('avatar.deleteTitle', { ns: 'common' })}
-

{t('avatar.deleteDescription', { ns: 'common' })}

+ !open && setIsShowDeleteConfirm(false)}> + +
{t('avatar.deleteTitle', { ns: 'common' })}
+

{t('avatar.deleteDescription', { ns: 'common' })}

-
- +
+ - -
- + +
+
+
) } diff --git a/web/app/account/(commonLayout)/account-page/email-change-modal.tsx b/web/app/account/(commonLayout)/account-page/email-change-modal.tsx index f0dfd4f12f..2e2d61f2f9 100644 --- a/web/app/account/(commonLayout)/account-page/email-change-modal.tsx +++ b/web/app/account/(commonLayout)/account-page/email-change-modal.tsx @@ -1,14 +1,12 @@ import type { ResponseError } from '@/service/fetch' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'es-toolkit/function' import * as React from 'react' import { useState } from 'react' import { Trans, useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' -import Modal from '@/app/components/base/modal' -import { ToastContext } from '@/app/components/base/toast/context' +import { Dialog, DialogContent } from '@/app/components/base/ui/dialog' +import { toast } from '@/app/components/base/ui/toast' import { useRouter } from '@/next/navigation' import { checkEmailExisted, @@ -34,7 +32,6 @@ enum STEP { const EmailChangeModal = ({ onClose, email, show }: Props) => { const { t } = useTranslation() - const { notify } = useContext(ToastContext) const router = useRouter() const [step, setStep] = useState(STEP.start) const [code, setCode] = useState('') @@ -70,10 +67,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { setStepToken(res.data) } catch (error) { - notify({ - type: 'error', - message: `Error sending verification code: ${error ? (error as any).message : ''}`, - }) + toast.error(`Error sending verification code: ${error ? (error as any).message : ''}`) } } @@ -89,17 +83,11 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { callback?.(res.token) } else { - notify({ - type: 'error', - message: 'Verifying email failed', - }) + toast.error('Verifying email failed') } } catch (error) { - notify({ - type: 'error', - message: `Error verifying email: ${error ? (error as any).message : ''}`, - }) + toast.error(`Error verifying email: ${error ? (error as any).message : ''}`) } } @@ -154,10 +142,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { const sendCodeToNewEmail = async () => { if (!isValidEmail(mail)) { - notify({ - type: 'error', - message: 'Invalid email format', - }) + toast.error('Invalid email format') return } await sendEmail( @@ -187,10 +172,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { handleLogout() } catch (error) { - notify({ - type: 'error', - message: `Error changing email: ${error ? (error as any).message : ''}`, - }) + toast.error(`Error changing email: ${error ? (error as any).message : ''}`) } } @@ -199,187 +181,185 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { } return ( - -
- -
- {step === STEP.start && ( - <> -
{t('account.changeEmail.title', { ns: 'common' })}
-
-
{t('account.changeEmail.authTip', { ns: 'common' })}
-
- }} - values={{ email }} + !open && onClose()}> + +
+ +
+ {step === STEP.start && ( + <> +
{t('account.changeEmail.title', { ns: 'common' })}
+
+
{t('account.changeEmail.authTip', { ns: 'common' })}
+
+ }} + values={{ email }} + /> +
+
+
+
+ + +
+ + )} + {step === STEP.verifyOrigin && ( + <> +
{t('account.changeEmail.verifyEmail', { ns: 'common' })}
+
+
+ }} + values={{ email }} + /> +
+
+
+
{t('account.changeEmail.codeLabel', { ns: 'common' })}
+ setCode(e.target.value)} + maxLength={6} />
-
-
-
- - -
- - )} - {step === STEP.verifyOrigin && ( - <> -
{t('account.changeEmail.verifyEmail', { ns: 'common' })}
-
-
- }} - values={{ email }} +
+ + +
+
+ {t('account.changeEmail.resendTip', { ns: 'common' })} + {time > 0 && ( + {t('account.changeEmail.resendCount', { ns: 'common', count: time })} + )} + {!time && ( + {t('account.changeEmail.resend', { ns: 'common' })} + )} +
+ + )} + {step === STEP.newEmail && ( + <> +
{t('account.changeEmail.newEmail', { ns: 'common' })}
+
+
{t('account.changeEmail.content3', { ns: 'common' })}
+
+
+
{t('account.changeEmail.emailLabel', { ns: 'common' })}
+ handleNewEmailValueChange(e.target.value)} + destructive={newEmailExited || unAvailableEmail} + /> + {newEmailExited && ( +
{t('account.changeEmail.existingEmail', { ns: 'common' })}
+ )} + {unAvailableEmail && ( +
{t('account.changeEmail.unAvailableEmail', { ns: 'common' })}
+ )} +
+
+ + +
+ + )} + {step === STEP.verifyNew && ( + <> +
{t('account.changeEmail.verifyNew', { ns: 'common' })}
+
+
+ }} + values={{ email: mail }} + /> +
+
+
+
{t('account.changeEmail.codeLabel', { ns: 'common' })}
+ setCode(e.target.value)} + maxLength={6} />
-
-
-
{t('account.changeEmail.codeLabel', { ns: 'common' })}
- setCode(e.target.value)} - maxLength={6} - /> -
-
- - -
-
- {t('account.changeEmail.resendTip', { ns: 'common' })} - {time > 0 && ( - {t('account.changeEmail.resendCount', { ns: 'common', count: time })} - )} - {!time && ( - {t('account.changeEmail.resend', { ns: 'common' })} - )} -
- - )} - {step === STEP.newEmail && ( - <> -
{t('account.changeEmail.newEmail', { ns: 'common' })}
-
-
{t('account.changeEmail.content3', { ns: 'common' })}
-
-
-
{t('account.changeEmail.emailLabel', { ns: 'common' })}
- handleNewEmailValueChange(e.target.value)} - destructive={newEmailExited || unAvailableEmail} - /> - {newEmailExited && ( -
{t('account.changeEmail.existingEmail', { ns: 'common' })}
- )} - {unAvailableEmail && ( -
{t('account.changeEmail.unAvailableEmail', { ns: 'common' })}
- )} -
-
- - -
- - )} - {step === STEP.verifyNew && ( - <> -
{t('account.changeEmail.verifyNew', { ns: 'common' })}
-
-
- }} - values={{ email: mail }} - /> +
+ +
-
-
-
{t('account.changeEmail.codeLabel', { ns: 'common' })}
- setCode(e.target.value)} - maxLength={6} - /> -
-
- - -
-
- {t('account.changeEmail.resendTip', { ns: 'common' })} - {time > 0 && ( - {t('account.changeEmail.resendCount', { ns: 'common', count: time })} - )} - {!time && ( - {t('account.changeEmail.resend', { ns: 'common' })} - )} -
- - )} - +
+ {t('account.changeEmail.resendTip', { ns: 'common' })} + {time > 0 && ( + {t('account.changeEmail.resendCount', { ns: 'common', count: time })} + )} + {!time && ( + {t('account.changeEmail.resend', { ns: 'common' })} + )} +
+ + )} + + ) } diff --git a/web/app/account/(commonLayout)/account-page/index.tsx b/web/app/account/(commonLayout)/account-page/index.tsx index 9a104619da..7b4a148530 100644 --- a/web/app/account/(commonLayout)/account-page/index.tsx +++ b/web/app/account/(commonLayout)/account-page/index.tsx @@ -7,13 +7,12 @@ import { import { useQueryClient } from '@tanstack/react-query' import { useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import AppIcon from '@/app/components/base/app-icon' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' -import Modal from '@/app/components/base/modal' import PremiumBadge from '@/app/components/base/premium-badge' -import { ToastContext } from '@/app/components/base/toast/context' +import { Dialog, DialogContent } from '@/app/components/base/ui/dialog' +import { toast } from '@/app/components/base/ui/toast' import Collapse from '@/app/components/header/account-setting/collapse' import { IS_CE_EDITION, validPassword } from '@/config' import { useGlobalPublicStore } from '@/context/global-public-context' @@ -43,7 +42,6 @@ export default function AccountPage() { const userProfile = userProfileResp?.profile const mutateUserProfile = () => queryClient.invalidateQueries({ queryKey: commonQueryKeys.userProfile }) const { isEducationAccount } = useProviderContext() - const { notify } = useContext(ToastContext) const [editNameModalVisible, setEditNameModalVisible] = useState(false) const [editName, setEditName] = useState('') const [editing, setEditing] = useState(false) @@ -68,22 +66,19 @@ export default function AccountPage() { try { setEditing(true) await updateUserProfile({ url: 'account/name', body: { name: editName } }) - notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) mutateUserProfile() setEditNameModalVisible(false) setEditing(false) } catch (e) { - notify({ type: 'error', message: (e as Error).message }) + toast.error((e as Error).message) setEditing(false) } } const showErrorMessage = (message: string) => { - notify({ - type: 'error', - message, - }) + toast.error(message) } const valid = () => { if (!password.trim()) { @@ -119,14 +114,14 @@ export default function AccountPage() { repeat_new_password: confirmPassword, }, }) - notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) mutateUserProfile() setEditPasswordModalVisible(false) resetPasswordForm() setEditing(false) } catch (e) { - notify({ type: 'error', message: (e as Error).message }) + toast.error((e as Error).message) setEditPasswordModalVisible(false) setEditing(false) } @@ -221,119 +216,112 @@ export default function AccountPage() {
{ editNameModalVisible && ( - setEditNameModalVisible(false)} - className="!w-[420px] !p-6" - > -
{t('account.editName', { ns: 'common' })}
-
{t('account.name', { ns: 'common' })}
- setEditName(e.target.value)} - /> -
- - -
-
+ !open && setEditNameModalVisible(false)}> + +
{t('account.editName', { ns: 'common' })}
+
{t('account.name', { ns: 'common' })}
+ setEditName(e.target.value)} + /> +
+ + +
+
+
) } { editPasswordModalVisible && ( - { - setEditPasswordModalVisible(false) - resetPasswordForm() - }} - className="!w-[420px] !p-6" - > -
{userProfile.is_password_set ? t('account.resetPassword', { ns: 'common' }) : t('account.setPassword', { ns: 'common' })}
- {userProfile.is_password_set && ( - <> -
{t('account.currentPassword', { ns: 'common' })}
-
- setCurrentPassword(e.target.value)} - /> + !open && (setEditPasswordModalVisible(false), resetPasswordForm())}> + +
{userProfile.is_password_set ? t('account.resetPassword', { ns: 'common' }) : t('account.setPassword', { ns: 'common' })}
+ {userProfile.is_password_set && ( + <> +
{t('account.currentPassword', { ns: 'common' })}
+
+ setCurrentPassword(e.target.value)} + /> -
- +
+ +
+ + )} +
+ {userProfile.is_password_set ? t('account.newPassword', { ns: 'common' }) : t('account.password', { ns: 'common' })} +
+
+ setPassword(e.target.value)} + /> +
+
- - )} -
- {userProfile.is_password_set ? t('account.newPassword', { ns: 'common' }) : t('account.password', { ns: 'common' })} -
-
- setPassword(e.target.value)} - /> -
+
+
{t('account.confirmPassword', { ns: 'common' })}
+
+ setConfirmPassword(e.target.value)} + /> +
+ +
+
+
+
-
-
{t('account.confirmPassword', { ns: 'common' })}
-
- setConfirmPassword(e.target.value)} - /> -
- -
-
-
- - -
- + +
) } { diff --git a/web/app/account/(commonLayout)/delete-account/components/feed-back.tsx b/web/app/account/(commonLayout)/delete-account/components/feed-back.tsx index ae73d778f8..60bd7e5c0d 100644 --- a/web/app/account/(commonLayout)/delete-account/components/feed-back.tsx +++ b/web/app/account/(commonLayout)/delete-account/components/feed-back.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import CustomDialog from '@/app/components/base/dialog' import Textarea from '@/app/components/base/textarea' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { useAppContext } from '@/context/app-context' import { useRouter } from '@/next/navigation' import { useLogout } from '@/service/use-common' @@ -28,7 +28,7 @@ export default function FeedBack(props: DeleteAccountProps) { await logout() // Tokens are now stored in cookies and cleared by backend router.push('/signin') - Toast.notify({ type: 'info', message: t('account.deleteSuccessTip', { ns: 'common' }) }) + toast.info(t('account.deleteSuccessTip', { ns: 'common' })) } catch (error) { console.error(error) } }, [router, t]) diff --git a/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts b/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts index deea28ce3e..d5eaa4bfe4 100644 --- a/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts +++ b/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts @@ -2,7 +2,16 @@ import { act, renderHook } from '@testing-library/react' import { AppModeEnum } from '@/types/app' import { useAppInfoActions } from '../use-app-info-actions' -const mockNotify = vi.fn() +const toastMocks = vi.hoisted(() => { + const call = vi.fn() + return { + call, + api: vi.fn((message: unknown, options?: Record) => call({ message, ...options })), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), + } +}) const mockReplace = vi.fn() const mockOnPlanInfoChanged = vi.fn() const mockInvalidateAppList = vi.fn() @@ -27,10 +36,6 @@ vi.mock('@/next/navigation', () => ({ useRouter: () => ({ replace: mockReplace }), })) -vi.mock('use-context-selector', () => ({ - useContext: () => ({ notify: mockNotify }), -})) - vi.mock('@/context/provider-context', () => ({ useProviderContext: () => ({ onPlanInfoChanged: mockOnPlanInfoChanged }), })) @@ -42,8 +47,16 @@ vi.mock('@/app/components/app/store', () => ({ }), })) -vi.mock('@/app/components/base/toast/context', () => ({ - ToastContext: {}, +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: Object.assign(toastMocks.api, { + success: vi.fn((message, options) => toastMocks.call({ type: 'success', message, ...options })), + error: vi.fn((message, options) => toastMocks.call({ type: 'error', message, ...options })), + warning: vi.fn((message, options) => toastMocks.call({ type: 'warning', message, ...options })), + info: vi.fn((message, options) => toastMocks.call({ type: 'info', message, ...options })), + dismiss: toastMocks.dismiss, + update: toastMocks.update, + promise: toastMocks.promise, + }), })) vi.mock('@/service/use-apps', () => ({ @@ -175,7 +188,7 @@ describe('useAppInfoActions', () => { expect(mockUpdateAppInfo).toHaveBeenCalled() expect(mockSetAppDetail).toHaveBeenCalledWith(updatedApp) - expect(mockNotify).toHaveBeenCalledWith({ type: 'success', message: 'app.editDone' }) + expect(toastMocks.call).toHaveBeenCalledWith({ type: 'success', message: 'app.editDone' }) }) it('should notify error on edit failure', async () => { @@ -194,7 +207,7 @@ describe('useAppInfoActions', () => { }) }) - expect(mockNotify).toHaveBeenCalledWith({ type: 'error', message: 'app.editFailed' }) + expect(toastMocks.call).toHaveBeenCalledWith({ type: 'error', message: 'app.editFailed' }) }) it('should not call updateAppInfo when appDetail is undefined', async () => { @@ -234,7 +247,7 @@ describe('useAppInfoActions', () => { }) expect(mockCopyApp).toHaveBeenCalled() - expect(mockNotify).toHaveBeenCalledWith({ type: 'success', message: 'app.newApp.appCreated' }) + expect(toastMocks.call).toHaveBeenCalledWith({ type: 'success', message: 'app.newApp.appCreated' }) expect(mockOnPlanInfoChanged).toHaveBeenCalled() }) @@ -252,7 +265,7 @@ describe('useAppInfoActions', () => { }) }) - expect(mockNotify).toHaveBeenCalledWith({ type: 'error', message: 'app.newApp.appCreateFailed' }) + expect(toastMocks.call).toHaveBeenCalledWith({ type: 'error', message: 'app.newApp.appCreateFailed' }) }) }) @@ -298,7 +311,7 @@ describe('useAppInfoActions', () => { await result.current.onExport() }) - expect(mockNotify).toHaveBeenCalledWith({ type: 'error', message: 'app.exportFailed' }) + expect(toastMocks.call).toHaveBeenCalledWith({ type: 'error', message: 'app.exportFailed' }) }) }) @@ -410,7 +423,7 @@ describe('useAppInfoActions', () => { await result.current.handleConfirmExport() }) - expect(mockNotify).toHaveBeenCalledWith({ type: 'error', message: 'app.exportFailed' }) + expect(toastMocks.call).toHaveBeenCalledWith({ type: 'error', message: 'app.exportFailed' }) }) }) @@ -456,7 +469,7 @@ describe('useAppInfoActions', () => { }) expect(mockDeleteApp).toHaveBeenCalledWith('app-1') - expect(mockNotify).toHaveBeenCalledWith({ type: 'success', message: 'app.appDeleted' }) + expect(toastMocks.call).toHaveBeenCalledWith({ type: 'success', message: 'app.appDeleted' }) expect(mockInvalidateAppList).toHaveBeenCalled() expect(mockReplace).toHaveBeenCalledWith('/apps') expect(mockSetAppDetail).toHaveBeenCalledWith() @@ -483,7 +496,7 @@ describe('useAppInfoActions', () => { await result.current.onConfirmDelete() }) - expect(mockNotify).toHaveBeenCalledWith({ + expect(toastMocks.call).toHaveBeenCalledWith({ type: 'error', message: expect.stringContaining('app.appDeleteFailed'), }) diff --git a/web/app/components/app-sidebar/app-info/use-app-info-actions.ts b/web/app/components/app-sidebar/app-info/use-app-info-actions.ts index 55ec13e506..8b559f7bba 100644 --- a/web/app/components/app-sidebar/app-info/use-app-info-actions.ts +++ b/web/app/components/app-sidebar/app-info/use-app-info-actions.ts @@ -3,9 +3,8 @@ import type { CreateAppModalProps } from '@/app/components/explore/create-app-mo import type { EnvironmentVariable } from '@/app/components/workflow/types' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import { useStore as useAppStore } from '@/app/components/app/store' -import { ToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useProviderContext } from '@/context/provider-context' import { useRouter } from '@/next/navigation' @@ -24,7 +23,6 @@ type UseAppInfoActionsParams = { export function useAppInfoActions({ onDetailExpand }: UseAppInfoActionsParams) { const { t } = useTranslation() - const { notify } = useContext(ToastContext) const { replace } = useRouter() const { onPlanInfoChanged } = useProviderContext() const appDetail = useAppStore(state => state.appDetail) @@ -72,13 +70,13 @@ export function useAppInfoActions({ onDetailExpand }: UseAppInfoActionsParams) { max_active_requests, }) closeModal() - notify({ type: 'success', message: t('editDone', { ns: 'app' }) }) + toast(t('editDone', { ns: 'app' }), { type: 'success' }) setAppDetail(app) } catch { - notify({ type: 'error', message: t('editFailed', { ns: 'app' }) }) + toast(t('editFailed', { ns: 'app' }), { type: 'error' }) } - }, [appDetail, closeModal, notify, setAppDetail, t]) + }, [appDetail, closeModal, setAppDetail, t]) const onCopy: DuplicateAppModalProps['onConfirm'] = useCallback(async ({ name, @@ -98,15 +96,15 @@ export function useAppInfoActions({ onDetailExpand }: UseAppInfoActionsParams) { mode: appDetail.mode, }) closeModal() - notify({ type: 'success', message: t('newApp.appCreated', { ns: 'app' }) }) + toast(t('newApp.appCreated', { ns: 'app' }), { type: 'success' }) localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') onPlanInfoChanged() getRedirection(true, newApp, replace) } catch { - notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) }) + toast(t('newApp.appCreateFailed', { ns: 'app' }), { type: 'error' }) } - }, [appDetail, closeModal, notify, onPlanInfoChanged, replace, t]) + }, [appDetail, closeModal, onPlanInfoChanged, replace, t]) const onExport = useCallback(async (include = false) => { if (!appDetail) @@ -117,9 +115,9 @@ export function useAppInfoActions({ onDetailExpand }: UseAppInfoActionsParams) { downloadBlob({ data: file, fileName: `${appDetail.name}.yml` }) } catch { - notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) }) + toast(t('exportFailed', { ns: 'app' }), { type: 'error' }) } - }, [appDetail, notify, t]) + }, [appDetail, t]) const exportCheck = useCallback(async () => { if (!appDetail) @@ -145,29 +143,26 @@ export function useAppInfoActions({ onDetailExpand }: UseAppInfoActionsParams) { setSecretEnvList(list) } catch { - notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) }) + toast(t('exportFailed', { ns: 'app' }), { type: 'error' }) } - }, [appDetail, closeModal, notify, onExport, t]) + }, [appDetail, closeModal, onExport, t]) const onConfirmDelete = useCallback(async () => { if (!appDetail) return try { await deleteApp(appDetail.id) - notify({ type: 'success', message: t('appDeleted', { ns: 'app' }) }) + toast(t('appDeleted', { ns: 'app' }), { type: 'success' }) invalidateAppList() onPlanInfoChanged() setAppDetail() replace('/apps') } catch (e: unknown) { - notify({ - type: 'error', - message: `${t('appDeleteFailed', { ns: 'app' })}${e instanceof Error && e.message ? `: ${e.message}` : ''}`, - }) + toast(`${t('appDeleteFailed', { ns: 'app' })}${e instanceof Error && e.message ? `: ${e.message}` : ''}`, { type: 'error' }) } closeModal() - }, [appDetail, closeModal, invalidateAppList, notify, onPlanInfoChanged, replace, setAppDetail, t]) + }, [appDetail, closeModal, invalidateAppList, onPlanInfoChanged, replace, setAppDetail, t]) return { appDetail, diff --git a/web/app/components/app-sidebar/dataset-info/dropdown.tsx b/web/app/components/app-sidebar/dataset-info/dropdown.tsx index 528bac831f..1d1208e7d3 100644 --- a/web/app/components/app-sidebar/dataset-info/dropdown.tsx +++ b/web/app/components/app-sidebar/dataset-info/dropdown.tsx @@ -3,6 +3,7 @@ import { RiMoreFill } from '@remixicon/react' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/components/base/ui/toast' import { useSelector as useAppContextWithSelector } from '@/context/app-context' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { useRouter } from '@/next/navigation' @@ -15,7 +16,6 @@ import { downloadBlob } from '@/utils/download' import ActionButton from '../../base/action-button' import Confirm from '../../base/confirm' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../base/portal-to-follow-elem' -import Toast from '../../base/toast' import RenameDatasetModal from '../../datasets/rename-modal' import Menu from './menu' @@ -69,7 +69,7 @@ const DropDown = ({ downloadBlob({ data: file, fileName: `${name}.pipeline` }) } catch { - Toast.notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) }) + toast(t('exportFailed', { ns: 'app' }), { type: 'error' }) } }, [dataset, exportPipelineConfig, handleTrigger, t]) @@ -81,7 +81,7 @@ const DropDown = ({ } catch (e: any) { const res = await e.json() - Toast.notify({ type: 'error', message: res?.message || 'Unknown error' }) + toast(res?.message || 'Unknown error', { type: 'error' }) } finally { handleTrigger() @@ -91,7 +91,7 @@ const DropDown = ({ const onConfirmDelete = useCallback(async () => { try { await deleteDataset(dataset.id) - Toast.notify({ type: 'success', message: t('datasetDeleted', { ns: 'dataset' }) }) + toast(t('datasetDeleted', { ns: 'dataset' }), { type: 'success' }) invalidDatasetList() replace('/datasets') } diff --git a/web/app/components/app/annotation/add-annotation-modal/index.spec.tsx b/web/app/components/app/annotation/add-annotation-modal/index.spec.tsx index bad3ceefdf..14f94d910b 100644 --- a/web/app/components/app/annotation/add-annotation-modal/index.spec.tsx +++ b/web/app/components/app/annotation/add-annotation-modal/index.spec.tsx @@ -9,10 +9,16 @@ vi.mock('@/context/provider-context', () => ({ })) const mockToastNotify = vi.fn() -vi.mock('@/app/components/base/toast', () => ({ +vi.mock('@/app/components/base/ui/toast', () => ({ default: { notify: vi.fn(args => mockToastNotify(args)), }, + toast: { + success: (message: string) => mockToastNotify({ type: 'success', message }), + error: (message: string) => mockToastNotify({ type: 'error', message }), + warning: (message: string) => mockToastNotify({ type: 'warning', message }), + info: (message: string) => mockToastNotify({ type: 'info', message }), + }, })) vi.mock('@/app/components/billing/annotation-full', () => ({ diff --git a/web/app/components/app/annotation/add-annotation-modal/index.tsx b/web/app/components/app/annotation/add-annotation-modal/index.tsx index a3100d5131..d4cc943a57 100644 --- a/web/app/components/app/annotation/add-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/add-annotation-modal/index.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import Checkbox from '@/app/components/base/checkbox' import Drawer from '@/app/components/base/drawer-plus' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import AnnotationFull from '@/app/components/billing/annotation-full' import { useProviderContext } from '@/context/provider-context' import EditItem, { EditItemType } from './edit-item' @@ -47,10 +47,7 @@ const AddAnnotationModal: FC = ({ answer, } if (isValid(payload) !== true) { - Toast.notify({ - type: 'error', - message: isValid(payload) as string, - }) + toast.error(isValid(payload) as string) return } diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.spec.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.spec.tsx index 55f5ee0564..847db74619 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.spec.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.spec.tsx @@ -1,11 +1,28 @@ import type { Props } from './csv-uploader' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' -import { ToastContext } from '@/app/components/base/toast/context' import CSVUploader from './csv-uploader' +const toastMocks = vi.hoisted(() => ({ + notify: vi.fn(), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), +})) + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + success: (message: string, options?: Record) => toastMocks.notify({ type: 'success', message, ...options }), + error: (message: string, options?: Record) => toastMocks.notify({ type: 'error', message, ...options }), + warning: (message: string, options?: Record) => toastMocks.notify({ type: 'warning', message, ...options }), + info: (message: string, options?: Record) => toastMocks.notify({ type: 'info', message, ...options }), + dismiss: toastMocks.dismiss, + update: toastMocks.update, + promise: toastMocks.promise, + }, +})) + describe('CSVUploader', () => { - const notify = vi.fn() const updateFile = vi.fn() const getDropElements = () => { @@ -24,9 +41,8 @@ describe('CSVUploader', () => { ...props, } return render( - - - , + , + ) } @@ -76,7 +92,7 @@ describe('CSVUploader', () => { fireEvent.drop(dropContainer, { dataTransfer: { files: [fileA, fileB] } }) - await waitFor(() => expect(notify).toHaveBeenCalledWith({ + await waitFor(() => expect(toastMocks.notify).toHaveBeenCalledWith({ type: 'error', message: 'datasetCreation.stepOne.uploader.validation.count', })) diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx index a969b3d491..0fbd3974aa 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx @@ -4,10 +4,9 @@ import { RiDeleteBinLine } from '@remixicon/react' import * as React from 'react' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import Button from '@/app/components/base/button' import { Csv as CSVIcon } from '@/app/components/base/icons/src/public/files' -import { ToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import { cn } from '@/utils/classnames' export type Props = { @@ -20,7 +19,6 @@ const CSVUploader: FC = ({ updateFile, }) => { const { t } = useTranslation() - const { notify } = useContext(ToastContext) const [dragging, setDragging] = useState(false) const dropRef = useRef(null) const dragRef = useRef(null) @@ -50,7 +48,7 @@ const CSVUploader: FC = ({ return const files = Array.from(e.dataTransfer.files) if (files.length > 1) { - notify({ type: 'error', message: t('stepOne.uploader.validation.count', { ns: 'datasetCreation' }) }) + toast.error(t('stepOne.uploader.validation.count', { ns: 'datasetCreation' })) return } updateFile(files[0]) diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/index.spec.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/index.spec.tsx index 7fdb99fbab..8929cc292f 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/index.spec.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/index.spec.tsx @@ -2,17 +2,10 @@ import type { Mock } from 'vitest' import type { IBatchModalProps } from './index' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' -import Toast from '@/app/components/base/toast' import { useProviderContext } from '@/context/provider-context' import { annotationBatchImport, checkAnnotationBatchImportProgress } from '@/service/annotation' import BatchModal, { ProcessStatus } from './index' -vi.mock('@/app/components/base/toast', () => ({ - default: { - notify: vi.fn(), - }, -})) - vi.mock('@/service/annotation', () => ({ annotationBatchImport: vi.fn(), checkAnnotationBatchImportProgress: vi.fn(), @@ -49,7 +42,18 @@ vi.mock('@/app/components/billing/annotation-full', () => ({ default: () =>
, })) -const mockNotify = Toast.notify as Mock +const mockNotify = vi.fn() +vi.mock('@/app/components/base/ui/toast', () => ({ + default: { + notify: (args: unknown) => mockNotify(args), + }, + toast: { + success: (message: string) => mockNotify({ type: 'success', message }), + error: (message: string) => mockNotify({ type: 'error', message }), + warning: (message: string) => mockNotify({ type: 'warning', message }), + info: (message: string) => mockNotify({ type: 'info', message }), + }, +})) const useProviderContextMock = useProviderContext as Mock const annotationBatchImportMock = annotationBatchImport as Mock const checkAnnotationBatchImportProgressMock = checkAnnotationBatchImportProgress as Mock diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx index be1518b708..f6d9512d3d 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx @@ -7,7 +7,7 @@ import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import Modal from '@/app/components/base/modal' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import AnnotationFull from '@/app/components/billing/annotation-full' import { useProviderContext } from '@/context/provider-context' import { annotationBatchImport, checkAnnotationBatchImportProgress } from '@/service/annotation' @@ -46,7 +46,6 @@ const BatchModal: FC = ({ }, [isShow]) const [importStatus, setImportStatus] = useState() - const notify = Toast.notify const checkProcess = async (jobID: string) => { try { const res = await checkAnnotationBatchImportProgress({ jobID, appId }) @@ -54,15 +53,15 @@ const BatchModal: FC = ({ if (res.job_status === ProcessStatus.WAITING || res.job_status === ProcessStatus.PROCESSING) setTimeout(() => checkProcess(res.job_id), 2500) if (res.job_status === ProcessStatus.ERROR) - notify({ type: 'error', message: `${t('batchModal.runError', { ns: 'appAnnotation' })}` }) + toast.error(`${t('batchModal.runError', { ns: 'appAnnotation' })}`) if (res.job_status === ProcessStatus.COMPLETED) { - notify({ type: 'success', message: `${t('batchModal.completed', { ns: 'appAnnotation' })}` }) + toast.success(`${t('batchModal.completed', { ns: 'appAnnotation' })}`) onAdded() onCancel() } } catch (e: any) { - notify({ type: 'error', message: `${t('batchModal.runError', { ns: 'appAnnotation' })}${'message' in e ? `: ${e.message}` : ''}` }) + toast.error(`${t('batchModal.runError', { ns: 'appAnnotation' })}${'message' in e ? `: ${e.message}` : ''}`) } } @@ -78,7 +77,7 @@ const BatchModal: FC = ({ checkProcess(res.job_id) } catch (e: any) { - notify({ type: 'error', message: `${t('batchModal.runError', { ns: 'appAnnotation' })}${'message' in e ? `: ${e.message}` : ''}` }) + toast.error(`${t('batchModal.runError', { ns: 'appAnnotation' })}${'message' in e ? `: ${e.message}` : ''}`) } } diff --git a/web/app/components/app/annotation/edit-annotation-modal/index.spec.tsx b/web/app/components/app/annotation/edit-annotation-modal/index.spec.tsx index 0bbd1ab67d..8f6dec42cf 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/index.spec.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/index.spec.tsx @@ -1,7 +1,6 @@ -import type { IToastProps, ToastHandle } from '@/app/components/base/toast' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import EditAnnotationModal from './index' const { mockAddAnnotation, mockEditAnnotation } = vi.hoisted(() => ({ @@ -37,10 +36,8 @@ vi.mock('@/app/components/billing/annotation-full', () => ({ default: () =>
, })) -type ToastNotifyProps = Pick -type ToastWithNotify = typeof Toast & { notify: (props: ToastNotifyProps) => ToastHandle } -const toastWithNotify = Toast as unknown as ToastWithNotify -const toastNotifySpy = vi.spyOn(toastWithNotify, 'notify').mockReturnValue({ clear: vi.fn() }) +const toastSuccessSpy = vi.spyOn(toast, 'success').mockReturnValue('toast-success') +const toastErrorSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error') describe('EditAnnotationModal', () => { const defaultProps = { @@ -55,7 +52,8 @@ describe('EditAnnotationModal', () => { } afterAll(() => { - toastNotifySpy.mockRestore() + toastSuccessSpy.mockRestore() + toastErrorSpy.mockRestore() }) beforeEach(() => { @@ -437,10 +435,7 @@ describe('EditAnnotationModal', () => { // Assert await waitFor(() => { - expect(toastNotifySpy).toHaveBeenCalledWith({ - message: 'API Error', - type: 'error', - }) + expect(toastErrorSpy).toHaveBeenCalledWith('API Error') }) expect(mockOnAdded).not.toHaveBeenCalled() @@ -475,10 +470,7 @@ describe('EditAnnotationModal', () => { // Assert await waitFor(() => { - expect(toastNotifySpy).toHaveBeenCalledWith({ - message: 'common.api.actionFailed', - type: 'error', - }) + expect(toastErrorSpy).toHaveBeenCalledWith('common.api.actionFailed') }) expect(mockOnAdded).not.toHaveBeenCalled() @@ -517,10 +509,7 @@ describe('EditAnnotationModal', () => { // Assert await waitFor(() => { - expect(toastNotifySpy).toHaveBeenCalledWith({ - message: 'API Error', - type: 'error', - }) + expect(toastErrorSpy).toHaveBeenCalledWith('API Error') }) expect(mockOnEdited).not.toHaveBeenCalled() @@ -557,10 +546,7 @@ describe('EditAnnotationModal', () => { // Assert await waitFor(() => { - expect(toastNotifySpy).toHaveBeenCalledWith({ - message: 'common.api.actionFailed', - type: 'error', - }) + expect(toastErrorSpy).toHaveBeenCalledWith('common.api.actionFailed') }) expect(mockOnEdited).not.toHaveBeenCalled() @@ -641,10 +627,7 @@ describe('EditAnnotationModal', () => { // Assert await waitFor(() => { - expect(toastNotifySpy).toHaveBeenCalledWith({ - message: 'common.api.actionSuccess', - type: 'success', - }) + expect(toastSuccessSpy).toHaveBeenCalledWith('common.api.actionSuccess') }) }) }) diff --git a/web/app/components/app/annotation/edit-annotation-modal/index.tsx b/web/app/components/app/annotation/edit-annotation-modal/index.tsx index 2595ec38b2..c0e60b65dc 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/index.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next' import Confirm from '@/app/components/base/confirm' import Drawer from '@/app/components/base/drawer-plus' import { MessageCheckRemove } from '@/app/components/base/icons/src/vender/line/communication' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import AnnotationFull from '@/app/components/billing/annotation-full' import { useProviderContext } from '@/context/provider-context' import useTimestamp from '@/hooks/use-timestamp' @@ -72,18 +72,12 @@ const EditAnnotationModal: FC = ({ onAdded(res.id, res.account?.name ?? '', postQuery, postAnswer) } - Toast.notify({ - message: t('api.actionSuccess', { ns: 'common' }) as string, - type: 'success', - }) + toast.success(t('api.actionSuccess', { ns: 'common' }) as string) } catch (error) { const fallbackMessage = t('api.actionFailed', { ns: 'common' }) as string const message = error instanceof Error && error.message ? error.message : fallbackMessage - Toast.notify({ - message, - type: 'error', - }) + toast.error(message) // Re-throw to preserve edit mode behavior for UI components throw error } diff --git a/web/app/components/app/annotation/index.spec.tsx b/web/app/components/app/annotation/index.spec.tsx index d62b60d33d..5f5e9f74c0 100644 --- a/web/app/components/app/annotation/index.spec.tsx +++ b/web/app/components/app/annotation/index.spec.tsx @@ -3,7 +3,7 @@ import type { AnnotationItem } from './type' import type { App } from '@/types/app' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { useProviderContext } from '@/context/provider-context' import { addAnnotation, @@ -17,10 +17,6 @@ import { AppModeEnum } from '@/types/app' import Annotation from './index' import { JobStatus } from './type' -vi.mock('@/app/components/base/toast', () => ({ - default: { notify: vi.fn() }, -})) - vi.mock('ahooks', () => ({ useDebounce: (value: any) => value, })) @@ -95,7 +91,23 @@ vi.mock('./view-annotation-modal', () => ({ vi.mock('@/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal', () => ({ default: (props: any) => props.isShow ?
: null })) vi.mock('@/app/components/billing/annotation-full/modal', () => ({ default: (props: any) => props.show ?
: null })) -const mockNotify = Toast.notify as Mock +const mockNotify = vi.fn() +vi.spyOn(toast, 'success').mockImplementation((message, options) => { + mockNotify({ type: 'success', message, ...options }) + return 'toast-success-id' +}) +vi.spyOn(toast, 'error').mockImplementation((message, options) => { + mockNotify({ type: 'error', message, ...options }) + return 'toast-error-id' +}) +vi.spyOn(toast, 'warning').mockImplementation((message, options) => { + mockNotify({ type: 'warning', message, ...options }) + return 'toast-warning-id' +}) +vi.spyOn(toast, 'info').mockImplementation((message, options) => { + mockNotify({ type: 'info', message, ...options }) + return 'toast-info-id' +}) const addAnnotationMock = addAnnotation as Mock const delAnnotationMock = delAnnotation as Mock const delAnnotationsMock = delAnnotations as Mock diff --git a/web/app/components/app/annotation/index.tsx b/web/app/components/app/annotation/index.tsx index ee276603cc..0ea25744ff 100644 --- a/web/app/components/app/annotation/index.tsx +++ b/web/app/components/app/annotation/index.tsx @@ -15,6 +15,7 @@ import { MessageFast } from '@/app/components/base/icons/src/vender/solid/commun import Loading from '@/app/components/base/loading' import Pagination from '@/app/components/base/pagination' import Switch from '@/app/components/base/switch' +import { toast } from '@/app/components/base/ui/toast' import AnnotationFullModal from '@/app/components/billing/annotation-full/modal' import { APP_PAGE_LIMIT } from '@/config' import { useProviderContext } from '@/context/provider-context' @@ -22,7 +23,6 @@ import { addAnnotation, delAnnotation, delAnnotations, fetchAnnotationConfig as import { AppModeEnum } from '@/types/app' import { sleep } from '@/utils' import { cn } from '@/utils/classnames' -import Toast from '../../base/toast' import EmptyElement from './empty-element' import Filter from './filter' import HeaderOpts from './header-opts' @@ -98,14 +98,14 @@ const Annotation: FC = (props) => { const handleAdd = async (payload: AnnotationItemBasic) => { await addAnnotation(appDetail.id, payload) - Toast.notify({ message: t('api.actionSuccess', { ns: 'common' }), type: 'success' }) + toast.success(t('api.actionSuccess', { ns: 'common' })) fetchList() setControlUpdateList(Date.now()) } const handleRemove = async (id: string) => { await delAnnotation(appDetail.id, id) - Toast.notify({ message: t('api.actionSuccess', { ns: 'common' }), type: 'success' }) + toast.success(t('api.actionSuccess', { ns: 'common' })) fetchList() setControlUpdateList(Date.now()) } @@ -113,13 +113,13 @@ const Annotation: FC = (props) => { const handleBatchDelete = async () => { try { await delAnnotations(appDetail.id, selectedIds) - Toast.notify({ message: t('api.actionSuccess', { ns: 'common' }), type: 'success' }) + toast.success(t('api.actionSuccess', { ns: 'common' })) fetchList() setControlUpdateList(Date.now()) setSelectedIds([]) } catch (e: any) { - Toast.notify({ type: 'error', message: e.message || t('api.actionFailed', { ns: 'common' }) }) + toast.error(e.message || t('api.actionFailed', { ns: 'common' })) } } @@ -132,7 +132,7 @@ const Annotation: FC = (props) => { if (!currItem) return await editAnnotation(appDetail.id, currItem.id, { question, answer }) - Toast.notify({ message: t('api.actionSuccess', { ns: 'common' }), type: 'success' }) + toast.success(t('api.actionSuccess', { ns: 'common' })) fetchList() setControlUpdateList(Date.now()) } @@ -170,10 +170,7 @@ const Annotation: FC = (props) => { const { job_id: jobId }: any = await updateAnnotationStatus(appDetail.id, AnnotationEnableStatus.disable, annotationConfig?.embedding_model, annotationConfig?.score_threshold) await ensureJobCompleted(jobId, AnnotationEnableStatus.disable) await fetchAnnotationConfig() - Toast.notify({ - message: t('api.actionSuccess', { ns: 'common' }), - type: 'success', - }) + toast.success(t('api.actionSuccess', { ns: 'common' })) } }} > @@ -263,10 +260,7 @@ const Annotation: FC = (props) => { await updateAnnotationScore(appDetail.id, annotationId, score) await fetchAnnotationConfig() - Toast.notify({ - message: t('api.actionSuccess', { ns: 'common' }), - type: 'success', - }) + toast.success(t('api.actionSuccess', { ns: 'common' })) setIsShowEdit(false) }} annotationConfig={annotationConfig!} diff --git a/web/app/components/app/app-access-control/access-control.spec.tsx b/web/app/components/app/app-access-control/access-control.spec.tsx index 3a5f2272ed..7411676586 100644 --- a/web/app/components/app/app-access-control/access-control.spec.tsx +++ b/web/app/components/app/app-access-control/access-control.spec.tsx @@ -2,9 +2,9 @@ import type { AccessControlAccount, AccessControlGroup, Subject } from '@/models import type { App } from '@/types/app' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { toast } from '@/app/components/base/ui/toast' import useAccessControlStore from '@/context/access-control-store' import { AccessMode, SubjectType } from '@/models/access-control' -import Toast from '../../base/toast' import AccessControlDialog from './access-control-dialog' import AccessControlItem from './access-control-item' import AddMemberOrGroupDialog from './add-member-or-group-pop' @@ -303,7 +303,7 @@ describe('AccessControl', () => { it('should initialize menu from app and call update on confirm', async () => { const onClose = vi.fn() const onConfirm = vi.fn() - const toastSpy = vi.spyOn(Toast, 'notify').mockReturnValue({}) + const toastSpy = vi.spyOn(toast, 'success').mockReturnValue('toast-success') useAccessControlStore.setState({ specificGroups: [baseGroup], specificMembers: [baseMember], @@ -336,7 +336,7 @@ describe('AccessControl', () => { { subjectId: baseMember.id, subjectType: SubjectType.ACCOUNT }, ], }) - expect(toastSpy).toHaveBeenCalled() + expect(toastSpy).toHaveBeenCalledWith('app.accessControlDialog.updateSuccess') expect(onConfirm).toHaveBeenCalled() }) }) diff --git a/web/app/components/app/app-access-control/index.tsx b/web/app/components/app/app-access-control/index.tsx index 8d46e41a11..0c1c64eadc 100644 --- a/web/app/components/app/app-access-control/index.tsx +++ b/web/app/components/app/app-access-control/index.tsx @@ -5,12 +5,12 @@ import { Description as DialogDescription, DialogTitle } from '@headlessui/react import { RiBuildingLine, RiGlobalLine, RiVerifiedBadgeLine } from '@remixicon/react' import { useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/components/base/ui/toast' import { useGlobalPublicStore } from '@/context/global-public-context' import { AccessMode, SubjectType } from '@/models/access-control' import { useUpdateAccessMode } from '@/service/access-control' import useAccessControlStore from '../../../../context/access-control-store' import Button from '../../base/button' -import Toast from '../../base/toast' import AccessControlDialog from './access-control-dialog' import AccessControlItem from './access-control-item' import SpecificGroupsOrMembers, { WebAppSSONotEnabledTip } from './specific-groups-or-members' @@ -61,7 +61,7 @@ export default function AccessControl(props: AccessControlProps) { submitData.subjects = subjects } await updateAccessMode(submitData) - Toast.notify({ type: 'success', message: t('accessControlDialog.updateSuccess', { ns: 'app' }) }) + toast.success(t('accessControlDialog.updateSuccess', { ns: 'app' })) onConfirm?.() }, [updateAccessMode, app, specificGroups, specificMembers, t, onConfirm, currentMenu]) return ( diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index 74d6a19cc1..649b225b23 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -35,8 +35,8 @@ import { AppModeEnum } from '@/types/app' import { basePath } from '@/utils/var' import Divider from '../../base/divider' import Loading from '../../base/loading' -import Toast from '../../base/toast' import Tooltip from '../../base/tooltip' +import { toast } from '../../base/ui/toast' import ShortcutsName from '../../workflow/shortcuts-name' import { getKeyboardKeyCodeBySystem } from '../../workflow/utils' import AccessControl from '../app-access-control' @@ -219,7 +219,7 @@ const AppPublisher = ({ throw new Error('No app found in Explore') }, { onError: (err) => { - Toast.notify({ type: 'error', message: `${err.message || err}` }) + toast.error(`${err.message || err}`) }, }) }, [appDetail?.id, openAsyncWindow]) diff --git a/web/app/components/app/app-publisher/version-info-modal.tsx b/web/app/components/app/app-publisher/version-info-modal.tsx index ee896cf583..a1d6edcf04 100644 --- a/web/app/components/app/app-publisher/version-info-modal.tsx +++ b/web/app/components/app/app-publisher/version-info-modal.tsx @@ -5,7 +5,7 @@ import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Modal from '@/app/components/base/modal' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import Button from '../../base/button' import Input from '../../base/input' import Textarea from '../../base/textarea' @@ -35,10 +35,7 @@ const VersionInfoModal: FC = ({ const handlePublish = () => { if (title.length > TITLE_MAX_LENGTH) { setTitleError(true) - Toast.notify({ - type: 'error', - message: t('versionHistory.editField.titleLengthLimit', { ns: 'workflow', limit: TITLE_MAX_LENGTH }), - }) + toast.error(t('versionHistory.editField.titleLengthLimit', { ns: 'workflow', limit: TITLE_MAX_LENGTH })) return } else { @@ -48,10 +45,7 @@ const VersionInfoModal: FC = ({ if (releaseNotes.length > RELEASE_NOTES_MAX_LENGTH) { setReleaseNotesError(true) - Toast.notify({ - type: 'error', - message: t('versionHistory.editField.releaseNotesLengthLimit', { ns: 'workflow', limit: RELEASE_NOTES_MAX_LENGTH }), - }) + toast.error(t('versionHistory.editField.releaseNotesLengthLimit', { ns: 'workflow', limit: RELEASE_NOTES_MAX_LENGTH })) return } else { diff --git a/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx b/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx index 9625204d81..482f61bb82 100644 --- a/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx +++ b/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx @@ -20,8 +20,12 @@ import { } from '@/app/components/base/icons/src/vender/line/files' import PromptEditor from '@/app/components/base/prompt-editor' import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '@/app/components/base/prompt-editor/plugins/variable-block' -import { useToastContext } from '@/app/components/base/toast/context' -import Tooltip from '@/app/components/base/tooltip' +import { toast } from '@/app/components/base/ui/toast' +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/app/components/base/ui/tooltip' import ConfigContext from '@/context/debug-configuration' import { useEventEmitterContextContext } from '@/context/event-emitter' import { useModalContext } from '@/context/modal-context' @@ -74,7 +78,6 @@ const AdvancedPromptInput: FC = ({ showSelectDataSet, externalDataToolsConfig, } = useContext(ConfigContext) - const { notify } = useToastContext() const { setShowExternalDataToolModal } = useModalContext() const handleOpenExternalDataToolModal = () => { setShowExternalDataToolModal({ @@ -94,7 +97,7 @@ const AdvancedPromptInput: FC = ({ onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => { for (let i = 0; i < promptVariables.length; i++) { if (promptVariables[i].key === newExternalDataTool.variable) { - notify({ type: 'error', message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }) }) + toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key })) return false } } @@ -180,13 +183,18 @@ const AdvancedPromptInput: FC = ({
{t('pageTitle.line1', { ns: 'appDebug' })}
- + + )} + /> +
{t('promptTip', { ns: 'appDebug' })}
- )} - /> +
+
)}
diff --git a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx index 39a1699063..bc54e0f16d 100644 --- a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx +++ b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx @@ -17,8 +17,12 @@ import { useFeaturesStore } from '@/app/components/base/features/hooks' import PromptEditor from '@/app/components/base/prompt-editor' import { PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER } from '@/app/components/base/prompt-editor/plugins/update-block' import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '@/app/components/base/prompt-editor/plugins/variable-block' -import { useToastContext } from '@/app/components/base/toast/context' -import Tooltip from '@/app/components/base/tooltip' +import { toast } from '@/app/components/base/ui/toast' +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/app/components/base/ui/tooltip' import ConfigContext from '@/context/debug-configuration' import { useEventEmitterContextContext } from '@/context/event-emitter' import { useModalContext } from '@/context/modal-context' @@ -72,7 +76,6 @@ const Prompt: FC = ({ showSelectDataSet, externalDataToolsConfig, } = useContext(ConfigContext) - const { notify } = useToastContext() const { setShowExternalDataToolModal } = useModalContext() const handleOpenExternalDataToolModal = () => { setShowExternalDataToolModal({ @@ -92,7 +95,7 @@ const Prompt: FC = ({ onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => { for (let i = 0; i < promptVariables.length; i++) { if (promptVariables[i].key === newExternalDataTool.variable) { - notify({ type: 'error', message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }) }) + toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key })) return false } } @@ -180,13 +183,18 @@ const Prompt: FC = ({
{mode !== AppModeEnum.COMPLETION ? t('chatSubTitle', { ns: 'appDebug' }) : t('completionSubTitle', { ns: 'appDebug' })}
{!readonly && ( - + + )} + /> +
{t('promptTip', { ns: 'appDebug' })}
- )} - /> +
+
)}
diff --git a/web/app/components/app/configuration/config-var/config-modal/index.tsx b/web/app/components/app/configuration/config-var/config-modal/index.tsx index 7ea784baa3..b864206b26 100644 --- a/web/app/components/app/configuration/config-var/config-modal/index.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/index.tsx @@ -15,7 +15,7 @@ import Input from '@/app/components/base/input' import Modal from '@/app/components/base/modal' import { SimpleSelect } from '@/app/components/base/select' import Textarea from '@/app/components/base/textarea' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { DEFAULT_FILE_UPLOAD_SETTING } from '@/app/components/workflow/constants' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' import FileUploadSetting from '@/app/components/workflow/nodes/_base/components/file-upload-setting' @@ -98,10 +98,7 @@ const ConfigModal: FC = ({ const checkVariableName = useCallback((value: string, canBeEmpty?: boolean) => { const { isValid, errorMessageKey } = checkKeys([value], canBeEmpty) if (!isValid) { - Toast.notify({ - type: 'error', - message: t(`varKeyError.${errorMessageKey}`, { ns: 'appDebug', key: t('variableConfig.varName', { ns: 'appDebug' }) }), - }) + toast.error(t(`varKeyError.${errorMessageKey}`, { ns: 'appDebug', key: t('variableConfig.varName', { ns: 'appDebug' }) })) return false } return true @@ -219,10 +216,7 @@ const ConfigModal: FC = ({ const value = e.target.value const { isValid, errorKey, errorMessageKey } = checkKeys([value], true) if (!isValid) { - Toast.notify({ - type: 'error', - message: t(`varKeyError.${errorMessageKey}`, { ns: 'appDebug', key: errorKey }), - }) + toast.error(t(`varKeyError.${errorMessageKey}`, { ns: 'appDebug', key: errorKey })) return } handlePayloadChange('variable')(e.target.value) @@ -264,7 +258,7 @@ const ConfigModal: FC = ({ return if (!tempPayload.label) { - Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.labelNameRequired', { ns: 'appDebug' }) }) + toast.error(t('variableConfig.errorMsg.labelNameRequired', { ns: 'appDebug' })) return } if (isStringInput || type === InputVarType.number) { @@ -272,7 +266,7 @@ const ConfigModal: FC = ({ } else if (type === InputVarType.select) { if (options?.length === 0) { - Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.atLeastOneOption', { ns: 'appDebug' }) }) + toast.error(t('variableConfig.errorMsg.atLeastOneOption', { ns: 'appDebug' })) return } const obj: Record = {} @@ -285,7 +279,7 @@ const ConfigModal: FC = ({ obj[o] = true }) if (hasRepeatedItem) { - Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.optionRepeat', { ns: 'appDebug' }) }) + toast.error(t('variableConfig.errorMsg.optionRepeat', { ns: 'appDebug' })) return } onConfirm(payloadToSave, moreInfo) @@ -293,12 +287,12 @@ const ConfigModal: FC = ({ else if ([InputVarType.singleFile, InputVarType.multiFiles].includes(type)) { if (tempPayload.allowed_file_types?.length === 0) { const errorMessages = t('errorMsg.fieldRequired', { ns: 'workflow', field: t('variableConfig.file.supportFileTypes', { ns: 'appDebug' }) }) - Toast.notify({ type: 'error', message: errorMessages }) + toast.error(errorMessages) return } if (tempPayload.allowed_file_types?.includes(SupportUploadFileTypes.custom) && !tempPayload.allowed_file_extensions?.length) { const errorMessages = t('errorMsg.fieldRequired', { ns: 'workflow', field: t('variableConfig.file.custom.name', { ns: 'appDebug' }) }) - Toast.notify({ type: 'error', message: errorMessages }) + toast.error(errorMessages) return } onConfirm(payloadToSave, moreInfo) @@ -308,12 +302,12 @@ const ConfigModal: FC = ({ try { const schema = JSON.parse(normalizedJsonSchema) if (schema?.type !== 'object') { - Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.jsonSchemaMustBeObject', { ns: 'appDebug' }) }) + toast.error(t('variableConfig.errorMsg.jsonSchemaMustBeObject', { ns: 'appDebug' })) return } } catch { - Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.jsonSchemaInvalid', { ns: 'appDebug' }) }) + toast.error(t('variableConfig.errorMsg.jsonSchemaInvalid', { ns: 'appDebug' })) return } } diff --git a/web/app/components/app/configuration/config-var/index.spec.tsx b/web/app/components/app/configuration/config-var/index.spec.tsx index 096358c805..a48d3233f5 100644 --- a/web/app/components/app/configuration/config-var/index.spec.tsx +++ b/web/app/components/app/configuration/config-var/index.spec.tsx @@ -5,13 +5,13 @@ import type { PromptVariable } from '@/models/debug' import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react' import * as React from 'react' import { vi } from 'vitest' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import DebugConfigurationContext from '@/context/debug-configuration' import { AppModeEnum } from '@/types/app' import ConfigVar, { ADD_EXTERNAL_DATA_TOOL } from './index' -const notifySpy = vi.spyOn(Toast, 'notify').mockImplementation(vi.fn()) +const toastErrorSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error') const setShowExternalDataToolModal = vi.fn() @@ -112,7 +112,7 @@ describe('ConfigVar', () => { latestSortableProps = null subscriptionCallback = null variableIndex = 0 - notifySpy.mockClear() + toastErrorSpy.mockClear() }) it('should show empty state when no variables exist', () => { @@ -152,7 +152,7 @@ describe('ConfigVar', () => { latestSortableProps = null subscriptionCallback = null variableIndex = 0 - notifySpy.mockClear() + toastErrorSpy.mockClear() }) it('should add a text variable when selecting the string option', async () => { @@ -218,7 +218,7 @@ describe('ConfigVar', () => { latestSortableProps = null subscriptionCallback = null variableIndex = 0 - notifySpy.mockClear() + toastErrorSpy.mockClear() }) it('should save updates when editing a basic variable', async () => { @@ -268,7 +268,7 @@ describe('ConfigVar', () => { fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' })) - expect(Toast.notify).toHaveBeenCalled() + expect(toastErrorSpy).toHaveBeenCalled() expect(onPromptVariablesChange).not.toHaveBeenCalled() }) @@ -294,7 +294,7 @@ describe('ConfigVar', () => { fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' })) - expect(Toast.notify).toHaveBeenCalled() + expect(toastErrorSpy).toHaveBeenCalled() expect(onPromptVariablesChange).not.toHaveBeenCalled() }) }) @@ -306,7 +306,7 @@ describe('ConfigVar', () => { latestSortableProps = null subscriptionCallback = null variableIndex = 0 - notifySpy.mockClear() + toastErrorSpy.mockClear() }) it('should remove variable directly when context confirmation is not required', () => { @@ -359,7 +359,7 @@ describe('ConfigVar', () => { latestSortableProps = null subscriptionCallback = null variableIndex = 0 - notifySpy.mockClear() + toastErrorSpy.mockClear() }) it('should append external data tool variables from event emitter', () => { diff --git a/web/app/components/app/configuration/config-var/index.tsx b/web/app/components/app/configuration/config-var/index.tsx index 4d9a4e480f..17f5e2efe5 100644 --- a/web/app/components/app/configuration/config-var/index.tsx +++ b/web/app/components/app/configuration/config-var/index.tsx @@ -12,8 +12,8 @@ import { useTranslation } from 'react-i18next' import { ReactSortable } from 'react-sortablejs' import { useContext } from 'use-context-selector' import Confirm from '@/app/components/base/confirm' -import Toast from '@/app/components/base/toast' import Tooltip from '@/app/components/base/tooltip' +import { toast } from '@/app/components/base/ui/toast' import { InputVarType } from '@/app/components/workflow/types' import ConfigContext from '@/context/debug-configuration' import { useEventEmitterContextContext } from '@/context/event-emitter' @@ -108,10 +108,7 @@ const ConfigVar: FC = ({ promptVariables, readonly, onPromptVar }) const duplicateError = getDuplicateError(newPromptVariables) if (duplicateError) { - Toast.notify({ - type: 'error', - message: t(duplicateError.errorMsgKey as I18nKeysByPrefix<'appDebug', 'duplicateError.'>, { ns: 'appDebug', key: t(duplicateError.typeName as I18nKeysByPrefix<'appDebug', 'duplicateError.'>, { ns: 'appDebug' }) }) as string, - }) + toast.error(t(duplicateError.errorMsgKey as I18nKeysByPrefix<'appDebug', 'duplicateError.'>, { ns: 'appDebug', key: t(duplicateError.typeName as I18nKeysByPrefix<'appDebug', 'duplicateError.'>, { ns: 'appDebug' }) }) as string) return false } @@ -161,7 +158,7 @@ const ConfigVar: FC = ({ promptVariables, readonly, onPromptVar onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => { for (let i = 0; i < promptVariables.length; i++) { if (promptVariables[i].key === newExternalDataTool.variable && i !== index) { - Toast.notify({ type: 'error', message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }) }) + toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key })) return false } } diff --git a/web/app/components/app/configuration/config/agent/prompt-editor.tsx b/web/app/components/app/configuration/config/agent/prompt-editor.tsx index f719d87261..e807c21518 100644 --- a/web/app/components/app/configuration/config/agent/prompt-editor.tsx +++ b/web/app/components/app/configuration/config/agent/prompt-editor.tsx @@ -12,7 +12,7 @@ import { CopyCheck, } from '@/app/components/base/icons/src/vender/line/files' import PromptEditor from '@/app/components/base/prompt-editor' -import { useToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import ConfigContext from '@/context/debug-configuration' import { useModalContext } from '@/context/modal-context' import { cn } from '@/utils/classnames' @@ -32,8 +32,6 @@ const Editor: FC = ({ }) => { const { t } = useTranslation() - const { notify } = useToastContext() - const [isCopied, setIsCopied] = React.useState(false) const { modelConfig, @@ -59,14 +57,14 @@ const Editor: FC = ({ onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => { for (let i = 0; i < promptVariables.length; i++) { if (promptVariables[i].key === newExternalDataTool.variable) { - notify({ type: 'error', message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }) }) + toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key })) return false } } for (let i = 0; i < externalDataToolsConfig.length; i++) { if (externalDataToolsConfig[i].variable === newExternalDataTool.variable) { - notify({ type: 'error', message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: externalDataToolsConfig[i].variable }) }) + toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: externalDataToolsConfig[i].variable })) return false } } diff --git a/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx b/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx index 8ad284bcfb..6c135fdee3 100644 --- a/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx +++ b/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx @@ -23,9 +23,9 @@ import Button from '@/app/components/base/button' import Confirm from '@/app/components/base/confirm' import { Generator } from '@/app/components/base/icons/src/vender/other' import Loading from '@/app/components/base/loading' - import Modal from '@/app/components/base/modal' -import Toast from '@/app/components/base/toast' + +import { toast } from '@/app/components/base/ui/toast' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' @@ -159,13 +159,10 @@ const GetAutomaticRes: FC = ({ const isValid = () => { if (instruction.trim() === '') { - Toast.notify({ - type: 'error', - message: t('errorMsg.fieldRequired', { - ns: 'common', - field: t('generate.instruction', { ns: 'appDebug' }), - }), - }) + toast.error(t('errorMsg.fieldRequired', { + ns: 'common', + field: t('generate.instruction', { ns: 'appDebug' }), + })) return false } return true @@ -242,10 +239,7 @@ const GetAutomaticRes: FC = ({ } as GenRes if (error) { hasError = true - Toast.notify({ - type: 'error', - message: error, - }) + toast.error(error) } } else { @@ -260,10 +254,7 @@ const GetAutomaticRes: FC = ({ apiRes = res if (error) { hasError = true - Toast.notify({ - type: 'error', - message: error, - }) + toast.error(error) } } if (!hasError) diff --git a/web/app/components/app/configuration/config/automatic/result.tsx b/web/app/components/app/configuration/config/automatic/result.tsx index ef82007e51..776d774bd8 100644 --- a/web/app/components/app/configuration/config/automatic/result.tsx +++ b/web/app/components/app/configuration/config/automatic/result.tsx @@ -6,7 +6,7 @@ import copy from 'copy-to-clipboard' import * as React from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import CodeEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/code-editor' import PromptRes from './prompt-res' import PromptResInWorkflow from './prompt-res-in-workflow' @@ -54,7 +54,7 @@ const Result: FC = ({ className="px-2" onClick={() => { copy(current.modified) - Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.copySuccessfully', { ns: 'common' })) }} > diff --git a/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx b/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx index a7bc7ab97b..6bdb59fa17 100644 --- a/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx +++ b/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx @@ -15,7 +15,7 @@ import Confirm from '@/app/components/base/confirm' import { Generator } from '@/app/components/base/icons/src/vender/other' import Loading from '@/app/components/base/loading' import Modal from '@/app/components/base/modal' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' @@ -97,13 +97,10 @@ export const GetCodeGeneratorResModal: FC = ( const isValid = () => { if (instruction.trim() === '') { - Toast.notify({ - type: 'error', - message: t('errorMsg.fieldRequired', { - ns: 'common', - field: t('code.instruction', { ns: 'appDebug' }), - }), - }) + toast.error(t('errorMsg.fieldRequired', { + ns: 'common', + field: t('code.instruction', { ns: 'appDebug' }), + })) return false } return true @@ -149,10 +146,7 @@ export const GetCodeGeneratorResModal: FC = ( res.modified = (res as any).code if (error) { - Toast.notify({ - type: 'error', - message: error, - }) + toast.error(error) } else { addVersion(res) diff --git a/web/app/components/app/configuration/dataset-config/params-config/config-content.spec.tsx b/web/app/components/app/configuration/dataset-config/params-config/config-content.spec.tsx index 2cd8418c65..8a53e9a8b0 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/config-content.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/config-content.spec.tsx @@ -5,7 +5,7 @@ import type { DatasetConfigs } from '@/models/debug' import type { RetrievalConfig } from '@/types/app' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { useCurrentProviderAndModel, useModelListAndDefaultModelAndCurrentProviderAndModel, @@ -46,7 +46,7 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () const mockedUseModelListAndDefaultModelAndCurrentProviderAndModel = useModelListAndDefaultModelAndCurrentProviderAndModel as MockedFunction const mockedUseCurrentProviderAndModel = useCurrentProviderAndModel as MockedFunction -let toastNotifySpy: MockInstance +let toastErrorSpy: MockInstance const baseRetrievalConfig: RetrievalConfig = { search_method: RETRIEVE_METHOD.semantic, @@ -172,7 +172,7 @@ const createDatasetConfigs = (overrides: Partial = {}): DatasetC describe('ConfigContent', () => { beforeEach(() => { vi.clearAllMocks() - toastNotifySpy = vi.spyOn(Toast, 'notify').mockImplementation(() => ({})) + toastErrorSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error') mockedUseModelListAndDefaultModelAndCurrentProviderAndModel.mockReturnValue({ modelList: [], defaultModel: undefined, @@ -186,7 +186,7 @@ describe('ConfigContent', () => { }) afterEach(() => { - toastNotifySpy.mockRestore() + toastErrorSpy.mockRestore() }) // State management @@ -331,10 +331,7 @@ describe('ConfigContent', () => { await user.click(screen.getByText('common.modelProvider.rerankModel.key')) // Assert - expect(toastNotifySpy).toHaveBeenCalledWith({ - type: 'error', - message: 'workflow.errorMsg.rerankModelRequired', - }) + expect(toastErrorSpy).toHaveBeenCalledWith('workflow.errorMsg.rerankModelRequired') expect(onChange).toHaveBeenCalledWith( expect.objectContaining({ reranking_mode: RerankingModeEnum.RerankingModel, @@ -373,10 +370,7 @@ describe('ConfigContent', () => { await user.click(screen.getByRole('switch')) // Assert - expect(toastNotifySpy).toHaveBeenCalledWith({ - type: 'error', - message: 'workflow.errorMsg.rerankModelRequired', - }) + expect(toastErrorSpy).toHaveBeenCalledWith('workflow.errorMsg.rerankModelRequired') expect(onChange).toHaveBeenCalledWith( expect.objectContaining({ reranking_enable: true, diff --git a/web/app/components/app/configuration/dataset-config/params-config/config-content.tsx b/web/app/components/app/configuration/dataset-config/params-config/config-content.tsx index 6dd03d217e..be0d1d9394 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/config-content.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/config-content.tsx @@ -15,8 +15,8 @@ import Divider from '@/app/components/base/divider' import ScoreThresholdItem from '@/app/components/base/param-item/score-threshold-item' import TopKItem from '@/app/components/base/param-item/top-k-item' import Switch from '@/app/components/base/switch' -import Toast from '@/app/components/base/toast' import Tooltip from '@/app/components/base/tooltip' +import { toast } from '@/app/components/base/ui/toast' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useCurrentProviderAndModel, useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' @@ -136,7 +136,7 @@ const ConfigContent: FC = ({ return if (mode === RerankingModeEnum.RerankingModel && !currentRerankModel) - Toast.notify({ type: 'error', message: t('errorMsg.rerankModelRequired', { ns: 'workflow' }) }) + toast.error(t('errorMsg.rerankModelRequired', { ns: 'workflow' })) onChange({ ...datasetConfigs, @@ -179,7 +179,7 @@ const ConfigContent: FC = ({ const handleManuallyToggleRerank = useCallback((enable: boolean) => { if (!currentRerankModel && enable) - Toast.notify({ type: 'error', message: t('errorMsg.rerankModelRequired', { ns: 'workflow' }) }) + toast.error(t('errorMsg.rerankModelRequired', { ns: 'workflow' })) onChange({ ...datasetConfigs, reranking_enable: enable, diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/index.spec.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/index.spec.tsx index ea70725ea8..7fdf9d0a23 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/index.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/index.spec.tsx @@ -3,7 +3,6 @@ import type { DataSet } from '@/models/datasets' import type { RetrievalConfig } from '@/types/app' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { ToastContext } from '@/app/components/base/toast/context' import { IndexingType } from '@/app/components/datasets/create/step-two' import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' @@ -13,7 +12,24 @@ import { useMembers } from '@/service/use-common' import { RETRIEVE_METHOD } from '@/types/app' import SettingsModal from './index' -const mockNotify = vi.fn() +const toastMocks = vi.hoisted(() => ({ + call: vi.fn(), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), +})) + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: Object.assign(toastMocks.call, { + success: vi.fn((message: string, options?: Record) => toastMocks.call({ type: 'success', message, ...options })), + error: vi.fn((message: string, options?: Record) => toastMocks.call({ type: 'error', message, ...options })), + warning: vi.fn((message: string, options?: Record) => toastMocks.call({ type: 'warning', message, ...options })), + info: vi.fn((message: string, options?: Record) => toastMocks.call({ type: 'info', message, ...options })), + dismiss: toastMocks.dismiss, + update: toastMocks.update, + promise: toastMocks.promise, + }), +})) const mockOnCancel = vi.fn() const mockOnSave = vi.fn() const mockSetShowAccountSettingModal = vi.fn() @@ -183,13 +199,12 @@ const createDataset = (overrides: Partial = {}, retrievalOverrides: Par const renderWithProviders = (dataset: DataSet) => { return render( - - - , + , + ) } @@ -378,7 +393,7 @@ describe('SettingsModal', () => { await user.click(screen.getByRole('button', { name: 'common.operation.save' })) // Assert - expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ + expect(toastMocks.call).toHaveBeenCalledWith(expect.objectContaining({ type: 'error', message: 'datasetSettings.form.nameError', })) @@ -402,7 +417,7 @@ describe('SettingsModal', () => { await user.click(screen.getByRole('button', { name: 'common.operation.save' })) // Assert - expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ + expect(toastMocks.call).toHaveBeenCalledWith(expect.objectContaining({ type: 'error', message: 'appDebug.datasetConfig.rerankModelRequired', })) @@ -444,7 +459,7 @@ describe('SettingsModal', () => { permission: DatasetPermission.allTeamMembers, }), })) - expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ + expect(toastMocks.call).toHaveBeenCalledWith(expect.objectContaining({ type: 'success', message: 'common.actionMsg.modifiedSuccessfully', })) @@ -528,7 +543,7 @@ describe('SettingsModal', () => { // Assert await waitFor(() => { - expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' })) + expect(toastMocks.call).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' })) }) }) }) diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx index bc534599de..8b2c4270cd 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' import Textarea from '@/app/components/base/textarea' -import { useToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import { isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model' import { IndexingType } from '@/app/components/datasets/create/step-two' import IndexMethod from '@/app/components/datasets/settings/index-method' @@ -51,7 +51,6 @@ const SettingsModal: FC = ({ const { data: rerankModelList } = useModelList(ModelTypeEnum.rerank) const { t } = useTranslation() const docLink = useDocLink() - const { notify } = useToastContext() const ref = useRef(null) const isExternal = currentDataset.provider === 'external' const { setShowAccountSettingModal } = useModalContext() @@ -96,7 +95,7 @@ const SettingsModal: FC = ({ if (loading) return if (!localeCurrentDataset.name?.trim()) { - notify({ type: 'error', message: t('form.nameError', { ns: 'datasetSettings' }) }) + toast.error(t('form.nameError', { ns: 'datasetSettings' })) return } if ( @@ -106,7 +105,7 @@ const SettingsModal: FC = ({ indexMethod, }) ) { - notify({ type: 'error', message: t('datasetConfig.rerankModelRequired', { ns: 'appDebug' }) }) + toast.error(t('datasetConfig.rerankModelRequired', { ns: 'appDebug' })) return } try { @@ -146,7 +145,7 @@ const SettingsModal: FC = ({ }) } await updateDatasetSetting(requestParams) - notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) onSave({ ...localeCurrentDataset, indexing_technique: indexMethod, @@ -154,7 +153,7 @@ const SettingsModal: FC = ({ }) } catch { - notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) }) + toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' })) } finally { setLoading(false) diff --git a/web/app/components/app/configuration/debug/debug-with-single-model/index.spec.tsx b/web/app/components/app/configuration/debug/debug-with-single-model/index.spec.tsx index a75516a43f..910a8fd2b5 100644 --- a/web/app/components/app/configuration/debug/debug-with-single-model/index.spec.tsx +++ b/web/app/components/app/configuration/debug/debug-with-single-model/index.spec.tsx @@ -386,13 +386,6 @@ vi.mock('@/context/event-emitter', () => ({ })), })) -// Mock toast context -vi.mock('@/app/components/base/toast/context', () => ({ - useToastContext: vi.fn(() => ({ - notify: vi.fn(), - })), -})) - // Mock hooks/use-timestamp vi.mock('@/hooks/use-timestamp', () => ({ default: vi.fn(() => ({ diff --git a/web/app/components/app/configuration/debug/index.spec.tsx b/web/app/components/app/configuration/debug/index.spec.tsx index e94695f1ef..61fe673079 100644 --- a/web/app/components/app/configuration/debug/index.spec.tsx +++ b/web/app/components/app/configuration/debug/index.spec.tsx @@ -1,7 +1,6 @@ import type { ComponentProps } from 'react' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' -import { ToastContext } from '@/app/components/base/toast/context' import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import ConfigContext from '@/context/debug-configuration' import { AppModeEnum, ModelModeType, TransferMethod } from '@/types/app' @@ -16,6 +15,10 @@ const mockState = vi.hoisted(() => ({ mockHandleRestart: vi.fn(), mockSetFeatures: vi.fn(), mockEventEmitterEmit: vi.fn(), + mockToastCall: vi.fn(), + mockToastDismiss: vi.fn(), + mockToastUpdate: vi.fn(), + mockToastPromise: vi.fn(), mockText2speechDefaultModel: null as unknown, mockStoreState: { currentLogItem: null as unknown, @@ -43,6 +46,22 @@ const mockState = vi.hoisted(() => ({ }, })) +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: Object.assign(mockState.mockToastCall, { + success: vi.fn((message: string, options?: Record) => + mockState.mockToastCall({ type: 'success', message, ...options })), + error: vi.fn((message: string, options?: Record) => + mockState.mockToastCall({ type: 'error', message, ...options })), + warning: vi.fn((message: string, options?: Record) => + mockState.mockToastCall({ type: 'warning', message, ...options })), + info: vi.fn((message: string, options?: Record) => + mockState.mockToastCall({ type: 'info', message, ...options })), + dismiss: mockState.mockToastDismiss, + update: mockState.mockToastUpdate, + promise: mockState.mockToastPromise, + }), +})) + vi.mock('@/app/components/app/configuration/debug/chat-user-input', () => ({ default: () =>
ChatUserInput
, })) @@ -215,19 +234,27 @@ vi.mock('./debug-with-multiple-model', () => ({ ), })) -vi.mock('./debug-with-single-model', () => ({ - default: React.forwardRef((props: { checkCanSend: () => boolean }, ref) => { +vi.mock('./debug-with-single-model', () => { + function DebugWithSingleModelMock({ + checkCanSend, + ref, + }: { + checkCanSend: () => boolean + ref?: React.Ref<{ handleRestart: () => void }> + }) { React.useImperativeHandle(ref, () => ({ handleRestart: mockState.mockHandleRestart, })) return (
- +
) - }), -})) + } + + return { default: DebugWithSingleModelMock } +}) const createContextValue = (overrides: Partial = {}): DebugContextValue => ({ readonly: false, @@ -376,7 +403,6 @@ const renderDebug = (options: { props?: Partial } = {}) => { const onSetting = vi.fn() - const notify = vi.fn() const props: ComponentProps = { isAPIKeySet: true, onSetting, @@ -392,14 +418,16 @@ const renderDebug = (options: { } render( - - - - - , + React.createElement( + ConfigContext.Provider, + { + value: createContextValue(options.contextValue), + children: , + }, + ), ) - return { onSetting, notify, props } + return { onSetting, notify: mockState.mockToastCall, props } } describe('Debug', () => { diff --git a/web/app/components/app/configuration/debug/index.tsx b/web/app/components/app/configuration/debug/index.tsx index cd07885f0c..36cd4c3445 100644 --- a/web/app/components/app/configuration/debug/index.tsx +++ b/web/app/components/app/configuration/debug/index.tsx @@ -29,8 +29,12 @@ import Button from '@/app/components/base/button' import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks' import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows' import PromptLogModal from '@/app/components/base/prompt-log-modal' -import { ToastContext } from '@/app/components/base/toast/context' -import TooltipPlus from '@/app/components/base/tooltip' +import { toast } from '@/app/components/base/ui/toast' +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/app/components/base/ui/tooltip' import { ModelFeatureEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config' @@ -139,22 +143,20 @@ const Debug: FC = ({ setIsShowFormattingChangeConfirm(false) setFormattingChanged(false) } - - const { notify } = useContext(ToastContext) const logError = useCallback((message: string) => { - notify({ type: 'error', message }) - }, [notify]) + toast.error(message) + }, []) const [completionFiles, setCompletionFiles] = useState([]) const checkCanSend = useCallback(() => { if (isAdvancedMode && mode !== AppModeEnum.COMPLETION) { if (modelModeType === ModelModeType.completion) { if (!hasSetBlockStatus.history) { - notify({ type: 'error', message: t('otherError.historyNoBeEmpty', { ns: 'appDebug' }) }) + toast.error(t('otherError.historyNoBeEmpty', { ns: 'appDebug' })) return false } if (!hasSetBlockStatus.query) { - notify({ type: 'error', message: t('otherError.queryNoBeEmpty', { ns: 'appDebug' }) }) + toast.error(t('otherError.queryNoBeEmpty', { ns: 'appDebug' })) return false } } @@ -180,7 +182,7 @@ const Debug: FC = ({ } if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) { - notify({ type: 'info', message: t('errorMessage.waitForFileUpload', { ns: 'appDebug' }) }) + toast.info(t('errorMessage.waitForFileUpload', { ns: 'appDebug' })) return false } return !hasEmptyInput @@ -194,7 +196,6 @@ const Debug: FC = ({ modelConfig.configs.prompt_variables, t, logError, - notify, modelModeType, ]) @@ -205,7 +206,7 @@ const Debug: FC = ({ const sendTextCompletion = async () => { if (isResponding) { - notify({ type: 'info', message: t('errorMessage.waitForResponse', { ns: 'appDebug' }) }) + toast.info(t('errorMessage.waitForResponse', { ns: 'appDebug' })) return false } @@ -420,27 +421,24 @@ const Debug: FC = ({ <> { !readonly && ( - - - - - - + + } /> + + {t('operation.refresh', { ns: 'common' })} + + ) } { varList.length > 0 && (
- - !readonly && setExpanded(!expanded)}> - - - + + !readonly && setExpanded(!expanded)}>} /> + + {t('panel.userInputField', { ns: 'workflow' })} + + {expanded &&
}
) diff --git a/web/app/components/app/configuration/index.tsx b/web/app/components/app/configuration/index.tsx index aa1bbe0a16..08df556d8e 100644 --- a/web/app/components/app/configuration/index.tsx +++ b/web/app/components/app/configuration/index.tsx @@ -26,7 +26,6 @@ import { produce } from 'immer' import * as React from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import { useShallow } from 'zustand/react/shallow' import AppPublisher from '@/app/components/app/app-publisher/features-wrapper' import Config from '@/app/components/app/configuration/config' @@ -48,8 +47,7 @@ import { FeaturesProvider } from '@/app/components/base/features' import NewFeaturePanel from '@/app/components/base/features/new-feature-panel' import Loading from '@/app/components/base/loading' import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants' -import Toast from '@/app/components/base/toast' -import { ToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' import { ModelFeatureEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { @@ -93,7 +91,6 @@ type PublishConfig = { const Configuration: FC = () => { const { t } = useTranslation() - const { notify } = useContext(ToastContext) const { isLoadingCurrentWorkspace, currentWorkspace } = useAppContext() const { appDetail, showAppConfigureFeaturesModal, setAppSidebarExpand, setShowAppConfigureFeaturesModal } = useAppStore(useShallow(state => ({ @@ -492,11 +489,11 @@ const Configuration: FC = () => { isAdvancedMode, ) if (Object.keys(removedDetails).length) - Toast.notify({ type: 'warning', message: `${t('modelProvider.parametersInvalidRemoved', { ns: 'common' })}: ${Object.entries(removedDetails).map(([k, reason]) => `${k} (${reason})`).join(', ')}` }) + toast.warning(`${t('modelProvider.parametersInvalidRemoved', { ns: 'common' })}: ${Object.entries(removedDetails).map(([k, reason]) => `${k} (${reason})`).join(', ')}`) setCompletionParams(filtered) } catch { - Toast.notify({ type: 'error', message: t('error', { ns: 'common' }) }) + toast.error(t('error', { ns: 'common' })) setCompletionParams({}) } } @@ -767,23 +764,23 @@ const Configuration: FC = () => { const promptVariables = modelConfig.configs.prompt_variables if (promptEmpty) { - notify({ type: 'error', message: t('otherError.promptNoBeEmpty', { ns: 'appDebug' }) }) + toast.error(t('otherError.promptNoBeEmpty', { ns: 'appDebug' })) return } if (isAdvancedMode && mode !== AppModeEnum.COMPLETION) { if (modelModeType === ModelModeType.completion) { if (!hasSetBlockStatus.history) { - notify({ type: 'error', message: t('otherError.historyNoBeEmpty', { ns: 'appDebug' }) }) + toast.error(t('otherError.historyNoBeEmpty', { ns: 'appDebug' })) return } if (!hasSetBlockStatus.query) { - notify({ type: 'error', message: t('otherError.queryNoBeEmpty', { ns: 'appDebug' }) }) + toast.error(t('otherError.queryNoBeEmpty', { ns: 'appDebug' })) return } } } if (contextVarEmpty) { - notify({ type: 'error', message: t('feature.dataSet.queryVariable.contextVarNotEmpty', { ns: 'appDebug' }) }) + toast.error(t('feature.dataSet.queryVariable.contextVarNotEmpty', { ns: 'appDebug' })) return } const postDatasets = dataSets.map(({ id }) => ({ @@ -849,7 +846,7 @@ const Configuration: FC = () => { modelConfig: newModelConfig, completionParams, }) - notify({ type: 'success', message: t('api.success', { ns: 'common' }) }) + toast.success(t('api.success', { ns: 'common' })) setCanReturnToSimpleMode(false) return true diff --git a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx index dd7a0c6a6c..1c9adca1d1 100644 --- a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx +++ b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx @@ -11,9 +11,9 @@ import Button from '@/app/components/base/button' import EmojiPicker from '@/app/components/base/emoji-picker' import FormGeneration from '@/app/components/base/features/new-feature-panel/moderation/form-generation' import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education' -import Modal from '@/app/components/base/modal' -import { SimpleSelect } from '@/app/components/base/select' -import { useToastContext } from '@/app/components/base/toast/context' +import { Dialog, DialogContent } from '@/app/components/base/ui/dialog' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/app/components/base/ui/select' +import { toast } from '@/app/components/base/ui/toast' import ApiBasedExtensionSelector from '@/app/components/header/account-setting/api-based-extension-page/selector' import { useDocLink, useLocale } from '@/context/i18n' import { LanguagesSupported } from '@/i18n-config/language' @@ -39,7 +39,6 @@ const ExternalDataToolModal: FC = ({ }) => { const { t } = useTranslation() const docLink = useDocLink() - const { notify } = useToastContext() const locale = useLocale() const [localeData, setLocaleData] = useState(data.type ? data : { ...data, type: 'api' }) const [showEmojiPicker, setShowEmojiPicker] = useState(false) @@ -133,37 +132,34 @@ const ExternalDataToolModal: FC = ({ const handleSave = () => { if (!localeData.type) { - notify({ type: 'error', message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: t('feature.tools.modal.toolType.title', { ns: 'appDebug' }) }) }) + toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: t('feature.tools.modal.toolType.title', { ns: 'appDebug' }) })) return } if (!localeData.label) { - notify({ type: 'error', message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: t('feature.tools.modal.name.title', { ns: 'appDebug' }) }) }) + toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: t('feature.tools.modal.name.title', { ns: 'appDebug' }) })) return } if (!localeData.variable) { - notify({ type: 'error', message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: t('feature.tools.modal.variableName.title', { ns: 'appDebug' }) }) }) + toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: t('feature.tools.modal.variableName.title', { ns: 'appDebug' }) })) return } if (localeData.variable && !/^[a-z_]\w{0,29}$/i.test(localeData.variable)) { - notify({ type: 'error', message: t('varKeyError.notValid', { ns: 'appDebug', key: t('feature.tools.modal.variableName.title', { ns: 'appDebug' }) }) }) + toast.error(t('varKeyError.notValid', { ns: 'appDebug', key: t('feature.tools.modal.variableName.title', { ns: 'appDebug' }) })) return } if (localeData.type === 'api' && !localeData.config?.api_based_extension_id) { - notify({ type: 'error', message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? 'API Extension' : 'API 扩展' }) }) + toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? 'API Extension' : 'API 扩展' })) return } if (systemTypes.findIndex(t => t === localeData.type) < 0 && currentProvider?.form_schema) { for (let i = 0; i < currentProvider.form_schema.length; i++) { if (!localeData.config?.[currentProvider.form_schema[i].variable] && currentProvider.form_schema[i].required) { - notify({ - type: 'error', - message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? currentProvider.form_schema[i].label['en-US'] : currentProvider.form_schema[i].label['zh-Hans'] }), - }) + toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? currentProvider.form_schema[i].label['en-US'] : currentProvider.form_schema[i].label['zh-Hans'] })) return } } @@ -180,122 +176,128 @@ const ExternalDataToolModal: FC = ({ const action = data.type ? t('operation.edit', { ns: 'common' }) : t('operation.add', { ns: 'common' }) return ( - -
- {`${action} ${t('variableConfig.apiBasedVar', { ns: 'appDebug' })}`} -
-
-
- {t('apiBasedExtension.type', { ns: 'common' })} + +
+ {`${action} ${t('variableConfig.apiBasedVar', { ns: 'appDebug' })}`}
- { - return { - value: option.key, - name: option.name, - } - })} - onSelect={item => handleDataTypeChange(item.value as string)} - /> -
-
-
- {t('feature.tools.modal.name.title', { ns: 'appDebug' })} +
+
+ {t('apiBasedExtension.type', { ns: 'common' })} +
+
-
- handleValueChange({ label: e.target.value })} - className="mr-2 block h-9 grow appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-components-input-text-filled outline-none" - placeholder={t('feature.tools.modal.name.placeholder', { ns: 'appDebug' }) || ''} - /> - { setShowEmojiPicker(true) }} - className="!h-9 !w-9 cursor-pointer rounded-lg border-[0.5px] border-components-panel-border" - icon={localeData.icon} - background={localeData.icon_background} - /> -
-
-
-
- {t('feature.tools.modal.variableName.title', { ns: 'appDebug' })} -
- handleValueChange({ variable: e.target.value })} - className="block h-9 w-full appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-components-input-text-filled outline-none" - placeholder={t('feature.tools.modal.variableName.placeholder', { ns: 'appDebug' }) || ''} - /> -
- { - localeData.type === 'api' && ( -
-
- {t('apiBasedExtension.selector.title', { ns: 'common' })} - - - {t('apiBasedExtension.link', { ns: 'common' })} - -
- +
+ {t('feature.tools.modal.name.title', { ns: 'appDebug' })} +
+
+ handleValueChange({ label: e.target.value })} + className="mr-2 block h-9 grow appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-components-input-text-filled outline-none" + placeholder={t('feature.tools.modal.name.placeholder', { ns: 'appDebug' }) || ''} + /> + { setShowEmojiPicker(true) }} + className="!h-9 !w-9 cursor-pointer rounded-lg border-[0.5px] border-components-panel-border" + icon={localeData.icon} + background={localeData.icon_background} />
- ) - } - { - systemTypes.findIndex(t => t === localeData.type) < 0 - && currentProvider?.form_schema - && ( - +
+
+ {t('feature.tools.modal.variableName.title', { ns: 'appDebug' })} +
+ handleValueChange({ variable: e.target.value })} + className="block h-9 w-full appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-components-input-text-filled outline-none" + placeholder={t('feature.tools.modal.variableName.placeholder', { ns: 'appDebug' }) || ''} /> - ) - } -
- - -
- { - showEmojiPicker && ( - { - handleValueChange({ icon, icon_background }) - setShowEmojiPicker(false) - }} - onClose={() => { - handleValueChange({ icon: '', icon_background: '' }) - setShowEmojiPicker(false) - }} - /> - ) - } - +
+ { + localeData.type === 'api' && ( +
+
+ {t('apiBasedExtension.selector.title', { ns: 'common' })} + + + {t('apiBasedExtension.link', { ns: 'common' })} + +
+ +
+ ) + } + { + systemTypes.findIndex(t => t === localeData.type) < 0 + && currentProvider?.form_schema + && ( + + ) + } +
+ + +
+ { + showEmojiPicker && ( + { + handleValueChange({ icon, icon_background }) + setShowEmojiPicker(false) + }} + onClose={() => { + handleValueChange({ icon: '', icon_background: '' }) + setShowEmojiPicker(false) + }} + /> + ) + } + + ) } diff --git a/web/app/components/app/configuration/tools/index.tsx b/web/app/components/app/configuration/tools/index.tsx index 51a9e87a97..8ab71c73cf 100644 --- a/web/app/components/app/configuration/tools/index.tsx +++ b/web/app/components/app/configuration/tools/index.tsx @@ -5,7 +5,6 @@ import { RiDeleteBinLine, } from '@remixicon/react' import copy from 'copy-to-clipboard' -// abandoned import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' @@ -15,14 +14,17 @@ import { } from '@/app/components/base/icons/src/vender/line/general' import { Tool03 } from '@/app/components/base/icons/src/vender/solid/general' import Switch from '@/app/components/base/switch' -import { useToastContext } from '@/app/components/base/toast/context' -import Tooltip from '@/app/components/base/tooltip' +import { toast } from '@/app/components/base/ui/toast' +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/app/components/base/ui/tooltip' import ConfigContext from '@/context/debug-configuration' import { useModalContext } from '@/context/modal-context' const Tools = () => { const { t } = useTranslation() - const { notify } = useToastContext() const { setShowExternalDataToolModal } = useModalContext() const { externalDataToolsConfig, @@ -48,7 +50,7 @@ const Tools = () => { const promptVariables = modelConfig?.configs?.prompt_variables || [] for (let i = 0; i < promptVariables.length; i++) { if (promptVariables[i].key === newExternalDataTool.variable) { - notify({ type: 'error', message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }) }) + toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key })) return false } } @@ -66,7 +68,7 @@ const Tools = () => { for (let i = 0; i < existedExternalDataTools.length; i++) { if (existedExternalDataTools[i].variable === newExternalDataTool.variable) { - notify({ type: 'error', message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: existedExternalDataTools[i].variable }) }) + toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: existedExternalDataTools[i].variable })) return false } } @@ -110,13 +112,14 @@ const Tools = () => {
{t('feature.tools.title', { ns: 'appDebug' })}
- + } /> +
{t('feature.tools.tips', { ns: 'appDebug' })}
- )} - /> +
+
{ !expanded && !!externalDataToolsConfig.length && ( @@ -151,18 +154,23 @@ const Tools = () => { background={item.icon_background} />
{item.label}
- -
{ - copy(item.variable || '') - setCopied(true) - }} - > - {item.variable} -
+ + { + copy(item.variable || '') + setCopied(true) + }} + > + {item.variable} +
+ )} + /> + + {copied ? t('copied', { ns: 'appApi' }) : `${item.variable}, ${t('copy', { ns: 'appApi' })}`} +
({ vi.mock('@/service/apps', () => ({ createApp: vi.fn(), })) +const toastMocks = vi.hoisted(() => ({ + mockToastSuccess: vi.fn(), + mockToastError: vi.fn(), +})) +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + success: toastMocks.mockToastSuccess, + error: toastMocks.mockToastError, + }, +})) vi.mock('@/utils/app-redirection', () => ({ getRedirection: vi.fn(), })) @@ -48,7 +57,6 @@ vi.mock('@/hooks/use-theme', () => ({ default: () => ({ theme: 'light' }), })) -const mockNotify = vi.fn() const mockUseRouter = vi.mocked(useRouter) const mockPush = vi.fn() const mockCreateApp = vi.mocked(createApp) @@ -56,6 +64,7 @@ const mockTrackEvent = vi.mocked(trackEvent) const mockGetRedirection = vi.mocked(getRedirection) const mockUseProviderContext = vi.mocked(useProviderContext) const mockUseAppContext = vi.mocked(useAppContext) +const { mockToastSuccess, mockToastError } = toastMocks const defaultPlanUsage = { buildApps: 0, @@ -70,11 +79,7 @@ const defaultPlanUsage = { const renderModal = () => { const onClose = vi.fn() const onSuccess = vi.fn() - render( - - - , - ) + render() return { onClose, onSuccess } } @@ -140,7 +145,7 @@ describe('CreateAppModal', () => { app_mode: AppModeEnum.ADVANCED_CHAT, description: '', }) - expect(mockNotify).toHaveBeenCalledWith({ type: 'success', message: 'app.newApp.appCreated' }) + expect(mockToastSuccess).toHaveBeenCalledWith('app.newApp.appCreated') expect(onSuccess).toHaveBeenCalled() expect(onClose).toHaveBeenCalled() await waitFor(() => expect(mockSetItem).toHaveBeenCalledWith(NEED_REFRESH_APP_LIST_KEY, '1')) @@ -156,7 +161,7 @@ describe('CreateAppModal', () => { fireEvent.click(screen.getByRole('button', { name: /app\.newApp\.Create/ })) await waitFor(() => expect(mockCreateApp).toHaveBeenCalled()) - expect(mockNotify).toHaveBeenCalledWith({ type: 'error', message: 'boom' }) + expect(mockToastError).toHaveBeenCalledWith('boom') expect(onClose).not.toHaveBeenCalled() }) }) diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx index 556773c341..8750b732b1 100644 --- a/web/app/components/app/create-app-modal/index.tsx +++ b/web/app/components/app/create-app-modal/index.tsx @@ -6,7 +6,6 @@ import { RiArrowRightLine, RiArrowRightSLine, RiExchange2Fill } from '@remixicon import { useDebounceFn, useKeyPress } from 'ahooks' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import { trackEvent } from '@/app/components/base/amplitude' import AppIcon from '@/app/components/base/app-icon' import Button from '@/app/components/base/button' @@ -15,7 +14,7 @@ import FullScreenModal from '@/app/components/base/fullscreen-modal' import { BubbleTextMod, ChatBot, ListSparkle, Logic } from '@/app/components/base/icons/src/vender/solid/communication' import Input from '@/app/components/base/input' import Textarea from '@/app/components/base/textarea' -import { ToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import AppsFull from '@/app/components/billing/apps-full-in-dialog' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useAppContext } from '@/context/app-context' @@ -40,7 +39,6 @@ type CreateAppProps = { function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }: CreateAppProps) { const { t } = useTranslation() const { push } = useRouter() - const { notify } = useContext(ToastContext) const [appMode, setAppMode] = useState(defaultAppMode || AppModeEnum.ADVANCED_CHAT) const [appIcon, setAppIcon] = useState({ type: 'emoji', icon: '🤖', background: '#FFEAD5' }) @@ -62,11 +60,11 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }: const onCreate = useCallback(async () => { if (!appMode) { - notify({ type: 'error', message: t('newApp.appTypeRequired', { ns: 'app' }) }) + toast.error(t('newApp.appTypeRequired', { ns: 'app' })) return } if (!name.trim()) { - notify({ type: 'error', message: t('newApp.nameNotEmpty', { ns: 'app' }) }) + toast.error(t('newApp.nameNotEmpty', { ns: 'app' })) return } if (isCreatingRef.current) @@ -88,20 +86,17 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }: description, }) - notify({ type: 'success', message: t('newApp.appCreated', { ns: 'app' }) }) + toast.success(t('newApp.appCreated', { ns: 'app' })) onSuccess() onClose() localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') getRedirection(isCurrentWorkspaceEditor, app, push) } catch (e: any) { - notify({ - type: 'error', - message: e.message || t('newApp.appCreateFailed', { ns: 'app' }), - }) + toast.error(e.message || t('newApp.appCreateFailed', { ns: 'app' })) } isCreatingRef.current = false - }, [name, notify, t, appMode, appIcon, description, onSuccess, onClose, push, isCurrentWorkspaceEditor]) + }, [name, t, appMode, appIcon, description, onSuccess, onClose, push, isCurrentWorkspaceEditor]) const { run: handleCreateApp } = useDebounceFn(onCreate, { wait: 300 }) useKeyPress(['meta.enter', 'ctrl.enter'], () => { diff --git a/web/app/components/app/create-from-dsl-modal/index.tsx b/web/app/components/app/create-from-dsl-modal/index.tsx index eaaee50973..dd17655e3c 100644 --- a/web/app/components/app/create-from-dsl-modal/index.tsx +++ b/web/app/components/app/create-from-dsl-modal/index.tsx @@ -6,12 +6,11 @@ import { useDebounceFn, useKeyPress } from 'ahooks' import { noop } from 'es-toolkit/function' import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import { trackEvent } from '@/app/components/base/amplitude' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' import Modal from '@/app/components/base/modal' -import { ToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import AppsFull from '@/app/components/billing/apps-full-in-dialog' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' @@ -48,7 +47,6 @@ export enum CreateFromDSLModalTab { const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDSLModalTab.FROM_FILE, dslUrl = '', droppedFile }: CreateFromDSLModalProps) => { const { push } = useRouter() const { t } = useTranslation() - const { notify } = useContext(ToastContext) const [currentFile, setDSLFile] = useState(droppedFile) const [fileContent, setFileContent] = useState() const [currentTab, setCurrentTab] = useState(activeTab) @@ -126,10 +124,11 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS if (onClose) onClose() - notify({ + toast(t(status === DSLImportStatus.COMPLETED ? 'newApp.appCreated' : 'newApp.caution', { ns: 'app' }), { type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning', - message: t(status === DSLImportStatus.COMPLETED ? 'newApp.appCreated' : 'newApp.caution', { ns: 'app' }), - children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('newApp.appCreateDSLWarning', { ns: 'app' }), + description: status === DSLImportStatus.COMPLETED_WITH_WARNINGS + ? t('newApp.appCreateDSLWarning', { ns: 'app' }) + : undefined, }) localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') if (app_id) @@ -147,12 +146,12 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS setImportId(id) } else { - notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) }) + toast.error(t('newApp.appCreateFailed', { ns: 'app' })) } } // eslint-disable-next-line unused-imports/no-unused-vars catch (e) { - notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) }) + toast.error(t('newApp.appCreateFailed', { ns: 'app' })) } isCreatingRef.current = false } @@ -185,22 +184,19 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS if (onClose) onClose() - notify({ - type: 'success', - message: t('newApp.appCreated', { ns: 'app' }), - }) + toast.success(t('newApp.appCreated', { ns: 'app' })) if (app_id) await handleCheckPluginDependencies(app_id) localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') getRedirection(isCurrentWorkspaceEditor, { id: app_id!, mode: app_mode }, push) } else if (status === DSLImportStatus.FAILED) { - notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) }) + toast.error(t('newApp.appCreateFailed', { ns: 'app' })) } } // eslint-disable-next-line unused-imports/no-unused-vars catch (e) { - notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) }) + toast.error(t('newApp.appCreateFailed', { ns: 'app' })) } } diff --git a/web/app/components/app/create-from-dsl-modal/uploader.tsx b/web/app/components/app/create-from-dsl-modal/uploader.tsx index 74c8e5f48e..3dcab1c6d6 100644 --- a/web/app/components/app/create-from-dsl-modal/uploader.tsx +++ b/web/app/components/app/create-from-dsl-modal/uploader.tsx @@ -7,10 +7,9 @@ import { import * as React from 'react' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import ActionButton from '@/app/components/base/action-button' import { Yaml as YamlIcon } from '@/app/components/base/icons/src/public/files' -import { ToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import { cn } from '@/utils/classnames' import { formatFileSize } from '@/utils/format' @@ -30,7 +29,6 @@ const Uploader: FC = ({ displayName = 'YAML', }) => { const { t } = useTranslation() - const { notify } = useContext(ToastContext) const [dragging, setDragging] = useState(false) const dropRef = useRef(null) const dragRef = useRef(null) @@ -60,7 +58,7 @@ const Uploader: FC = ({ return const files = Array.from(e.dataTransfer.files) if (files.length > 1) { - notify({ type: 'error', message: t('stepOne.uploader.validation.count', { ns: 'datasetCreation' }) }) + toast.error(t('stepOne.uploader.validation.count', { ns: 'datasetCreation' })) return } updateFile(files[0]) diff --git a/web/app/components/app/duplicate-modal/index.spec.tsx b/web/app/components/app/duplicate-modal/index.spec.tsx index ef12646571..e70329a105 100644 --- a/web/app/components/app/duplicate-modal/index.spec.tsx +++ b/web/app/components/app/duplicate-modal/index.spec.tsx @@ -2,7 +2,7 @@ import type { ProviderContextState } from '@/context/provider-context' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { Plan } from '@/app/components/billing/type' import { baseProviderContextValue } from '@/context/provider-context' import DuplicateAppModal from './index' @@ -129,7 +129,7 @@ describe('DuplicateAppModal', () => { it('should show error toast when name is empty', async () => { const user = userEvent.setup() - const toastSpy = vi.spyOn(Toast, 'notify') + const toastSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error') // Arrange const { onConfirm, onHide } = renderComponent() @@ -138,7 +138,7 @@ describe('DuplicateAppModal', () => { await user.click(screen.getByRole('button', { name: 'app.duplicate' })) // Assert - expect(toastSpy).toHaveBeenCalledWith({ type: 'error', message: 'explore.appCustomize.nameRequired' }) + expect(toastSpy).toHaveBeenCalledWith('explore.appCustomize.nameRequired') expect(onConfirm).not.toHaveBeenCalled() expect(onHide).not.toHaveBeenCalled() }) diff --git a/web/app/components/app/duplicate-modal/index.tsx b/web/app/components/app/duplicate-modal/index.tsx index 7d5b122f69..b2ba7f1d0f 100644 --- a/web/app/components/app/duplicate-modal/index.tsx +++ b/web/app/components/app/duplicate-modal/index.tsx @@ -9,7 +9,7 @@ import AppIcon from '@/app/components/base/app-icon' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' import Modal from '@/app/components/base/modal' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import AppsFull from '@/app/components/billing/apps-full-in-dialog' import { useProviderContext } from '@/context/provider-context' import { cn } from '@/utils/classnames' @@ -57,7 +57,7 @@ const DuplicateAppModal = ({ const submit = () => { if (!name.trim()) { - Toast.notify({ type: 'error', message: t('appCustomize.nameRequired', { ns: 'explore' }) }) + toast.error(t('appCustomize.nameRequired', { ns: 'explore' })) return } onConfirm({ diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index 453c7c9d4c..4a22a0c85f 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -30,8 +30,8 @@ import Drawer from '@/app/components/base/drawer' import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' import Loading from '@/app/components/base/loading' import MessageLogModal from '@/app/components/base/message-log-modal' -import { ToastContext } from '@/app/components/base/toast/context' import Tooltip from '@/app/components/base/tooltip' +import { toast } from '@/app/components/base/ui/toast' import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' import { WorkflowContextProvider } from '@/app/components/workflow/context' import { useAppContext } from '@/context/app-context' @@ -223,7 +223,6 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { const { userProfile: { timezone } } = useAppContext() const { formatTime } = useTimestamp() const { onClose, appDetail } = useContext(DrawerContext) - const { notify } = useContext(ToastContext) const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, showPromptLogModal, setShowPromptLogModal, currentLogModalActiveTab } = useAppStore(useShallow((state: AppStoreState) => ({ currentLogItem: state.currentLogItem, setCurrentLogItem: state.setCurrentLogItem, @@ -413,14 +412,14 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { return item })) - notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) return true } catch { - notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) }) + toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' })) return false } - }, [allChatItems, appDetail?.id, notify, t]) + }, [allChatItems, appDetail?.id, t]) const fetchInitiated = useRef(false) @@ -734,7 +733,6 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { const CompletionConversationDetailComp: FC<{ appId?: string, conversationId?: string }> = ({ appId, conversationId }) => { // Text Generator App Session Details Including Message List const { data: conversationDetail, refetch: conversationDetailMutate } = useCompletionConversationDetail(appId, conversationId) - const { notify } = useContext(ToastContext) const { t } = useTranslation() const handleFeedback = async (mid: string, { rating, content }: FeedbackType): Promise => { @@ -744,11 +742,11 @@ const CompletionConversationDetailComp: FC<{ appId?: string, conversationId?: st body: { message_id: mid, rating, content: content ?? undefined }, }) conversationDetailMutate() - notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) return true } catch { - notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) }) + toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' })) return false } } @@ -757,11 +755,11 @@ const CompletionConversationDetailComp: FC<{ appId?: string, conversationId?: st try { await updateLogMessageAnnotations({ url: `/apps/${appId}/annotations`, body: { message_id: mid, content: value } }) conversationDetailMutate() - notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) return true } catch { - notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) }) + toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' })) return false } } @@ -783,7 +781,6 @@ const CompletionConversationDetailComp: FC<{ appId?: string, conversationId?: st */ const ChatConversationDetailComp: FC<{ appId?: string, conversationId?: string }> = ({ appId, conversationId }) => { const { data: conversationDetail } = useChatConversationDetail(appId, conversationId) - const { notify } = useContext(ToastContext) const { t } = useTranslation() const handleFeedback = async (mid: string, { rating, content }: FeedbackType): Promise => { @@ -792,11 +789,11 @@ const ChatConversationDetailComp: FC<{ appId?: string, conversationId?: string } url: `/apps/${appId}/feedbacks`, body: { message_id: mid, rating, content: content ?? undefined }, }) - notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) return true } catch { - notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) }) + toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' })) return false } } @@ -804,11 +801,11 @@ const ChatConversationDetailComp: FC<{ appId?: string, conversationId?: string } const handleAnnotation = async (mid: string, value: string): Promise => { try { await updateLogMessageAnnotations({ url: `/apps/${appId}/annotations`, body: { message_id: mid, content: value } }) - notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) return true } catch { - notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) }) + toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' })) return false } } diff --git a/web/app/components/app/overview/settings/index.spec.tsx b/web/app/components/app/overview/settings/index.spec.tsx index e933855ca8..d6f9612f75 100644 --- a/web/app/components/app/overview/settings/index.spec.tsx +++ b/web/app/components/app/overview/settings/index.spec.tsx @@ -29,7 +29,24 @@ vi.mock('react-i18next', async () => { } }) -const mockNotify = vi.fn() +const toastMocks = vi.hoisted(() => ({ + call: vi.fn(), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), +})) + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: Object.assign(toastMocks.call, { + success: vi.fn((message: string, options?: Record) => toastMocks.call({ type: 'success', message, ...options })), + error: vi.fn((message: string, options?: Record) => toastMocks.call({ type: 'error', message, ...options })), + warning: vi.fn((message: string, options?: Record) => toastMocks.call({ type: 'warning', message, ...options })), + info: vi.fn((message: string, options?: Record) => toastMocks.call({ type: 'info', message, ...options })), + dismiss: toastMocks.dismiss, + update: toastMocks.update, + promise: toastMocks.promise, + }), +})) const mockOnClose = vi.fn() const mockOnSave = vi.fn() const mockSetShowPricingModal = vi.fn() @@ -56,13 +73,6 @@ vi.mock('@/context/modal-context', () => ({ useModalContext: () => buildModalContext(), })) -vi.mock('@/app/components/base/toast/context', () => ({ - useToastContext: () => ({ - notify: mockNotify, - close: vi.fn(), - }), -})) - vi.mock('@/context/i18n', async () => { const actual = await vi.importActual('@/context/i18n') return { @@ -112,7 +122,7 @@ const renderSettingsModal = () => render( describe('SettingsModal', () => { beforeEach(() => { - mockNotify.mockClear() + toastMocks.call.mockClear() mockOnClose.mockClear() mockOnSave.mockClear() mockSetShowPricingModal.mockClear() @@ -152,7 +162,7 @@ describe('SettingsModal', () => { fireEvent.click(screen.getByText('common.operation.save')) await waitFor(() => { - expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ message: 'app.newApp.nameNotEmpty' })) + expect(toastMocks.call).toHaveBeenCalledWith(expect.objectContaining({ message: 'app.newApp.nameNotEmpty' })) }) expect(mockOnSave).not.toHaveBeenCalled() }) @@ -164,7 +174,7 @@ describe('SettingsModal', () => { fireEvent.click(screen.getByText('common.operation.save')) await waitFor(() => { - expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ + expect(toastMocks.call).toHaveBeenCalledWith(expect.objectContaining({ message: 'appOverview.overview.appInfo.settings.invalidHexMessage', })) }) @@ -180,7 +190,7 @@ describe('SettingsModal', () => { fireEvent.click(screen.getByText('common.operation.save')) await waitFor(() => { - expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ + expect(toastMocks.call).toHaveBeenCalledWith(expect.objectContaining({ message: 'appOverview.overview.appInfo.settings.invalidPrivacyPolicy', })) }) diff --git a/web/app/components/app/overview/settings/index.tsx b/web/app/components/app/overview/settings/index.tsx index 13dacde424..0d77d32ec4 100644 --- a/web/app/components/app/overview/settings/index.tsx +++ b/web/app/components/app/overview/settings/index.tsx @@ -19,8 +19,8 @@ import PremiumBadge from '@/app/components/base/premium-badge' import { SimpleSelect } from '@/app/components/base/select' import Switch from '@/app/components/base/switch' import Textarea from '@/app/components/base/textarea' -import { useToastContext } from '@/app/components/base/toast/context' import Tooltip from '@/app/components/base/tooltip' +import { toast } from '@/app/components/base/ui/toast' import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' @@ -65,7 +65,6 @@ const SettingsModal: FC = ({ onClose, onSave, }) => { - const { notify } = useToastContext() const [isShowMore, setIsShowMore] = useState(false) const { title, @@ -159,7 +158,7 @@ const SettingsModal: FC = ({ const onClickSave = async () => { if (!inputInfo.title) { - notify({ type: 'error', message: t('newApp.nameNotEmpty', { ns: 'app' }) }) + toast.error(t('newApp.nameNotEmpty', { ns: 'app' })) return } @@ -181,11 +180,11 @@ const SettingsModal: FC = ({ if (inputInfo !== null) { if (!validateColorHex(inputInfo.chatColorTheme)) { - notify({ type: 'error', message: t(`${prefixSettings}.invalidHexMessage`, { ns: 'appOverview' }) }) + toast.error(t(`${prefixSettings}.invalidHexMessage`, { ns: 'appOverview' })) return } if (!validatePrivacyPolicy(inputInfo.privacyPolicy)) { - notify({ type: 'error', message: t(`${prefixSettings}.invalidPrivacyPolicy`, { ns: 'appOverview' }) }) + toast.error(t(`${prefixSettings}.invalidPrivacyPolicy`, { ns: 'appOverview' })) return } } diff --git a/web/app/components/app/switch-app-modal/index.spec.tsx b/web/app/components/app/switch-app-modal/index.spec.tsx index 53007b986b..147edeb5ed 100644 --- a/web/app/components/app/switch-app-modal/index.spec.tsx +++ b/web/app/components/app/switch-app-modal/index.spec.tsx @@ -3,7 +3,6 @@ import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' import { useStore as useAppStore } from '@/app/components/app/store' -import { ToastContext } from '@/app/components/base/toast/context' import { Plan } from '@/app/components/billing/type' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { AppModeEnum } from '@/types/app' @@ -108,27 +107,44 @@ const createMockApp = (overrides: Partial = {}): App => ({ ...overrides, }) +const toastMocks = vi.hoisted(() => ({ + notify: vi.fn(), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), +})) + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + success: (message: string, options?: Record) => toastMocks.notify({ type: 'success', message, ...options }), + error: (message: string, options?: Record) => toastMocks.notify({ type: 'error', message, ...options }), + warning: (message: string, options?: Record) => toastMocks.notify({ type: 'warning', message, ...options }), + info: (message: string, options?: Record) => toastMocks.notify({ type: 'info', message, ...options }), + dismiss: toastMocks.dismiss, + update: toastMocks.update, + promise: toastMocks.promise, + }, +})) + const renderComponent = (overrides: Partial> = {}) => { - const notify = vi.fn() const onClose = vi.fn() const onSuccess = vi.fn() const appDetail = createMockApp() const utils = render( - - - , + , + ) return { ...utils, - notify, + notify: toastMocks.notify, onClose, onSuccess, appDetail, diff --git a/web/app/components/app/switch-app-modal/index.tsx b/web/app/components/app/switch-app-modal/index.tsx index 7c3269d52c..ffa5dc6ef4 100644 --- a/web/app/components/app/switch-app-modal/index.tsx +++ b/web/app/components/app/switch-app-modal/index.tsx @@ -5,7 +5,6 @@ import { RiCloseLine } from '@remixicon/react' import { noop } from 'es-toolkit/function' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import { useStore as useAppStore } from '@/app/components/app/store' import AppIcon from '@/app/components/base/app-icon' import Button from '@/app/components/base/button' @@ -14,7 +13,7 @@ import Confirm from '@/app/components/base/confirm' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' import Input from '@/app/components/base/input' import Modal from '@/app/components/base/modal' -import { ToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import AppsFull from '@/app/components/billing/apps-full-in-dialog' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useAppContext } from '@/context/app-context' @@ -37,7 +36,6 @@ type SwitchAppModalProps = { const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClose }: SwitchAppModalProps) => { const { push, replace } = useRouter() const { t } = useTranslation() - const { notify } = useContext(ToastContext) const setAppDetail = useAppStore(s => s.setAppDetail) const { isCurrentWorkspaceEditor } = useAppContext() @@ -68,7 +66,7 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo onSuccess() if (onClose) onClose() - notify({ type: 'success', message: t('newApp.appCreated', { ns: 'app' }) }) + toast.success(t('newApp.appCreated', { ns: 'app' })) if (inAppDetail) setAppDetail() if (removeOriginal) @@ -84,7 +82,7 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo ) } catch { - notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) }) + toast.error(t('newApp.appCreateFailed', { ns: 'app' })) } } diff --git a/web/app/components/app/text-generate/item/index.tsx b/web/app/components/app/text-generate/item/index.tsx index d22375a292..ab96077f67 100644 --- a/web/app/components/app/text-generate/item/index.tsx +++ b/web/app/components/app/text-generate/item/index.tsx @@ -28,7 +28,7 @@ import { useChatContext } from '@/app/components/base/chat/chat/context' import Loading from '@/app/components/base/loading' import { Markdown } from '@/app/components/base/markdown' import NewAudioButton from '@/app/components/base/new-audio-button' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { useParams } from '@/next/navigation' import { fetchTextGenerationMessage } from '@/service/debug' import { AppSourceType, fetchMoreLikeThis, submitHumanInputForm, updateFeedback } from '@/service/share' @@ -145,7 +145,7 @@ const GenerationItem: FC = ({ const handleMoreLikeThis = async () => { if (isQuerying || !messageId) { - Toast.notify({ type: 'warning', message: t('errorMessage.waitForResponse', { ns: 'appDebug' }) }) + toast.warning(t('errorMessage.waitForResponse', { ns: 'appDebug' })) return } startQuerying() @@ -366,7 +366,7 @@ const GenerationItem: FC = ({ copy(copyContent) else copy(JSON.stringify(copyContent)) - Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.copySuccessfully', { ns: 'common' })) }} > diff --git a/web/app/components/app/text-generate/saved-items/index.spec.tsx b/web/app/components/app/text-generate/saved-items/index.spec.tsx index b45a1cca6c..dff0950f89 100644 --- a/web/app/components/app/text-generate/saved-items/index.spec.tsx +++ b/web/app/components/app/text-generate/saved-items/index.spec.tsx @@ -4,7 +4,7 @@ import copy from 'copy-to-clipboard' import * as React from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import SavedItems from './index' vi.mock('copy-to-clipboard', () => ({ @@ -16,7 +16,7 @@ vi.mock('@/next/navigation', () => ({ })) const mockCopy = vi.mocked(copy) -const toastNotifySpy = vi.spyOn(Toast, 'notify') +const toastSuccessSpy = vi.spyOn(toast, 'success').mockReturnValue('toast-success') const baseProps: ISavedItemsProps = { list: [ @@ -30,7 +30,7 @@ const baseProps: ISavedItemsProps = { describe('SavedItems', () => { beforeEach(() => { vi.clearAllMocks() - toastNotifySpy.mockClear() + toastSuccessSpy.mockClear() }) it('renders saved answers with metadata and controls', () => { @@ -58,7 +58,7 @@ describe('SavedItems', () => { fireEvent.click(copyButton) expect(mockCopy).toHaveBeenCalledWith('hello world') - expect(toastNotifySpy).toHaveBeenCalledWith({ type: 'success', message: 'common.actionMsg.copySuccessfully' }) + expect(toastSuccessSpy).toHaveBeenCalledWith('common.actionMsg.copySuccessfully') fireEvent.click(deleteButton) expect(handleRemove).toHaveBeenCalledWith('1') diff --git a/web/app/components/app/text-generate/saved-items/index.tsx b/web/app/components/app/text-generate/saved-items/index.tsx index 36006402c4..cd43f354f3 100644 --- a/web/app/components/app/text-generate/saved-items/index.tsx +++ b/web/app/components/app/text-generate/saved-items/index.tsx @@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' import { Markdown } from '@/app/components/base/markdown' import NewAudioButton from '@/app/components/base/new-audio-button' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { cn } from '@/utils/classnames' import NoData from './no-data' @@ -60,7 +60,7 @@ const SavedItems: FC = ({ {isShowTextToSpeech && } { copy(answer) - Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.copySuccessfully', { ns: 'common' })) }} > diff --git a/web/app/components/apps/__tests__/app-card.spec.tsx b/web/app/components/apps/__tests__/app-card.spec.tsx index 86c87e0c5b..d1e89b7a85 100644 --- a/web/app/components/apps/__tests__/app-card.spec.tsx +++ b/web/app/components/apps/__tests__/app-card.spec.tsx @@ -17,16 +17,36 @@ vi.mock('@/next/navigation', () => ({ }), })) -// Mock use-context-selector with stable mockNotify reference for tracking calls +const toastMocks = vi.hoisted(() => { + const record = vi.fn() + const api = vi.fn((message: unknown, options?: Record) => record({ message, ...options })) + return { + record, + api: Object.assign(api, { + success: vi.fn((message: unknown, options?: Record) => record({ type: 'success', message, ...options })), + error: vi.fn((message: unknown, options?: Record) => record({ type: 'error', message, ...options })), + warning: vi.fn((message: unknown, options?: Record) => record({ type: 'warning', message, ...options })), + info: vi.fn((message: unknown, options?: Record) => record({ type: 'info', message, ...options })), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), + }), + } +}) + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: toastMocks.api, +})) + +// Mock use-context-selector with stable toast reference for tracking calls // Include createContext for components that use it (like Toast) -const mockNotify = vi.fn() vi.mock('use-context-selector', () => ({ createContext: (defaultValue: T) => React.createContext(defaultValue), useContext: () => ({ - notify: mockNotify, + notify: toastMocks.api, }), useContextSelector: (_context: unknown, selector: (state: Record) => unknown) => selector({ - notify: mockNotify, + notify: toastMocks.api, }), })) @@ -591,7 +611,7 @@ describe('AppCard', () => { await waitFor(() => { expect(mockDeleteAppMutation).toHaveBeenCalled() - expect(mockNotify).toHaveBeenCalledWith({ type: 'error', message: expect.stringContaining('Delete failed') }) + expect(toastMocks.record).toHaveBeenCalledWith({ type: 'error', message: expect.stringContaining('Delete failed') }) }) }) @@ -670,7 +690,7 @@ describe('AppCard', () => { await waitFor(() => { expect(appsService.copyApp).toHaveBeenCalled() - expect(mockNotify).toHaveBeenCalledWith({ type: 'error', message: 'app.newApp.appCreateFailed' }) + expect(toastMocks.record).toHaveBeenCalledWith({ type: 'error', message: 'app.newApp.appCreateFailed' }) }) }) @@ -699,7 +719,7 @@ describe('AppCard', () => { await waitFor(() => { expect(appsService.exportAppConfig).toHaveBeenCalled() - expect(mockNotify).toHaveBeenCalledWith({ type: 'error', message: 'app.exportFailed' }) + expect(toastMocks.record).toHaveBeenCalledWith({ type: 'error', message: 'app.exportFailed' }) }) }) }) @@ -945,7 +965,7 @@ describe('AppCard', () => { await waitFor(() => { expect(appsService.updateAppInfo).toHaveBeenCalled() - expect(mockNotify).toHaveBeenCalledWith({ type: 'error', message: expect.stringContaining('Edit failed') }) + expect(toastMocks.record).toHaveBeenCalledWith({ type: 'error', message: expect.stringContaining('Edit failed') }) }) }) @@ -998,7 +1018,7 @@ describe('AppCard', () => { await waitFor(() => { expect(workflowService.fetchWorkflowDraft).toHaveBeenCalled() - expect(mockNotify).toHaveBeenCalledWith({ type: 'error', message: 'app.exportFailed' }) + expect(toastMocks.record).toHaveBeenCalledWith({ type: 'error', message: 'app.exportFailed' }) }) }) }) diff --git a/web/app/components/apps/app-card.tsx b/web/app/components/apps/app-card.tsx index 9a8abf6443..c1131ad2d4 100644 --- a/web/app/components/apps/app-card.tsx +++ b/web/app/components/apps/app-card.tsx @@ -10,14 +10,11 @@ import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill, RiVerifiedBadgeLi import * as React from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import { AppTypeIcon } from '@/app/components/app/type-selector' import AppIcon from '@/app/components/base/app-icon' import Divider from '@/app/components/base/divider' import CustomPopover from '@/app/components/base/popover' import TagSelector from '@/app/components/base/tag-management/selector' -import Toast from '@/app/components/base/toast' -import { ToastContext } from '@/app/components/base/toast/context' import Tooltip from '@/app/components/base/tooltip' import { AlertDialog, @@ -28,6 +25,7 @@ import { AlertDialogDescription, AlertDialogTitle, } from '@/app/components/base/ui/alert-dialog' +import { toast } from '@/app/components/base/ui/toast' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useAppContext } from '@/context/app-context' import { useGlobalPublicStore } from '@/context/global-public-context' @@ -71,7 +69,6 @@ export type AppCardProps = { const AppCard = ({ app, onRefresh }: AppCardProps) => { const { t } = useTranslation() - const { notify } = useContext(ToastContext) const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const { isCurrentWorkspaceEditor } = useAppContext() const { onPlanInfoChanged } = useProviderContext() @@ -90,20 +87,17 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { const onConfirmDelete = useCallback(async () => { try { await mutateDeleteApp(app.id) - notify({ type: 'success', message: t('appDeleted', { ns: 'app' }) }) + toast.success(t('appDeleted', { ns: 'app' })) onPlanInfoChanged() } catch (e: any) { - notify({ - type: 'error', - message: `${t('appDeleteFailed', { ns: 'app' })}${'message' in e ? `: ${e.message}` : ''}`, - }) + toast.error(`${t('appDeleteFailed', { ns: 'app' })}${'message' in e ? `: ${e.message}` : ''}`) } finally { setShowConfirmDelete(false) setConfirmDeleteInput('') } - }, [app.id, mutateDeleteApp, notify, onPlanInfoChanged, t]) + }, [app.id, mutateDeleteApp, onPlanInfoChanged, t]) const onDeleteDialogOpenChange = useCallback((open: boolean) => { if (isDeleting) @@ -135,20 +129,14 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { max_active_requests, }) setShowEditModal(false) - notify({ - type: 'success', - message: t('editDone', { ns: 'app' }), - }) + toast.success(t('editDone', { ns: 'app' })) if (onRefresh) onRefresh() } catch (e: any) { - notify({ - type: 'error', - message: e.message || t('editFailed', { ns: 'app' }), - }) + toast.error(e.message || t('editFailed', { ns: 'app' })) } - }, [app.id, notify, onRefresh, t]) + }, [app.id, onRefresh, t]) const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon_type, icon, icon_background }) => { try { @@ -161,10 +149,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { mode: app.mode, }) setShowDuplicateModal(false) - notify({ - type: 'success', - message: t('newApp.appCreated', { ns: 'app' }), - }) + toast.success(t('newApp.appCreated', { ns: 'app' })) localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') if (onRefresh) onRefresh() @@ -172,7 +157,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { getRedirection(isCurrentWorkspaceEditor, newApp, push) } catch { - notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) }) + toast.error(t('newApp.appCreateFailed', { ns: 'app' })) } } @@ -186,7 +171,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { downloadBlob({ data: file, fileName: `${app.name}.yml` }) } catch { - notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) }) + toast.error(t('exportFailed', { ns: 'app' })) } } @@ -205,7 +190,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { setSecretEnvList(list) } catch { - notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) }) + toast.error(t('exportFailed', { ns: 'app' })) } } @@ -274,13 +259,13 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { throw new Error('No app found in Explore') }, { onError: (err) => { - Toast.notify({ type: 'error', message: `${err.message || err}` }) + toast.error(`${err.message || err}`) }, }) } catch (e: unknown) { const message = e instanceof Error ? e.message : `${e}` - Toast.notify({ type: 'error', message }) + toast.error(message) } } return ( diff --git a/web/app/components/base/agent-log-modal/__tests__/detail.spec.tsx b/web/app/components/base/agent-log-modal/__tests__/detail.spec.tsx index 8b796435e0..6ce1e54a47 100644 --- a/web/app/components/base/agent-log-modal/__tests__/detail.spec.tsx +++ b/web/app/components/base/agent-log-modal/__tests__/detail.spec.tsx @@ -1,16 +1,32 @@ -import type { ComponentProps } from 'react' +import type { ComponentProps, ReactNode } from 'react' import type { IChatItem } from '@/app/components/base/chat/chat/type' import type { AgentLogDetailResponse } from '@/models/log' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { useStore as useAppStore } from '@/app/components/app/store' -import { ToastContext } from '@/app/components/base/toast/context' import { fetchAgentLogDetail } from '@/service/log' import AgentLogDetail from '../detail' +const { mockToast } = vi.hoisted(() => { + const mockToast = Object.assign(vi.fn(), { + success: vi.fn(), + error: vi.fn(), + warning: vi.fn(), + info: vi.fn(), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), + }) + return { mockToast } +}) + vi.mock('@/service/log', () => ({ fetchAgentLogDetail: vi.fn(), })) +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: mockToast, +})) + vi.mock('@/app/components/app/store', () => ({ useStore: vi.fn(selector => selector({ appDetail: { id: 'app-id' } })), })) @@ -22,7 +38,7 @@ vi.mock('@/app/components/workflow/run/status', () => ({ })) vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({ - default: ({ title, value }: { title: React.ReactNode, value: string | object }) => ( + default: ({ title, value }: { title: ReactNode, value: string | object }) => (
{title} {typeof value === 'string' ? value : JSON.stringify(value)} @@ -76,19 +92,13 @@ const createMockResponse = (overrides: Partial = {}): Ag }) describe('AgentLogDetail', () => { - const notify = vi.fn() - const renderComponent = (props: Partial> = {}) => { const defaultProps: ComponentProps = { conversationID: 'conv-id', messageID: 'msg-id', log: createMockLog(), } - return render( - ['value']}> - - , - ) + return render() } const renderAndWaitForData = async (props: Partial> = {}) => { @@ -212,10 +222,7 @@ describe('AgentLogDetail', () => { renderComponent() await waitFor(() => { - expect(notify).toHaveBeenCalledWith({ - type: 'error', - message: 'Error: API Error', - }) + expect(mockToast.error).toHaveBeenCalledWith('Error: API Error') }) }) diff --git a/web/app/components/base/agent-log-modal/__tests__/index.spec.tsx b/web/app/components/base/agent-log-modal/__tests__/index.spec.tsx index b2db524453..d1581c40b5 100644 --- a/web/app/components/base/agent-log-modal/__tests__/index.spec.tsx +++ b/web/app/components/base/agent-log-modal/__tests__/index.spec.tsx @@ -1,14 +1,30 @@ import type { IChatItem } from '@/app/components/base/chat/chat/type' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { useClickAway } from 'ahooks' -import { ToastContext } from '@/app/components/base/toast/context' import { fetchAgentLogDetail } from '@/service/log' import AgentLogModal from '../index' +const { mockToast } = vi.hoisted(() => { + const mockToast = Object.assign(vi.fn(), { + success: vi.fn(), + error: vi.fn(), + warning: vi.fn(), + info: vi.fn(), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), + }) + return { mockToast } +}) + vi.mock('@/service/log', () => ({ fetchAgentLogDetail: vi.fn(), })) +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: mockToast, +})) + vi.mock('@/app/components/app/store', () => ({ useStore: vi.fn(selector => selector({ appDetail: { id: 'app-id' } })), })) @@ -94,11 +110,7 @@ describe('AgentLogModal', () => { }) it('should render correctly when log item is provided', async () => { - render( - ['value']}> - - , - ) + render() expect(screen.getByText('appLog.runDetail.workflowTitle')).toBeInTheDocument() @@ -110,11 +122,7 @@ describe('AgentLogModal', () => { it('should call onCancel when close button is clicked', () => { vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {})) - render( - ['value']}> - - , - ) + render() const closeBtn = screen.getByRole('heading', { name: /appLog.runDetail.workflowTitle/i }).nextElementSibling! fireEvent.click(closeBtn) @@ -130,11 +138,7 @@ describe('AgentLogModal', () => { clickAwayHandler = callback }) - render( - ['value']}> - - , - ) + render() clickAwayHandler(new Event('click')) expect(mockProps.onCancel).toHaveBeenCalledTimes(1) @@ -150,11 +154,7 @@ describe('AgentLogModal', () => { } }) - render( - ['value']}> - - , - ) + render() expect(mockProps.onCancel).not.toHaveBeenCalled() }) diff --git a/web/app/components/base/agent-log-modal/detail.tsx b/web/app/components/base/agent-log-modal/detail.tsx index 21ed0be7e8..6550b305f8 100644 --- a/web/app/components/base/agent-log-modal/detail.tsx +++ b/web/app/components/base/agent-log-modal/detail.tsx @@ -7,10 +7,9 @@ import { flatten } from 'es-toolkit/compat' import * as React from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import { useStore as useAppStore } from '@/app/components/app/store' import Loading from '@/app/components/base/loading' -import { ToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import { fetchAgentLogDetail } from '@/service/log' import { cn } from '@/utils/classnames' import ResultPanel from './result' @@ -22,28 +21,19 @@ export type AgentLogDetailProps = { log: IChatItem messageID: string } - -const AgentLogDetail: FC = ({ - activeTab = 'DETAIL', - conversationID, - messageID, - log, -}) => { +const AgentLogDetail: FC = ({ activeTab = 'DETAIL', conversationID, messageID, log }) => { const { t } = useTranslation() - const { notify } = useContext(ToastContext) const [currentTab, setCurrentTab] = useState(activeTab) const appDetail = useAppStore(s => s.appDetail) const [loading, setLoading] = useState(true) const [runDetail, setRunDetail] = useState() const [list, setList] = useState([]) - const tools = useMemo(() => { const res = uniq(flatten(runDetail?.iterations.map((iteration) => { return iteration.tool_calls.map((tool: any) => tool.tool_name).filter(Boolean) })).filter(Boolean)) return res }, [runDetail]) - const getLogDetail = useCallback(async (appID: string, conversationID: string, messageID: string) => { try { const res = await fetchAgentLogDetail({ @@ -57,51 +47,30 @@ const AgentLogDetail: FC = ({ setList(res.iterations) } catch (err) { - notify({ - type: 'error', - message: `${err}`, - }) + toast.error(`${err}`) } - }, [notify]) - + }, []) const getData = async (appID: string, conversationID: string, messageID: string) => { setLoading(true) await getLogDetail(appID, conversationID, messageID) setLoading(false) } - const switchTab = async (tab: string) => { setCurrentTab(tab) } - useEffect(() => { // fetch data if (appDetail) getData(appDetail.id, conversationID, messageID) }, [appDetail, conversationID, messageID]) - return (
{/* tab */}
-
switchTab('DETAIL')} - > +
switchTab('DETAIL')}> {t('detail', { ns: 'runLog' })}
-
switchTab('TRACING')} - > +
switchTab('TRACING')}> {t('tracing', { ns: 'runLog' })}
@@ -112,29 +81,10 @@ const AgentLogDetail: FC = ({
)} - {!loading && currentTab === 'DETAIL' && runDetail && ( - - )} - {!loading && currentTab === 'TRACING' && ( - - )} + {!loading && currentTab === 'DETAIL' && runDetail && ()} + {!loading && currentTab === 'TRACING' && ()}
) } - export default AgentLogDetail diff --git a/web/app/components/base/agent-log-modal/index.stories.tsx b/web/app/components/base/agent-log-modal/index.stories.tsx index 87318848b4..e8b49600a5 100644 --- a/web/app/components/base/agent-log-modal/index.stories.tsx +++ b/web/app/components/base/agent-log-modal/index.stories.tsx @@ -3,7 +3,7 @@ import type { IChatItem } from '@/app/components/base/chat/chat/type' import type { AgentLogDetailResponse } from '@/models/log' import { useEffect, useRef } from 'react' import { useStore as useAppStore } from '@/app/components/app/store' -import { ToastProvider } from '@/app/components/base/toast' +import { ToastHost } from '@/app/components/base/ui/toast' import AgentLogModal from '.' const MOCK_RESPONSE: AgentLogDetailResponse = { @@ -109,7 +109,8 @@ const AgentLogModalDemo = ({ }, [setAppDetail]) return ( - + <> +
-
+ ) } diff --git a/web/app/components/base/audio-btn/__tests__/audio.spec.ts b/web/app/components/base/audio-btn/__tests__/audio.spec.ts index 00ffea2dfb..4399cb40fd 100644 --- a/web/app/components/base/audio-btn/__tests__/audio.spec.ts +++ b/web/app/components/base/audio-btn/__tests__/audio.spec.ts @@ -6,9 +6,9 @@ import AudioPlayer from '../audio' const mockToastNotify = vi.hoisted(() => vi.fn()) const mockTextToAudioStream = vi.hoisted(() => vi.fn()) -vi.mock('@/app/components/base/toast', () => ({ - default: { - notify: (...args: unknown[]) => mockToastNotify(...args), +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + error: (message: string) => mockToastNotify({ type: 'error', message }), }, })) diff --git a/web/app/components/base/audio-btn/audio.ts b/web/app/components/base/audio-btn/audio.ts index abfcad7c2f..5afe2bb656 100644 --- a/web/app/components/base/audio-btn/audio.ts +++ b/web/app/components/base/audio-btn/audio.ts @@ -1,4 +1,4 @@ -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { AppSourceType, textToAudioStream } from '@/service/share' declare global { @@ -7,7 +7,6 @@ declare global { ManagedMediaSource: any } } - export default class AudioPlayer { mediaSource: MediaSource | null audio: HTMLAudioElement @@ -22,7 +21,6 @@ export default class AudioPlayer { url: string isPublic: boolean callback: ((event: string) => void) | null - constructor(streamUrl: string, isPublic: boolean, msgId: string | undefined, msgContent: string | null | undefined, voice: string | undefined, callback: ((event: string) => void) | null) { this.audioContext = new AudioContext() this.msgId = msgId @@ -31,14 +29,10 @@ export default class AudioPlayer { this.isPublic = isPublic this.voice = voice this.callback = callback - // Compatible with iphone ios17 ManagedMediaSource const MediaSource = window.ManagedMediaSource || window.MediaSource if (!MediaSource) { - Toast.notify({ - message: 'Your browser does not support audio streaming, if you are using an iPhone, please update to iOS 17.1 or later.', - type: 'error', - }) + toast.error('Your browser does not support audio streaming, if you are using an iPhone, please update to iOS 17.1 or later.') } this.mediaSource = MediaSource ? new MediaSource() : null this.audio = new Audio() @@ -49,7 +43,6 @@ export default class AudioPlayer { } this.audio.src = this.mediaSource ? URL.createObjectURL(this.mediaSource) : '' this.audio.autoplay = true - const source = this.audioContext.createMediaElementSource(this.audio) source.connect(this.audioContext.destination) this.listenMediaSource('audio/mpeg') @@ -63,7 +56,6 @@ export default class AudioPlayer { this.mediaSource?.addEventListener('sourceopen', () => { if (this.sourceBuffer) return - this.sourceBuffer = this.mediaSource?.addSourceBuffer(contentType) }) } @@ -106,22 +98,18 @@ export default class AudioPlayer { voice: this.voice, text: this.msgContent, }) - if (audioResponse.status !== 200) { this.isLoadData = false if (this.callback) this.callback('error') } - const reader = audioResponse.body.getReader() while (true) { const { value, done } = await reader.read() - if (done) { this.receiveAudioData(value) break } - this.receiveAudioData(value) } } @@ -167,7 +155,6 @@ export default class AudioPlayer { this.theEndOfStream() clearInterval(timer) } - if (this.cacheBuffers.length && !this.sourceBuffer?.updating) { const arrayBuffer = this.cacheBuffers.shift()! this.sourceBuffer?.appendBuffer(arrayBuffer) @@ -180,7 +167,6 @@ export default class AudioPlayer { this.finishStream() return } - const audioContent = Buffer.from(audio, 'base64') this.receiveAudioData(new Uint8Array(audioContent)) if (play) { @@ -196,7 +182,6 @@ export default class AudioPlayer { this.callback?.('play') } else if (this.audio.played) { /* empty */ } - else { this.audio.play() this.callback?.('play') @@ -221,7 +206,6 @@ export default class AudioPlayer { this.finishStream() return } - if (this.sourceBuffer?.updating) { this.cacheBuffers.push(audioData) } diff --git a/web/app/components/base/audio-gallery/AudioPlayer.tsx b/web/app/components/base/audio-gallery/AudioPlayer.tsx index cbf50ddc13..5a0a753ecf 100644 --- a/web/app/components/base/audio-gallery/AudioPlayer.tsx +++ b/web/app/components/base/audio-gallery/AudioPlayer.tsx @@ -1,7 +1,7 @@ import { t } from 'i18next' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import useTheme from '@/hooks/use-theme' import { Theme } from '@/types/app' import { cn } from '@/utils/classnames' @@ -10,7 +10,6 @@ type AudioPlayerProps = { src?: string // Keep backward compatibility srcs?: string[] // Support multiple sources } - const AudioPlayer: React.FC = ({ src, srcs }) => { const [isPlaying, setIsPlaying] = useState(false) const [currentTime, setCurrentTime] = useState(0) @@ -23,43 +22,34 @@ const AudioPlayer: React.FC = ({ src, srcs }) => { const [hoverTime, setHoverTime] = useState(0) const [isAudioAvailable, setIsAudioAvailable] = useState(true) const { theme } = useTheme() - useEffect(() => { const audio = audioRef.current /* v8 ignore next 2 - @preserve */ if (!audio) return - const handleError = () => { setIsAudioAvailable(false) } - const setAudioData = () => { setDuration(audio.duration) } - const setAudioTime = () => { setCurrentTime(audio.currentTime) } - const handleProgress = () => { if (audio.buffered.length > 0) setBufferedTime(audio.buffered.end(audio.buffered.length - 1)) } - const handleEnded = () => { setIsPlaying(false) } - audio.addEventListener('loadedmetadata', setAudioData) audio.addEventListener('timeupdate', setAudioTime) audio.addEventListener('progress', handleProgress) audio.addEventListener('ended', handleEnded) audio.addEventListener('error', handleError) - // Preload audio metadata audio.load() - // Use the first source or src to generate waveform const primarySrc = srcs?.[0] || src if (primarySrc) { @@ -76,17 +66,12 @@ const AudioPlayer: React.FC = ({ src, srcs }) => { } } }, [src, srcs]) - const generateWaveformData = async (audioSrc: string) => { if (!window.AudioContext && !(window as any).webkitAudioContext) { setIsAudioAvailable(false) - Toast.notify({ - type: 'error', - message: 'Web Audio API is not supported in this browser', - }) + toast.error('Web Audio API is not supported in this browser') return null } - const primarySrc = srcs?.[0] || src const url = primarySrc ? new URL(primarySrc) : null const isHttp = url ? (url.protocol === 'http:' || url.protocol === 'https:') : false @@ -94,53 +79,43 @@ const AudioPlayer: React.FC = ({ src, srcs }) => { setIsAudioAvailable(false) return null } - const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)() const samples = 70 - try { const response = await fetch(audioSrc, { mode: 'cors' }) if (!response || !response.ok) { setIsAudioAvailable(false) return null } - const arrayBuffer = await response.arrayBuffer() const audioBuffer = await audioContext.decodeAudioData(arrayBuffer) const channelData = audioBuffer.getChannelData(0) const blockSize = Math.floor(channelData.length / samples) const waveformData: number[] = [] - for (let i = 0; i < samples; i++) { let sum = 0 for (let j = 0; j < blockSize; j++) sum += Math.abs(channelData[i * blockSize + j]) - // Apply nonlinear scaling to enhance small amplitudes waveformData.push((sum / blockSize) * 5) } - // Normalized waveform data const maxAmplitude = Math.max(...waveformData) const normalizedWaveform = waveformData.map(amp => amp / maxAmplitude) - setWaveformData(normalizedWaveform) setIsAudioAvailable(true) } catch { const waveform: number[] = [] let prevValue = Math.random() - for (let i = 0; i < samples; i++) { const targetValue = Math.random() const interpolatedValue = prevValue + (targetValue - prevValue) * 0.3 waveform.push(interpolatedValue) prevValue = interpolatedValue } - const maxAmplitude = Math.max(...waveform) const randomWaveform = waveform.map(amp => amp / maxAmplitude) - setWaveformData(randomWaveform) setIsAudioAvailable(true) } @@ -148,7 +123,6 @@ const AudioPlayer: React.FC = ({ src, srcs }) => { await audioContext.close() } } - const togglePlay = useCallback(() => { const audio = audioRef.current if (audio && isAudioAvailable) { @@ -160,99 +134,75 @@ const AudioPlayer: React.FC = ({ src, srcs }) => { setHasStartedPlaying(true) audio.play().catch(error => console.error('Error playing audio:', error)) } - setIsPlaying(!isPlaying) } else { - Toast.notify({ - type: 'error', - message: 'Audio element not found', - }) + toast.error('Audio element not found') setIsAudioAvailable(false) } }, [isAudioAvailable, isPlaying]) - const handleCanvasInteraction = useCallback((e: React.MouseEvent | React.TouchEvent) => { e.preventDefault() - const getClientX = (event: React.MouseEvent | React.TouchEvent): number => { if ('touches' in event) return event.touches[0].clientX return event.clientX } - const updateProgress = (clientX: number) => { const canvas = canvasRef.current const audio = audioRef.current if (!canvas || !audio) return - const rect = canvas.getBoundingClientRect() const percent = Math.min(Math.max(0, clientX - rect.left), rect.width) / rect.width const newTime = percent * duration - // Removes the buffer check, allowing drag to any location audio.currentTime = newTime setCurrentTime(newTime) - if (!isPlaying) { setIsPlaying(true) audio.play().catch((error) => { - Toast.notify({ - type: 'error', - message: `Error playing audio: ${error}`, - }) + toast.error(`Error playing audio: ${error}`) setIsPlaying(false) }) } } - updateProgress(getClientX(e)) }, [duration, isPlaying]) - const formatTime = (time: number) => { const minutes = Math.floor(time / 60) const seconds = Math.floor(time % 60) return `${minutes}:${seconds.toString().padStart(2, '0')}` } - const drawWaveform = useCallback(() => { const canvas = canvasRef.current /* v8 ignore next 2 - @preserve */ if (!canvas) return - const ctx = canvas.getContext('2d') if (!ctx) return - const width = canvas.width const height = canvas.height const data = waveformData - ctx.clearRect(0, 0, width, height) - const barWidth = width / data.length const playedWidth = (currentTime / duration) * width const cornerRadius = 2 - // Draw waveform bars data.forEach((value, index) => { let color - if (index * barWidth <= playedWidth) color = theme === Theme.light ? '#296DFF' : '#84ABFF' else if ((index * barWidth / width) * duration <= hoverTime) color = theme === Theme.light ? 'rgba(21,90,239,.40)' : 'rgba(200, 206, 218, 0.28)' else color = theme === Theme.light ? 'rgba(21,90,239,.20)' : 'rgba(200, 206, 218, 0.14)' - const barHeight = value * height const rectX = index * barWidth const rectY = (height - barHeight) / 2 const rectWidth = barWidth * 0.5 const rectHeight = barHeight - ctx.lineWidth = 1 ctx.fillStyle = color if (ctx.roundRect) { @@ -265,27 +215,22 @@ const AudioPlayer: React.FC = ({ src, srcs }) => { } }) }, [currentTime, duration, hoverTime, theme, waveformData]) - useEffect(() => { drawWaveform() }, [drawWaveform, bufferedTime, hasStartedPlaying]) - const handleMouseMove = useCallback((e: React.MouseEvent | React.TouchEvent) => { const canvas = canvasRef.current const audio = audioRef.current if (!canvas || !audio) return - const clientX = 'touches' in e ? e.touches[0]?.clientX ?? e.changedTouches[0]?.clientX : e.clientX if (clientX === undefined) return - const rect = canvas.getBoundingClientRect() const percent = Math.min(Math.max(0, clientX - rect.left), rect.width) / rect.width const time = percent * duration - // Check if the hovered position is within a buffered range before updating hoverTime for (let i = 0; i < audio.buffered.length; i++) { if (time >= audio.buffered.start(i) && time <= audio.buffered.end(i)) { @@ -294,38 +239,20 @@ const AudioPlayer: React.FC = ({ src, srcs }) => { } } }, [duration]) - return (
- ) } - const placeholder = '' const editAreaClassName = 'focus:outline-none bg-transparent text-sm' - const textAreaContent = (
!readonly && setIsEditing(true)}> {isEditing @@ -134,10 +105,10 @@ const BlockInput: FC = ({ onBlur={() => { blur() setIsEditing(false) - // click confirm also make blur. Then outer value is change. So below code has problem. - // setTimeout(() => { - // handleCancel() - // }, 1000) + // click confirm also make blur. Then outer value is change. So below code has problem. + // setTimeout(() => { + // handleCancel() + // }, 1000) }} />
@@ -145,7 +116,6 @@ const BlockInput: FC = ({ : }
) - return (
{textAreaContent} @@ -159,5 +129,4 @@ const BlockInput: FC = ({
) } - export default React.memo(BlockInput) diff --git a/web/app/components/base/chat/chat-with-history/__tests__/hooks.spec.tsx b/web/app/components/base/chat/chat-with-history/__tests__/hooks.spec.tsx index b004a1bee6..f4c8ef0c45 100644 --- a/web/app/components/base/chat/chat-with-history/__tests__/hooks.spec.tsx +++ b/web/app/components/base/chat/chat-with-history/__tests__/hooks.spec.tsx @@ -4,7 +4,7 @@ import type { InstalledApp } from '@/models/explore' import type { AppConversationData, AppData, AppMeta, ConversationItem } from '@/models/share' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { act, renderHook, waitFor } from '@testing-library/react' -import { ToastProvider } from '@/app/components/base/toast' +import { ToastHost } from '@/app/components/base/ui/toast' import { AppSourceType, delConversation, @@ -95,7 +95,8 @@ const createQueryClient = () => new QueryClient({ const createWrapper = (queryClient: QueryClient) => { return ({ children }: { children: ReactNode }) => ( - {children} + + {children} ) } diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index 23936111ce..e6f5657ff5 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -1,47 +1,21 @@ import type { ExtraContent } from '../chat/type' -import type { - Callback, - ChatConfig, - ChatItem, - Feedback, -} from '../types' +import type { Callback, ChatConfig, ChatItem, Feedback } from '../types' import type { InstalledApp } from '@/models/explore' -import type { - AppData, - ConversationItem, -} from '@/models/share' +import type { AppData, ConversationItem } from '@/models/share' import type { HumanInputFilledFormData, HumanInputFormData } from '@/types/workflow' import { useLocalStorageState } from 'ahooks' import { noop } from 'es-toolkit/function' import { produce } from 'immer' -import { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' -import { useToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import { InputVarType } from '@/app/components/workflow/types' import { useWebAppStore } from '@/context/web-app-context' import { useAppFavicon } from '@/hooks/use-app-favicon' import { changeLanguage } from '@/i18n-config/client' -import { - AppSourceType, - delConversation, - pinConversation, - renameConversation, - unpinConversation, - updateFeedback, -} from '@/service/share' -import { - useInvalidateShareConversations, - useShareChatList, - useShareConversationName, - useShareConversations, -} from '@/service/use-share' +import { AppSourceType, delConversation, pinConversation, renameConversation, unpinConversation, updateFeedback } from '@/service/share' +import { useInvalidateShareConversations, useShareChatList, useShareConversationName, useShareConversations } from '@/service/use-share' import { TransferMethod } from '@/types/app' import { addFileInfos, sortAgentSorts } from '../../../tools/utils' import { CONVERSATION_ID_INFO } from '../constants' @@ -93,14 +67,12 @@ function getFormattedChatList(messages: any[]) { }) return newChatList } - export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp const appInfo = useWebAppStore(s => s.appInfo) const appParams = useWebAppStore(s => s.appParams) const appMeta = useWebAppStore(s => s.appMeta) - useAppFavicon({ enable: !installedAppInfo, icon_type: appInfo?.site.icon_type, @@ -108,7 +80,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { icon_background: appInfo?.site.icon_background, icon_url: appInfo?.site.icon_url, }) - const appData = useMemo(() => { if (isInstalledApp) { const { id, app } = installedAppInfo! @@ -129,18 +100,15 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { custom_config: null, } as AppData } - return appInfo }, [isInstalledApp, installedAppInfo, appInfo]) const appId = useMemo(() => appData?.app_id, [appData]) - const [userId, setUserId] = useState() useEffect(() => { getProcessedSystemVariablesFromUrlParams().then(({ user_id }) => { setUserId(user_id) }) }, []) - useEffect(() => { const setLocaleFromProps = async () => { if (appData?.site.default_language) @@ -148,7 +116,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { } setLocaleFromProps() }, [appData]) - const [sidebarCollapseState, setSidebarCollapseState] = useState(() => { if (typeof window !== 'undefined') { try { @@ -192,15 +159,12 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { }) } }, [appId, conversationIdInfo, setConversationIdInfo, userId]) - const [newConversationId, setNewConversationId] = useState('') const chatShouldReloadKey = useMemo(() => { if (currentConversationId === newConversationId) return '' - return currentConversationId }, [currentConversationId, newConversationId]) - const { data: appPinnedConversationData } = useShareConversations({ appSourceType, appId, @@ -211,10 +175,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { refetchOnWindowFocus: false, refetchOnReconnect: false, }) - const { - data: appConversationData, - isLoading: appConversationDataLoading, - } = useShareConversations({ + const { data: appConversationData, isLoading: appConversationDataLoading } = useShareConversations({ appSourceType, appId, pinned: false, @@ -224,10 +185,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { refetchOnWindowFocus: false, refetchOnReconnect: false, }) - const { - data: appChatListData, - isLoading: appChatListDataLoading, - } = useShareChatList({ + const { data: appChatListData, isLoading: appChatListDataLoading } = useShareChatList({ conversationId: chatShouldReloadKey, appSourceType, appId, @@ -237,18 +195,12 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { refetchOnReconnect: false, }) const invalidateShareConversations = useInvalidateShareConversations() - const [clearChatList, setClearChatList] = useState(false) const [isResponding, setIsResponding] = useState(false) - const appPrevChatTree = useMemo( - () => (currentConversationId && appChatListData?.data.length) - ? buildChatItemTree(getFormattedChatList(appChatListData.data)) - : [], - [appChatListData, currentConversationId], - ) - + const appPrevChatTree = useMemo(() => (currentConversationId && appChatListData?.data.length) + ? buildChatItemTree(getFormattedChatList(appChatListData.data)) + : [], [appChatListData, currentConversationId]) const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false) - const pinnedConversationList = useMemo(() => { return appPinnedConversationData?.data || [] }, [appPinnedConversationData]) @@ -267,7 +219,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { let value = initInputs[item.paragraph.variable] if (value && item.paragraph.max_length && value.length > item.paragraph.max_length) value = value.slice(0, item.paragraph.max_length) - return { ...item.paragraph, default: value || item.default || item.paragraph.default, @@ -282,7 +233,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { type: 'number', } } - if (item.checkbox) { const preset = initInputs[item.checkbox.variable] === true return { @@ -291,7 +241,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { type: 'checkbox', } } - if (item.select) { const isInputInOptions = item.select.options.includes(initInputs[item.select.variable]) return { @@ -300,32 +249,27 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { type: 'select', } } - if (item['file-list']) { return { ...item['file-list'], type: 'file-list', } } - if (item.file) { return { ...item.file, type: 'file', } } - if (item.json_object) { return { ...item.json_object, type: 'json_object', } } - let value = initInputs[item['text-input'].variable] if (value && item['text-input'].max_length && value.length > item['text-input'].max_length) value = value.slice(0, item['text-input'].max_length) - return { ...item['text-input'], default: value || item.default || item['text-input'].default, @@ -333,11 +277,9 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { } }) }, [initInputs, appParams]) - const allInputsHidden = useMemo(() => { return inputsForms.length > 0 && inputsForms.every(item => item.hide === true) }, [inputsForms]) - useEffect(() => { // init inputs from url params (async () => { @@ -347,16 +289,13 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { setInitUserVariables(userVariables) })() }, []) - useEffect(() => { const conversationInputs: Record = {} - inputsForms.forEach((item: any) => { conversationInputs[item.variable] = item.default || null }) handleNewConversationInputsChange(conversationInputs) }, [handleNewConversationInputsChange, inputsForms]) - const { data: newConversation } = useShareConversationName({ conversationId: newConversationId, appSourceType, @@ -372,7 +311,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { }, [appConversationData, appConversationDataLoading]) const conversationList = useMemo(() => { const data = originConversationList.slice() - if (showNewConversationItemInList && data[0]?.id !== '') { data.unshift({ id: '', @@ -383,12 +321,10 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { } return data }, [originConversationList, showNewConversationItemInList, t]) - useEffect(() => { if (newConversation) { setOriginConversationList(produce((draft) => { const index = draft.findIndex(item => item.id === newConversation.id) - if (index > -1) draft[index] = newConversation else @@ -396,16 +332,12 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { })) } }, [newConversation]) - const currentConversationItem = useMemo(() => { let conversationItem = conversationList.find(item => item.id === currentConversationId) - if (!conversationItem && pinnedConversationList.length) conversationItem = pinnedConversationList.find(item => item.id === currentConversationId) - return conversationItem }, [conversationList, currentConversationId, pinnedConversationList]) - const currentConversationLatestInputs = useMemo(() => { if (!currentConversationId || !appChatListData?.data.length) return newConversationInputsRef.current || {} @@ -416,12 +348,9 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { if (currentConversationItem) setCurrentConversationInputs(currentConversationLatestInputs || {}) }, [currentConversationItem, currentConversationLatestInputs]) - - const { notify } = useToastContext() const checkInputsRequired = useCallback((silent?: boolean) => { if (allInputsHidden) return true - let hasEmptyInput = '' let fileIsUploading = false const requiredVars = inputsForms.filter(({ required, type }) => required && type !== InputVarType.checkbox) @@ -429,13 +358,10 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { requiredVars.forEach(({ variable, label, type }) => { if (hasEmptyInput) return - if (fileIsUploading) return - if (!newConversationInputsRef.current[variable] && !silent) hasEmptyInput = label as string - if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && newConversationInputsRef.current[variable] && !silent) { const files = newConversationInputsRef.current[variable] if (Array.isArray(files)) @@ -445,26 +371,25 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { } }) } - if (hasEmptyInput) { - notify({ type: 'error', message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: hasEmptyInput }) }) + toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: hasEmptyInput })) return false } - if (fileIsUploading) { - notify({ type: 'info', message: t('errorMessage.waitForFileUpload', { ns: 'appDebug' }) }) + toast.info(t('errorMessage.waitForFileUpload', { ns: 'appDebug' })) return } - return true - }, [inputsForms, notify, t, allInputsHidden]) + }, [inputsForms, t, allInputsHidden]) const handleStartChat = useCallback((callback: any) => { if (checkInputsRequired()) { setShowNewConversationItemInList(true) callback?.() } }, [setShowNewConversationItemInList, checkInputsRequired]) - const currentChatInstanceRef = useRef<{ handleStop: () => void }>({ handleStop: noop }) + const currentChatInstanceRef = useRef<{ + handleStop: () => void + }>({ handleStop: noop }) const handleChangeConversation = useCallback((conversationId: string) => { currentChatInstanceRef.current.handleStop() setNewConversationId('') @@ -486,76 +411,48 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { const handleUpdateConversationList = useCallback(() => { invalidateShareConversations() }, [invalidateShareConversations]) - const handlePinConversation = useCallback(async (conversationId: string) => { await pinConversation(appSourceType, appId, conversationId) - notify({ type: 'success', message: t('api.success', { ns: 'common' }) }) + toast.success(t('api.success', { ns: 'common' })) handleUpdateConversationList() - }, [appSourceType, appId, notify, t, handleUpdateConversationList]) - + }, [appSourceType, appId, t, handleUpdateConversationList]) const handleUnpinConversation = useCallback(async (conversationId: string) => { await unpinConversation(appSourceType, appId, conversationId) - notify({ type: 'success', message: t('api.success', { ns: 'common' }) }) + toast.success(t('api.success', { ns: 'common' })) handleUpdateConversationList() - }, [appSourceType, appId, notify, t, handleUpdateConversationList]) - + }, [appSourceType, appId, t, handleUpdateConversationList]) const [conversationDeleting, setConversationDeleting] = useState(false) - const handleDeleteConversation = useCallback(async ( - conversationId: string, - { - onSuccess, - }: Callback, - ) => { + const handleDeleteConversation = useCallback(async (conversationId: string, { onSuccess }: Callback) => { if (conversationDeleting) return - try { setConversationDeleting(true) await delConversation(appSourceType, appId, conversationId) - notify({ type: 'success', message: t('api.success', { ns: 'common' }) }) + toast.success(t('api.success', { ns: 'common' })) onSuccess() } finally { setConversationDeleting(false) } - if (conversationId === currentConversationId) handleNewConversation() - handleUpdateConversationList() - }, [isInstalledApp, appId, notify, t, handleUpdateConversationList, handleNewConversation, currentConversationId, conversationDeleting]) - + }, [isInstalledApp, appId, t, handleUpdateConversationList, handleNewConversation, currentConversationId, conversationDeleting]) const [conversationRenaming, setConversationRenaming] = useState(false) - const handleRenameConversation = useCallback(async ( - conversationId: string, - newName: string, - { - onSuccess, - }: Callback, - ) => { + const handleRenameConversation = useCallback(async (conversationId: string, newName: string, { onSuccess }: Callback) => { if (conversationRenaming) return - if (!newName.trim()) { - notify({ - type: 'error', - message: t('chat.conversationNameCanNotEmpty', { ns: 'common' }), - }) + toast.error(t('chat.conversationNameCanNotEmpty', { ns: 'common' })) return } - setConversationRenaming(true) try { await renameConversation(appSourceType, appId, conversationId, newName) - - notify({ - type: 'success', - message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }), - }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) setOriginConversationList(produce((draft) => { const index = originConversationList.findIndex(item => item.id === conversationId) const item = draft[index] - draft[index] = { ...item, name: newName, @@ -566,20 +463,17 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { finally { setConversationRenaming(false) } - }, [isInstalledApp, appId, notify, t, conversationRenaming, originConversationList]) - + }, [isInstalledApp, appId, t, conversationRenaming, originConversationList]) const handleNewConversationCompleted = useCallback((newConversationId: string) => { setNewConversationId(newConversationId) handleConversationIdInfoChange(newConversationId) setShowNewConversationItemInList(false) invalidateShareConversations() }, [handleConversationIdInfoChange, invalidateShareConversations]) - const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => { await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, appSourceType, appId) - notify({ type: 'success', message: t('api.success', { ns: 'common' }) }) - }, [appSourceType, appId, t, notify]) - + toast.success(t('api.success', { ns: 'common' })) + }, [appSourceType, appId, t]) return { isInstalledApp, appId, diff --git a/web/app/components/base/chat/chat/__tests__/check-input-forms-hooks.spec.tsx b/web/app/components/base/chat/chat/__tests__/check-input-forms-hooks.spec.tsx index 6afbc26582..1e96c1f798 100644 --- a/web/app/components/base/chat/chat/__tests__/check-input-forms-hooks.spec.tsx +++ b/web/app/components/base/chat/chat/__tests__/check-input-forms-hooks.spec.tsx @@ -5,8 +5,16 @@ import { TransferMethod } from '@/types/app' import { useCheckInputsForms } from '../check-input-forms-hooks' const mockNotify = vi.fn() -vi.mock('@/app/components/base/toast/context', () => ({ - useToastContext: () => ({ notify: mockNotify }), +vi.mock('@/app/components/base/ui/toast', () => ({ + default: { + notify: (args: unknown) => mockNotify(args), + }, + toast: { + success: (message: string) => mockNotify({ type: 'success', message }), + error: (message: string) => mockNotify({ type: 'error', message }), + warning: (message: string) => mockNotify({ type: 'warning', message }), + info: (message: string) => mockNotify({ type: 'info', message }), + }, })) describe('useCheckInputsForms', () => { diff --git a/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx b/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx index 92fa9ea42e..89327341de 100644 --- a/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx +++ b/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx @@ -20,8 +20,14 @@ vi.mock('@/app/components/base/audio-btn/audio.player.manager', () => ({ }, })) -vi.mock('@/app/components/base/toast/context', () => ({ - useToastContext: () => ({ notify: vi.fn() }), +vi.mock('@/app/components/base/ui/toast', () => ({ + default: { notify: vi.fn() }, + toast: { + success: vi.fn(), + error: vi.fn(), + warning: vi.fn(), + info: vi.fn(), + }, })) vi.mock('@/hooks/use-timestamp', () => ({ diff --git a/web/app/components/base/chat/chat/__tests__/question.spec.tsx b/web/app/components/base/chat/chat/__tests__/question.spec.tsx index e9392adb8a..9d49be3a15 100644 --- a/web/app/components/base/chat/chat/__tests__/question.spec.tsx +++ b/web/app/components/base/chat/chat/__tests__/question.spec.tsx @@ -5,7 +5,7 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import copy from 'copy-to-clipboard' import * as React from 'react' -import Toast from '../../../toast' +import { toast } from '@/app/components/base/ui/toast' import { ThemeBuilder } from '../../embedded-chatbot/theme/theme-context' import { ChatContextProvider } from '../context-provider' import Question from '../question' @@ -179,7 +179,7 @@ describe('Question component', () => { it('should call copy-to-clipboard and show a toast when copy action is clicked', async () => { const user = userEvent.setup() - const toastSpy = vi.spyOn(Toast, 'notify') + const toastSpy = vi.spyOn(toast, 'success').mockReturnValue('toast-success') renderWithProvider(makeItem()) diff --git a/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx b/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx index 836397a586..588b261323 100644 --- a/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx +++ b/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx @@ -29,7 +29,7 @@ const { vi.mock('copy-to-clipboard', () => ({ default: vi.fn() })) -vi.mock('@/app/components/base/toast', () => ({ +vi.mock('@/app/components/base/ui/toast', () => ({ default: { notify: vi.fn() }, })) diff --git a/web/app/components/base/chat/chat/answer/operation.tsx b/web/app/components/base/chat/chat/answer/operation.tsx index f0d077975c..26a4b6bd99 100644 --- a/web/app/components/base/chat/chat/answer/operation.tsx +++ b/web/app/components/base/chat/chat/answer/operation.tsx @@ -17,8 +17,8 @@ import AnnotationCtrlButton from '@/app/components/base/features/new-feature-pan import Modal from '@/app/components/base/modal/modal' import NewAudioButton from '@/app/components/base/new-audio-button' import Textarea from '@/app/components/base/textarea' -import Toast from '@/app/components/base/toast' import Tooltip from '@/app/components/base/tooltip' +import { toast } from '@/app/components/base/ui/toast' import { cn } from '@/utils/classnames' import { useChatContext } from '../context' @@ -302,7 +302,7 @@ const Operation: FC = ({ { copy(content) - Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.copySuccessfully', { ns: 'common' })) }} data-testid="copy-btn" > diff --git a/web/app/components/base/chat/chat/chat-input-area/__tests__/index.spec.tsx b/web/app/components/base/chat/chat/chat-input-area/__tests__/index.spec.tsx index f628b7de82..1a8dd55f61 100644 --- a/web/app/components/base/chat/chat/chat-input-area/__tests__/index.spec.tsx +++ b/web/app/components/base/chat/chat/chat-input-area/__tests__/index.spec.tsx @@ -175,8 +175,16 @@ vi.mock('@/app/components/base/features/hooks', () => ({ // --------------------------------------------------------------------------- // Toast context // --------------------------------------------------------------------------- -vi.mock('@/app/components/base/toast/context', () => ({ - useToastContext: () => ({ notify: mockNotify, close: vi.fn() }), +vi.mock('@/app/components/base/ui/toast', () => ({ + default: { + notify: (args: unknown) => mockNotify(args), + }, + toast: { + success: (message: string) => mockNotify({ type: 'success', message }), + error: (message: string) => mockNotify({ type: 'error', message }), + warning: (message: string) => mockNotify({ type: 'warning', message }), + info: (message: string) => mockNotify({ type: 'info', message }), + }, })) // --------------------------------------------------------------------------- diff --git a/web/app/components/base/chat/chat/chat-input-area/index.tsx b/web/app/components/base/chat/chat/chat-input-area/index.tsx index 8b5ca18585..0ea928d6d6 100644 --- a/web/app/components/base/chat/chat/chat-input-area/index.tsx +++ b/web/app/components/base/chat/chat/chat-input-area/index.tsx @@ -1,28 +1,18 @@ import type { Theme } from '../../embedded-chatbot/theme/theme-context' -import type { - EnableType, - OnSend, -} from '../../types' +import type { EnableType, OnSend } from '../../types' import type { InputForm } from '../type' import type { FileUpload } from '@/app/components/base/features/types' import { noop } from 'es-toolkit/function' import { decode } from 'html-entities' import Recorder from 'js-audio-recorder' -import { - useCallback, - useRef, - useState, -} from 'react' +import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import Textarea from 'react-textarea-autosize' import FeatureBar from '@/app/components/base/features/new-feature-panel/feature-bar' import { FileListInChatInput } from '@/app/components/base/file-uploader' import { useFile } from '@/app/components/base/file-uploader/hooks' -import { - FileContextProvider, - useFileStore, -} from '@/app/components/base/file-uploader/store' -import { useToastContext } from '@/app/components/base/toast/context' +import { FileContextProvider, useFileStore } from '@/app/components/base/file-uploader/store' +import { toast } from '@/app/components/base/ui/toast' import VoiceInput from '@/app/components/base/voice-input' import { TransferMethod } from '@/types/app' import { cn } from '@/utils/classnames' @@ -53,71 +43,34 @@ type ChatInputAreaProps = { */ sendOnEnter?: boolean } -const ChatInputArea = ({ - readonly, - botName, - showFeatureBar, - showFileUpload, - featureBarDisabled, - onFeatureBarClick, - visionConfig, - speechToTextConfig = { enabled: true }, - onSend, - inputs = {}, - inputsForm = [], - theme, - isResponding, - disabled, - sendOnEnter = true, -}: ChatInputAreaProps) => { +const ChatInputArea = ({ readonly, botName, showFeatureBar, showFileUpload, featureBarDisabled, onFeatureBarClick, visionConfig, speechToTextConfig = { enabled: true }, onSend, inputs = {}, inputsForm = [], theme, isResponding, disabled, sendOnEnter = true }: ChatInputAreaProps) => { const { t } = useTranslation() - const { notify } = useToastContext() - const { - wrapperRef, - textareaRef, - textValueRef, - holdSpaceRef, - handleTextareaResize, - isMultipleLine, - } = useTextAreaHeight() + const { wrapperRef, textareaRef, textValueRef, holdSpaceRef, handleTextareaResize, isMultipleLine } = useTextAreaHeight() const [query, setQuery] = useState('') const [showVoiceInput, setShowVoiceInput] = useState(false) const filesStore = useFileStore() - const { - handleDragFileEnter, - handleDragFileLeave, - handleDragFileOver, - handleDropFile, - handleClipboardPasteFile, - isDragActive, - } = useFile(visionConfig!, false) + const { handleDragFileEnter, handleDragFileLeave, handleDragFileOver, handleDropFile, handleClipboardPasteFile, isDragActive } = useFile(visionConfig!, false) const { checkInputsForm } = useCheckInputsForms() const historyRef = useRef(['']) const [currentIndex, setCurrentIndex] = useState(-1) const isComposingRef = useRef(false) - - const handleQueryChange = useCallback( - (value: string) => { - setQuery(value) - setTimeout(handleTextareaResize, 0) - }, - [handleTextareaResize], - ) - + const handleQueryChange = useCallback((value: string) => { + setQuery(value) + setTimeout(handleTextareaResize, 0) + }, [handleTextareaResize]) const handleSend = () => { if (isResponding) { - notify({ type: 'info', message: t('errorMessage.waitForResponse', { ns: 'appDebug' }) }) + toast.info(t('errorMessage.waitForResponse', { ns: 'appDebug' })) return } - if (onSend) { const { files, setFiles } = filesStore.getState() if (files.some(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId)) { - notify({ type: 'info', message: t('errorMessage.waitForFileUpload', { ns: 'appDebug' }) }) + toast.info(t('errorMessage.waitForFileUpload', { ns: 'appDebug' })) return } if (!query || !query.trim()) { - notify({ type: 'info', message: t('errorMessage.queryRequired', { ns: 'appAnnotation' }) }) + toast.info(t('errorMessage.queryRequired', { ns: 'appAnnotation' })) return } if (checkInputsForm(inputs, inputsForm)) { @@ -145,7 +98,6 @@ const ChatInputArea = ({ const isSendCombo = sendOnEnter ? (e.key === 'Enter' && !e.shiftKey) : (e.key === 'Enter' && e.shiftKey) - if (isSendCombo && !e.nativeEvent.isComposing) { // if isComposing, exit if (isComposingRef.current) @@ -176,101 +128,36 @@ const ChatInputArea = ({ } } } - const handleShowVoiceInput = useCallback(() => { (Recorder as any).getPermission().then(() => { setShowVoiceInput(true) }, () => { - notify({ type: 'error', message: t('voiceInput.notAllow', { ns: 'common' }) }) + toast.error(t('voiceInput.notAllow', { ns: 'common' })) }) - }, [t, notify]) - - const operation = ( - - ) - + }, [t]) + const operation = () return ( <> -
+
-
+
-
+
{query}
-
NAMEVALUE{t('env.export.name', { ns: 'workflow' })}{t('env.export.value', { ns: 'workflow' })}