diff --git a/.github/workflows/expose_service_ports.sh b/.github/workflows/expose_service_ports.sh index 01772ccf9f..fa0fd2ee8c 100755 --- a/.github/workflows/expose_service_ports.sh +++ b/.github/workflows/expose_service_ports.sh @@ -1,6 +1,7 @@ #!/bin/bash yq eval '.services.weaviate.ports += ["8080:8080"]' -i docker/docker-compose.yaml +yq eval '.services.weaviate.ports += ["50051:50051"]' -i docker/docker-compose.yaml yq eval '.services.qdrant.ports += ["6333:6333"]' -i docker/docker-compose.yaml yq eval '.services.chroma.ports += ["8000:8000"]' -i docker/docker-compose.yaml yq eval '.services["milvus-standalone"].ports += ["19530:19530"]' -i docker/docker-compose.yaml diff --git a/api/controllers/console/apikey.py b/api/controllers/console/apikey.py index efab1bc03c..7c39b04464 100644 --- a/api/controllers/console/apikey.py +++ b/api/controllers/console/apikey.py @@ -1,5 +1,4 @@ import flask_restx -from flask import Response from flask_restx import Resource, fields, marshal_with from flask_restx._http import HTTPStatus from sqlalchemy import select @@ -156,11 +155,6 @@ class AppApiKeyListResource(BaseApiKeyListResource): """Create a new API key for an app""" return super().post(resource_id) - def after_request(self, resp: Response): - resp.headers["Access-Control-Allow-Origin"] = "*" - resp.headers["Access-Control-Allow-Credentials"] = "true" - return resp - resource_type = "app" resource_model = App resource_id_field = "app_id" @@ -177,11 +171,6 @@ class AppApiKeyResource(BaseApiKeyResource): """Delete an API key for an app""" return super().delete(resource_id, api_key_id) - def after_request(self, resp): - resp.headers["Access-Control-Allow-Origin"] = "*" - resp.headers["Access-Control-Allow-Credentials"] = "true" - return resp - resource_type = "app" resource_model = App resource_id_field = "app_id" @@ -206,11 +195,6 @@ class DatasetApiKeyListResource(BaseApiKeyListResource): """Create a new API key for a dataset""" return super().post(resource_id) - def after_request(self, resp: Response): - resp.headers["Access-Control-Allow-Origin"] = "*" - resp.headers["Access-Control-Allow-Credentials"] = "true" - return resp - resource_type = "dataset" resource_model = Dataset resource_id_field = "dataset_id" @@ -227,11 +211,6 @@ class DatasetApiKeyResource(BaseApiKeyResource): """Delete an API key for a dataset""" return super().delete(resource_id, api_key_id) - def after_request(self, resp: Response): - resp.headers["Access-Control-Allow-Origin"] = "*" - resp.headers["Access-Control-Allow-Credentials"] = "true" - return resp - resource_type = "dataset" resource_model = Dataset resource_id_field = "dataset_id" diff --git a/api/core/entities/provider_configuration.py b/api/core/entities/provider_configuration.py index 29b8f8f610..b069955836 100644 --- a/api/core/entities/provider_configuration.py +++ b/api/core/entities/provider_configuration.py @@ -472,6 +472,9 @@ class ProviderConfiguration(BaseModel): provider_model_credentials_cache.delete() self.switch_preferred_provider_type(provider_type=ProviderType.CUSTOM, session=session) + else: + # some historical data may have a provider record but not be set as valid + provider_record.is_valid = True session.commit() except Exception: diff --git a/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py b/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py index 8820c0a846..4793d2bb50 100644 --- a/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py +++ b/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py @@ -1,9 +1,24 @@ +""" +Weaviate vector database implementation for Dify's RAG system. + +This module provides integration with Weaviate vector database for storing and retrieving +document embeddings used in retrieval-augmented generation workflows. +""" + import datetime import json +import logging +import uuid as _uuid from typing import Any +from urllib.parse import urlparse -import weaviate # type: ignore +import weaviate +import weaviate.classes.config as wc from pydantic import BaseModel, model_validator +from weaviate.classes.data import DataObject +from weaviate.classes.init import Auth +from weaviate.classes.query import Filter, MetadataQuery +from weaviate.exceptions import UnexpectedStatusCodeError from configs import dify_config from core.rag.datasource.vdb.field import Field @@ -15,265 +30,394 @@ from core.rag.models.document import Document from extensions.ext_redis import redis_client from models.dataset import Dataset +logger = logging.getLogger(__name__) + class WeaviateConfig(BaseModel): + """ + Configuration model for Weaviate connection settings. + + Attributes: + endpoint: Weaviate server endpoint URL + api_key: Optional API key for authentication + batch_size: Number of objects to batch per insert operation + """ + endpoint: str api_key: str | None = None batch_size: int = 100 @model_validator(mode="before") @classmethod - def validate_config(cls, values: dict): + def validate_config(cls, values: dict) -> dict: + """Validates that required configuration values are present.""" if not values["endpoint"]: raise ValueError("config WEAVIATE_ENDPOINT is required") return values class WeaviateVector(BaseVector): + """ + Weaviate vector database implementation for document storage and retrieval. + + Handles creation, insertion, deletion, and querying of document embeddings + in a Weaviate collection. + """ + def __init__(self, collection_name: str, config: WeaviateConfig, attributes: list): + """ + Initializes the Weaviate vector store. + + Args: + collection_name: Name of the Weaviate collection + config: Weaviate configuration settings + attributes: List of metadata attributes to store + """ super().__init__(collection_name) self._client = self._init_client(config) self._attributes = attributes - def _init_client(self, config: WeaviateConfig) -> weaviate.Client: - auth_config = weaviate.AuthApiKey(api_key=config.api_key or "") + def _init_client(self, config: WeaviateConfig) -> weaviate.WeaviateClient: + """ + Initializes and returns a connected Weaviate client. - weaviate.connect.connection.has_grpc = False # ty: ignore [unresolved-attribute] + Configures both HTTP and gRPC connections with proper authentication. + """ + p = urlparse(config.endpoint) + host = p.hostname or config.endpoint.replace("https://", "").replace("http://", "") + http_secure = p.scheme == "https" + http_port = p.port or (443 if http_secure else 80) - try: - client = weaviate.Client( - url=config.endpoint, auth_client_secret=auth_config, timeout_config=(5, 60), startup_period=None - ) - except Exception as exc: - raise ConnectionError("Vector database connection error") from exc + grpc_host = host + grpc_secure = http_secure + grpc_port = 443 if grpc_secure else 50051 - client.batch.configure( - # `batch_size` takes an `int` value to enable auto-batching - # (`None` is used for manual batching) - batch_size=config.batch_size, - # dynamically update the `batch_size` based on import speed - dynamic=True, - # `timeout_retries` takes an `int` value to retry on time outs - timeout_retries=3, + client = weaviate.connect_to_custom( + http_host=host, + http_port=http_port, + http_secure=http_secure, + grpc_host=grpc_host, + grpc_port=grpc_port, + grpc_secure=grpc_secure, + auth_credentials=Auth.api_key(config.api_key) if config.api_key else None, ) + if not client.is_ready(): + raise ConnectionError("Vector database is not ready") + return client def get_type(self) -> str: + """Returns the vector database type identifier.""" return VectorType.WEAVIATE def get_collection_name(self, dataset: Dataset) -> str: + """ + Retrieves or generates the collection name for a dataset. + + Uses existing index structure if available, otherwise generates from dataset ID. + """ if dataset.index_struct_dict: class_prefix: str = dataset.index_struct_dict["vector_store"]["class_prefix"] if not class_prefix.endswith("_Node"): - # original class_prefix class_prefix += "_Node" - return class_prefix dataset_id = dataset.id return Dataset.gen_collection_name_by_id(dataset_id) - def to_index_struct(self): + def to_index_struct(self) -> dict: + """Returns the index structure dictionary for persistence.""" return {"type": self.get_type(), "vector_store": {"class_prefix": self._collection_name}} def create(self, texts: list[Document], embeddings: list[list[float]], **kwargs): - # create collection + """ + Creates a new collection and adds initial documents with embeddings. + """ self._create_collection() - # create vector self.add_texts(texts, embeddings) def _create_collection(self): + """ + Creates the Weaviate collection with required schema if it doesn't exist. + + Uses Redis locking to prevent concurrent creation attempts. + """ lock_name = f"vector_indexing_lock_{self._collection_name}" with redis_client.lock(lock_name, timeout=20): - collection_exist_cache_key = f"vector_indexing_{self._collection_name}" - if redis_client.get(collection_exist_cache_key): + cache_key = f"vector_indexing_{self._collection_name}" + if redis_client.get(cache_key): return - schema = self._default_schema(self._collection_name) - if not self._client.schema.contains(schema): - # create collection - self._client.schema.create_class(schema) - redis_client.set(collection_exist_cache_key, 1, ex=3600) + + try: + if not self._client.collections.exists(self._collection_name): + self._client.collections.create( + name=self._collection_name, + properties=[ + wc.Property( + name=Field.TEXT_KEY.value, + data_type=wc.DataType.TEXT, + tokenization=wc.Tokenization.WORD, + ), + wc.Property(name="document_id", data_type=wc.DataType.TEXT), + wc.Property(name="doc_id", data_type=wc.DataType.TEXT), + wc.Property(name="chunk_index", data_type=wc.DataType.INT), + ], + vector_config=wc.Configure.Vectors.self_provided(), + ) + + self._ensure_properties() + redis_client.set(cache_key, 1, ex=3600) + except Exception as e: + logger.exception("Error creating collection %s", self._collection_name) + raise + + def _ensure_properties(self) -> None: + """ + Ensures all required properties exist in the collection schema. + + Adds missing properties if the collection exists but lacks them. + """ + if not self._client.collections.exists(self._collection_name): + return + + col = self._client.collections.use(self._collection_name) + cfg = col.config.get() + existing = {p.name for p in (cfg.properties or [])} + + to_add = [] + if "document_id" not in existing: + to_add.append(wc.Property(name="document_id", data_type=wc.DataType.TEXT)) + if "doc_id" not in existing: + to_add.append(wc.Property(name="doc_id", data_type=wc.DataType.TEXT)) + if "chunk_index" not in existing: + to_add.append(wc.Property(name="chunk_index", data_type=wc.DataType.INT)) + + for prop in to_add: + try: + col.config.add_property(prop) + except Exception as e: + logger.warning("Could not add property %s: %s", prop.name, e) + + def _get_uuids(self, documents: list[Document]) -> list[str]: + """ + Generates deterministic UUIDs for documents based on their content. + + Uses UUID5 with URL namespace to ensure consistent IDs for identical content. + """ + URL_NAMESPACE = _uuid.UUID("6ba7b811-9dad-11d1-80b4-00c04fd430c8") + + uuids = [] + for doc in documents: + uuid_val = _uuid.uuid5(URL_NAMESPACE, doc.page_content) + uuids.append(str(uuid_val)) + + return uuids def add_texts(self, documents: list[Document], embeddings: list[list[float]], **kwargs): + """ + Adds documents with their embeddings to the collection. + + Batches insertions for efficiency and returns the list of inserted object IDs. + """ uuids = self._get_uuids(documents) texts = [d.page_content for d in documents] metadatas = [d.metadata for d in documents] - ids = [] + col = self._client.collections.use(self._collection_name) + objs: list[DataObject] = [] + ids_out: list[str] = [] - with self._client.batch as batch: - for i, text in enumerate(texts): - data_properties = {Field.TEXT_KEY: text} - if metadatas is not None: - # metadata maybe None - for key, val in (metadatas[i] or {}).items(): - data_properties[key] = self._json_serializable(val) + for i, text in enumerate(texts): + props: dict[str, Any] = {Field.TEXT_KEY.value: text} + meta = metadatas[i] or {} + for k, v in meta.items(): + props[k] = self._json_serializable(v) - batch.add_data_object( - data_object=data_properties, - class_name=self._collection_name, - uuid=uuids[i], - vector=embeddings[i] if embeddings else None, + candidate = uuids[i] if uuids else None + uid = candidate if (candidate and self._is_uuid(candidate)) else str(_uuid.uuid4()) + ids_out.append(uid) + + vec_payload = None + if embeddings and i < len(embeddings) and embeddings[i]: + vec_payload = {"default": embeddings[i]} + + objs.append( + DataObject( + uuid=uid, + properties=props, # type: ignore[arg-type] # mypy incorrectly infers DataObject signature + vector=vec_payload, ) - ids.append(uuids[i]) - return ids + ) - def delete_by_metadata_field(self, key: str, value: str): - # check whether the index already exists - schema = self._default_schema(self._collection_name) - if self._client.schema.contains(schema): - where_filter = {"operator": "Equal", "path": [key], "valueText": value} + batch_size = max(1, int(dify_config.WEAVIATE_BATCH_SIZE or 100)) + with col.batch.dynamic() as batch: + for obj in objs: + batch.add_object(properties=obj.properties, uuid=obj.uuid, vector=obj.vector) - self._client.batch.delete_objects(class_name=self._collection_name, where=where_filter, output="minimal") + return ids_out + + def _is_uuid(self, val: str) -> bool: + """Validates whether a string is a valid UUID format.""" + try: + _uuid.UUID(str(val)) + return True + except Exception: + return False + + def delete_by_metadata_field(self, key: str, value: str) -> None: + """Deletes all objects matching a specific metadata field value.""" + if not self._client.collections.exists(self._collection_name): + return + + col = self._client.collections.use(self._collection_name) + col.data.delete_many(where=Filter.by_property(key).equal(value)) def delete(self): - # check whether the index already exists - schema = self._default_schema(self._collection_name) - if self._client.schema.contains(schema): - self._client.schema.delete_class(self._collection_name) + """Deletes the entire collection from Weaviate.""" + if self._client.collections.exists(self._collection_name): + self._client.collections.delete(self._collection_name) def text_exists(self, id: str) -> bool: - collection_name = self._collection_name - schema = self._default_schema(self._collection_name) - - # check whether the index already exists - if not self._client.schema.contains(schema): + """Checks if a document with the given doc_id exists in the collection.""" + if not self._client.collections.exists(self._collection_name): return False - result = ( - self._client.query.get(collection_name) - .with_additional(["id"]) - .with_where( - { - "path": ["doc_id"], - "operator": "Equal", - "valueText": id, - } - ) - .with_limit(1) - .do() + + col = self._client.collections.use(self._collection_name) + res = col.query.fetch_objects( + filters=Filter.by_property("doc_id").equal(id), + limit=1, + return_properties=["doc_id"], ) - if "errors" in result: - raise ValueError(f"Error during query: {result['errors']}") + return len(res.objects) > 0 - entries = result["data"]["Get"][collection_name] - if len(entries) == 0: - return False + def delete_by_ids(self, ids: list[str]) -> None: + """ + Deletes objects by their UUID identifiers. - return True + Silently ignores 404 errors for non-existent IDs. + """ + if not self._client.collections.exists(self._collection_name): + return - def delete_by_ids(self, ids: list[str]): - # check whether the index already exists - schema = self._default_schema(self._collection_name) - if self._client.schema.contains(schema): - for uuid in ids: - try: - self._client.data_object.delete( - class_name=self._collection_name, - uuid=uuid, - ) - except weaviate.UnexpectedStatusCodeException as e: - # tolerate not found error - if e.status_code != 404: - raise e + col = self._client.collections.use(self._collection_name) + + for uid in ids: + try: + col.data.delete_by_id(uid) + except UnexpectedStatusCodeError as e: + if getattr(e, "status_code", None) != 404: + raise def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]: - """Look up similar documents by embedding vector in Weaviate.""" - collection_name = self._collection_name - properties = self._attributes - properties.append(Field.TEXT_KEY) - query_obj = self._client.query.get(collection_name, properties) + """ + Performs vector similarity search using the provided query vector. - vector = {"vector": query_vector} - document_ids_filter = kwargs.get("document_ids_filter") - if document_ids_filter: - operands = [] - for document_id_filter in document_ids_filter: - operands.append({"path": ["document_id"], "operator": "Equal", "valueText": document_id_filter}) - where_filter = {"operator": "Or", "operands": operands} - query_obj = query_obj.with_where(where_filter) - result = ( - query_obj.with_near_vector(vector) - .with_limit(kwargs.get("top_k", 4)) - .with_additional(["vector", "distance"]) - .do() + Filters by document IDs if provided and applies score threshold. + Returns documents sorted by relevance score. + """ + if not self._client.collections.exists(self._collection_name): + return [] + + col = self._client.collections.use(self._collection_name) + props = list({*self._attributes, "document_id", Field.TEXT_KEY.value}) + + where = None + doc_ids = kwargs.get("document_ids_filter") or [] + if doc_ids: + ors = [Filter.by_property("document_id").equal(x) for x in doc_ids] + where = ors[0] + for f in ors[1:]: + where = where | f + + top_k = int(kwargs.get("top_k", 4)) + score_threshold = float(kwargs.get("score_threshold") or 0.0) + + res = col.query.near_vector( + near_vector=query_vector, + limit=top_k, + return_properties=props, + return_metadata=MetadataQuery(distance=True), + include_vector=False, + filters=where, + target_vector="default", ) - if "errors" in result: - raise ValueError(f"Error during query: {result['errors']}") - docs_and_scores = [] - for res in result["data"]["Get"][collection_name]: - text = res.pop(Field.TEXT_KEY) - score = 1 - res["_additional"]["distance"] - docs_and_scores.append((Document(page_content=text, metadata=res), score)) + docs: list[Document] = [] + for obj in res.objects: + properties = dict(obj.properties or {}) + text = properties.pop(Field.TEXT_KEY.value, "") + distance = (obj.metadata.distance if obj.metadata else None) or 1.0 + score = 1.0 - distance - docs = [] - for doc, score in docs_and_scores: - score_threshold = float(kwargs.get("score_threshold") or 0.0) - # check score threshold - if score >= score_threshold: - if doc.metadata is not None: - doc.metadata["score"] = score - docs.append(doc) - # Sort the documents by score in descending order - docs = sorted(docs, key=lambda x: x.metadata.get("score", 0) if x.metadata else 0, reverse=True) + if score > score_threshold: + properties["score"] = score + docs.append(Document(page_content=text, metadata=properties)) + + docs.sort(key=lambda d: d.metadata.get("score", 0.0), reverse=True) return docs def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]: - """Return docs using BM25F. - - Args: - query: Text to look up documents similar to. - - Returns: - List of Documents most similar to the query. """ - collection_name = self._collection_name - content: dict[str, Any] = {"concepts": [query]} - properties = self._attributes - properties.append(Field.TEXT_KEY) - if kwargs.get("search_distance"): - content["certainty"] = kwargs.get("search_distance") - query_obj = self._client.query.get(collection_name, properties) - document_ids_filter = kwargs.get("document_ids_filter") - if document_ids_filter: - operands = [] - for document_id_filter in document_ids_filter: - operands.append({"path": ["document_id"], "operator": "Equal", "valueText": document_id_filter}) - where_filter = {"operator": "Or", "operands": operands} - query_obj = query_obj.with_where(where_filter) - query_obj = query_obj.with_additional(["vector"]) - properties = ["text"] - result = query_obj.with_bm25(query=query, properties=properties).with_limit(kwargs.get("top_k", 4)).do() - if "errors" in result: - raise ValueError(f"Error during query: {result['errors']}") - docs = [] - for res in result["data"]["Get"][collection_name]: - text = res.pop(Field.TEXT_KEY) - additional = res.pop("_additional") - docs.append(Document(page_content=text, vector=additional["vector"], metadata=res)) + Performs BM25 full-text search on document content. + + Filters by document IDs if provided and returns matching documents with vectors. + """ + if not self._client.collections.exists(self._collection_name): + return [] + + col = self._client.collections.use(self._collection_name) + props = list({*self._attributes, Field.TEXT_KEY.value}) + + where = None + doc_ids = kwargs.get("document_ids_filter") or [] + if doc_ids: + ors = [Filter.by_property("document_id").equal(x) for x in doc_ids] + where = ors[0] + for f in ors[1:]: + where = where | f + + top_k = int(kwargs.get("top_k", 4)) + + res = col.query.bm25( + query=query, + query_properties=[Field.TEXT_KEY.value], + limit=top_k, + return_properties=props, + include_vector=True, + filters=where, + ) + + docs: list[Document] = [] + for obj in res.objects: + properties = dict(obj.properties or {}) + text = properties.pop(Field.TEXT_KEY.value, "") + + vec = obj.vector + if isinstance(vec, dict): + vec = vec.get("default") or next(iter(vec.values()), None) + + docs.append(Document(page_content=text, vector=vec, metadata=properties)) return docs - def _default_schema(self, index_name: str): - return { - "class": index_name, - "properties": [ - { - "name": "text", - "dataType": ["text"], - } - ], - } - - def _json_serializable(self, value: Any): + def _json_serializable(self, value: Any) -> Any: + """Converts values to JSON-serializable format, handling datetime objects.""" if isinstance(value, datetime.datetime): return value.isoformat() return value class WeaviateVectorFactory(AbstractVectorFactory): + """Factory class for creating WeaviateVector instances.""" + def init_vector(self, dataset: Dataset, attributes: list, embeddings: Embeddings) -> WeaviateVector: + """ + Initializes a WeaviateVector instance for the given dataset. + + Uses existing collection name from dataset index structure or generates a new one. + Updates dataset index structure if not already set. + """ if dataset.index_struct_dict: class_prefix: str = dataset.index_struct_dict["vector_store"]["class_prefix"] collection_name = class_prefix @@ -281,7 +425,6 @@ class WeaviateVectorFactory(AbstractVectorFactory): dataset_id = dataset.id collection_name = Dataset.gen_collection_name_by_id(dataset_id) dataset.index_struct = json.dumps(self.gen_index_struct_dict(VectorType.WEAVIATE, collection_name)) - return WeaviateVector( collection_name=collection_name, config=WeaviateConfig( diff --git a/api/core/rag/extractor/firecrawl/firecrawl_app.py b/api/core/rag/extractor/firecrawl/firecrawl_app.py index c20ecd2b89..789ac8557d 100644 --- a/api/core/rag/extractor/firecrawl/firecrawl_app.py +++ b/api/core/rag/extractor/firecrawl/firecrawl_app.py @@ -25,7 +25,7 @@ class FirecrawlApp: } if params: json_data.update(params) - response = self._post_request(f"{self.base_url}/v1/scrape", json_data, headers) + response = self._post_request(f"{self.base_url}/v2/scrape", json_data, headers) if response.status_code == 200: response_data = response.json() data = response_data["data"] @@ -42,7 +42,7 @@ class FirecrawlApp: json_data = {"url": url} if params: json_data.update(params) - response = self._post_request(f"{self.base_url}/v1/crawl", json_data, headers) + response = self._post_request(f"{self.base_url}/v2/crawl", json_data, headers) if response.status_code == 200: # There's also another two fields in the response: "success" (bool) and "url" (str) job_id = response.json().get("id") @@ -51,9 +51,25 @@ class FirecrawlApp: self._handle_error(response, "start crawl job") return "" # unreachable + def map(self, url: str, params: dict[str, Any] | None = None) -> dict[str, Any]: + # Documentation: https://docs.firecrawl.dev/api-reference/endpoint/map + headers = self._prepare_headers() + json_data: dict[str, Any] = {"url": url, "integration": "dify"} + if params: + # Pass through provided params, including optional "sitemap": "only" | "include" | "skip" + json_data.update(params) + response = self._post_request(f"{self.base_url}/v2/map", json_data, headers) + if response.status_code == 200: + return cast(dict[str, Any], response.json()) + elif response.status_code in {402, 409, 500, 429, 408}: + self._handle_error(response, "start map job") + return {} + else: + raise Exception(f"Failed to start map job. Status code: {response.status_code}") + def check_crawl_status(self, job_id) -> dict[str, Any]: headers = self._prepare_headers() - response = self._get_request(f"{self.base_url}/v1/crawl/{job_id}", headers) + response = self._get_request(f"{self.base_url}/v2/crawl/{job_id}", headers) if response.status_code == 200: crawl_status_response = response.json() if crawl_status_response.get("status") == "completed": @@ -135,12 +151,16 @@ class FirecrawlApp: "lang": "en", "country": "us", "timeout": 60000, - "ignoreInvalidURLs": False, + "ignoreInvalidURLs": True, "scrapeOptions": {}, + "sources": [ + {"type": "web"}, + ], + "integration": "dify", } if params: json_data.update(params) - response = self._post_request(f"{self.base_url}/v1/search", json_data, headers) + response = self._post_request(f"{self.base_url}/v2/search", json_data, headers) if response.status_code == 200: response_data = response.json() if not response_data.get("success"): diff --git a/api/core/tools/entities/tool_entities.py b/api/core/tools/entities/tool_entities.py index 62e3aa8b5d..15a4f0aafd 100644 --- a/api/core/tools/entities/tool_entities.py +++ b/api/core/tools/entities/tool_entities.py @@ -189,6 +189,11 @@ class ToolInvokeMessage(BaseModel): data: Mapping[str, Any] = Field(..., description="Detailed log data") metadata: Mapping[str, Any] = Field(default_factory=dict, description="The metadata of the log") + @field_validator("metadata", mode="before") + @classmethod + def _normalize_metadata(cls, value: Mapping[str, Any] | None) -> Mapping[str, Any]: + return value or {} + class RetrieverResourceMessage(BaseModel): retriever_resources: list[RetrievalSourceMetadata] = Field(..., description="retriever resources") context: str = Field(..., description="context") @@ -376,6 +381,11 @@ class ToolEntity(BaseModel): def set_parameters(cls, v, validation_info: ValidationInfo) -> list[ToolParameter]: return v or [] + @field_validator("output_schema", mode="before") + @classmethod + def _normalize_output_schema(cls, value: Mapping[str, object] | None) -> Mapping[str, object]: + return value or {} + class OAuthSchema(BaseModel): client_schema: list[ProviderConfig] = Field( diff --git a/api/core/workflow/graph_engine/event_management/event_handlers.py b/api/core/workflow/graph_engine/event_management/event_handlers.py index 7247b17967..1cb5851ab1 100644 --- a/api/core/workflow/graph_engine/event_management/event_handlers.py +++ b/api/core/workflow/graph_engine/event_management/event_handlers.py @@ -7,6 +7,7 @@ from collections.abc import Mapping from functools import singledispatchmethod from typing import TYPE_CHECKING, final +from core.model_runtime.entities.llm_entities import LLMUsage from core.workflow.entities import GraphRuntimeState from core.workflow.enums import ErrorStrategy, NodeExecutionType from core.workflow.graph import Graph @@ -125,6 +126,7 @@ class EventHandler: node_execution = self._graph_execution.get_or_create_node_execution(event.node_id) is_initial_attempt = node_execution.retry_count == 0 node_execution.mark_started(event.id) + self._graph_runtime_state.increment_node_run_steps() # Track in response coordinator for stream ordering self._response_coordinator.track_node_execution(event.node_id, event.id) @@ -163,6 +165,8 @@ class EventHandler: node_execution = self._graph_execution.get_or_create_node_execution(event.node_id) node_execution.mark_taken() + self._accumulate_node_usage(event.node_run_result.llm_usage) + # Store outputs in variable pool self._store_node_outputs(event.node_id, event.node_run_result.outputs) @@ -212,6 +216,8 @@ class EventHandler: node_execution.mark_failed(event.error) self._graph_execution.record_node_failure() + self._accumulate_node_usage(event.node_run_result.llm_usage) + result = self._error_handler.handle_node_failure(event) if result: @@ -235,6 +241,8 @@ class EventHandler: node_execution = self._graph_execution.get_or_create_node_execution(event.node_id) node_execution.mark_taken() + self._accumulate_node_usage(event.node_run_result.llm_usage) + # Persist outputs produced by the exception strategy (e.g. default values) self._store_node_outputs(event.node_id, event.node_run_result.outputs) @@ -286,6 +294,19 @@ class EventHandler: self._state_manager.enqueue_node(event.node_id) self._state_manager.start_execution(event.node_id) + def _accumulate_node_usage(self, usage: LLMUsage) -> None: + """Accumulate token usage into the shared runtime state.""" + if usage.total_tokens <= 0: + return + + self._graph_runtime_state.add_tokens(usage.total_tokens) + + current_usage = self._graph_runtime_state.llm_usage + if current_usage.total_tokens == 0: + self._graph_runtime_state.llm_usage = usage + else: + self._graph_runtime_state.llm_usage = current_usage.plus(usage) + def _store_node_outputs(self, node_id: str, outputs: Mapping[str, object]) -> None: """ Store node outputs in the variable pool. diff --git a/api/pyproject.toml b/api/pyproject.toml index 0ee2b1a291..62af88a1b2 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -214,7 +214,7 @@ vdb = [ "tidb-vector==0.0.9", "upstash-vector==0.6.0", "volcengine-compat~=1.0.0", - "weaviate-client~=3.24.0", + "weaviate-client>=4.0.0,<5.0.0", "xinference-client~=1.2.2", "mo-vector~=0.1.13", "mysql-connector-python>=9.3.0", diff --git a/api/schedule/clean_workflow_runlogs_precise.py b/api/schedule/clean_workflow_runlogs_precise.py index 485a79782c..db4198720d 100644 --- a/api/schedule/clean_workflow_runlogs_precise.py +++ b/api/schedule/clean_workflow_runlogs_precise.py @@ -1,8 +1,11 @@ import datetime import logging import time +from collections.abc import Sequence import click +from sqlalchemy import select +from sqlalchemy.orm import Session, sessionmaker import app from configs import dify_config @@ -35,50 +38,53 @@ def clean_workflow_runlogs_precise(): retention_days = dify_config.WORKFLOW_LOG_RETENTION_DAYS cutoff_date = datetime.datetime.now() - datetime.timedelta(days=retention_days) + session_factory = sessionmaker(db.engine, expire_on_commit=False) try: - total_workflow_runs = db.session.query(WorkflowRun).where(WorkflowRun.created_at < cutoff_date).count() - if total_workflow_runs == 0: - logger.info("No expired workflow run logs found") - return - logger.info("Found %s expired workflow run logs to clean", total_workflow_runs) + with session_factory.begin() as session: + total_workflow_runs = session.query(WorkflowRun).where(WorkflowRun.created_at < cutoff_date).count() + if total_workflow_runs == 0: + logger.info("No expired workflow run logs found") + return + logger.info("Found %s expired workflow run logs to clean", total_workflow_runs) total_deleted = 0 failed_batches = 0 batch_count = 0 - while True: - workflow_runs = ( - db.session.query(WorkflowRun.id).where(WorkflowRun.created_at < cutoff_date).limit(BATCH_SIZE).all() - ) + with session_factory.begin() as session: + workflow_run_ids = session.scalars( + select(WorkflowRun.id) + .where(WorkflowRun.created_at < cutoff_date) + .order_by(WorkflowRun.created_at, WorkflowRun.id) + .limit(BATCH_SIZE) + ).all() - if not workflow_runs: - break - - workflow_run_ids = [run.id for run in workflow_runs] - batch_count += 1 - - success = _delete_batch_with_retry(workflow_run_ids, failed_batches) - - if success: - total_deleted += len(workflow_run_ids) - failed_batches = 0 - else: - failed_batches += 1 - if failed_batches >= MAX_RETRIES: - logger.error("Failed to delete batch after %s retries, aborting cleanup for today", MAX_RETRIES) + if not workflow_run_ids: break + + batch_count += 1 + + success = _delete_batch(session, workflow_run_ids, failed_batches) + + if success: + total_deleted += len(workflow_run_ids) + failed_batches = 0 else: - # Calculate incremental delay times: 5, 10, 15 minutes - retry_delay_minutes = failed_batches * 5 - logger.warning("Batch deletion failed, retrying in %s minutes...", retry_delay_minutes) - time.sleep(retry_delay_minutes * 60) - continue + failed_batches += 1 + if failed_batches >= MAX_RETRIES: + logger.error("Failed to delete batch after %s retries, aborting cleanup for today", MAX_RETRIES) + break + else: + # Calculate incremental delay times: 5, 10, 15 minutes + retry_delay_minutes = failed_batches * 5 + logger.warning("Batch deletion failed, retrying in %s minutes...", retry_delay_minutes) + time.sleep(retry_delay_minutes * 60) + continue logger.info("Cleanup completed: %s expired workflow run logs deleted", total_deleted) except Exception: - db.session.rollback() logger.exception("Unexpected error in workflow log cleanup") raise @@ -87,69 +93,56 @@ def clean_workflow_runlogs_precise(): click.echo(click.style(f"Cleaned workflow run logs from db success latency: {execution_time:.2f}s", fg="green")) -def _delete_batch_with_retry(workflow_run_ids: list[str], attempt_count: int) -> bool: - """Delete a single batch with a retry mechanism and complete cascading deletion""" +def _delete_batch(session: Session, workflow_run_ids: Sequence[str], attempt_count: int) -> bool: + """Delete a single batch of workflow runs and all related data within a nested transaction.""" try: - with db.session.begin_nested(): + with session.begin_nested(): message_data = ( - db.session.query(Message.id, Message.conversation_id) + session.query(Message.id, Message.conversation_id) .where(Message.workflow_run_id.in_(workflow_run_ids)) .all() ) message_id_list = [msg.id for msg in message_data] conversation_id_list = list({msg.conversation_id for msg in message_data if msg.conversation_id}) if message_id_list: - db.session.query(AppAnnotationHitHistory).where( - AppAnnotationHitHistory.message_id.in_(message_id_list) - ).delete(synchronize_session=False) + message_related_models = [ + AppAnnotationHitHistory, + MessageAgentThought, + MessageChain, + MessageFile, + MessageAnnotation, + MessageFeedback, + ] + for model in message_related_models: + session.query(model).where(model.message_id.in_(message_id_list)).delete(synchronize_session=False) # type: ignore + # error: "DeclarativeAttributeIntercept" has no attribute "message_id". But this type is only in lib + # and these 6 types all have the message_id field. - db.session.query(MessageAgentThought).where(MessageAgentThought.message_id.in_(message_id_list)).delete( + session.query(Message).where(Message.workflow_run_id.in_(workflow_run_ids)).delete( synchronize_session=False ) - db.session.query(MessageChain).where(MessageChain.message_id.in_(message_id_list)).delete( - synchronize_session=False - ) - - db.session.query(MessageFile).where(MessageFile.message_id.in_(message_id_list)).delete( - synchronize_session=False - ) - - db.session.query(MessageAnnotation).where(MessageAnnotation.message_id.in_(message_id_list)).delete( - synchronize_session=False - ) - - db.session.query(MessageFeedback).where(MessageFeedback.message_id.in_(message_id_list)).delete( - synchronize_session=False - ) - - db.session.query(Message).where(Message.workflow_run_id.in_(workflow_run_ids)).delete( - synchronize_session=False - ) - - db.session.query(WorkflowAppLog).where(WorkflowAppLog.workflow_run_id.in_(workflow_run_ids)).delete( + session.query(WorkflowAppLog).where(WorkflowAppLog.workflow_run_id.in_(workflow_run_ids)).delete( synchronize_session=False ) - db.session.query(WorkflowNodeExecutionModel).where( + session.query(WorkflowNodeExecutionModel).where( WorkflowNodeExecutionModel.workflow_run_id.in_(workflow_run_ids) ).delete(synchronize_session=False) if conversation_id_list: - db.session.query(ConversationVariable).where( + session.query(ConversationVariable).where( ConversationVariable.conversation_id.in_(conversation_id_list) ).delete(synchronize_session=False) - db.session.query(Conversation).where(Conversation.id.in_(conversation_id_list)).delete( + session.query(Conversation).where(Conversation.id.in_(conversation_id_list)).delete( synchronize_session=False ) - db.session.query(WorkflowRun).where(WorkflowRun.id.in_(workflow_run_ids)).delete(synchronize_session=False) + session.query(WorkflowRun).where(WorkflowRun.id.in_(workflow_run_ids)).delete(synchronize_session=False) - db.session.commit() - return True + return True except Exception: - db.session.rollback() logger.exception("Batch deletion failed (attempt %s)", attempt_count + 1) return False diff --git a/api/services/website_service.py b/api/services/website_service.py index 37588d6ba5..a23f01ec71 100644 --- a/api/services/website_service.py +++ b/api/services/website_service.py @@ -23,6 +23,7 @@ class CrawlOptions: only_main_content: bool = False includes: str | None = None excludes: str | None = None + prompt: str | None = None max_depth: int | None = None use_sitemap: bool = True @@ -70,6 +71,7 @@ class WebsiteCrawlApiRequest: only_main_content=self.options.get("only_main_content", False), includes=self.options.get("includes"), excludes=self.options.get("excludes"), + prompt=self.options.get("prompt"), max_depth=self.options.get("max_depth"), use_sitemap=self.options.get("use_sitemap", True), ) @@ -174,6 +176,7 @@ class WebsiteService: def _crawl_with_firecrawl(cls, request: CrawlRequest, api_key: str, config: dict) -> dict[str, Any]: firecrawl_app = FirecrawlApp(api_key=api_key, base_url=config.get("base_url")) + params: dict[str, Any] if not request.options.crawl_sub_pages: params = { "includePaths": [], @@ -188,8 +191,10 @@ class WebsiteService: "limit": request.options.limit, "scrapeOptions": {"onlyMainContent": request.options.only_main_content}, } - if request.options.max_depth: - params["maxDepth"] = request.options.max_depth + + # Add optional prompt for Firecrawl v2 crawl-params compatibility + if request.options.prompt: + params["prompt"] = request.options.prompt job_id = firecrawl_app.crawl_url(request.url, params) website_crawl_time_cache_key = f"website_crawl_{job_id}" diff --git a/api/services/workflow_app_service.py b/api/services/workflow_app_service.py index ced6dca324..23dd436675 100644 --- a/api/services/workflow_app_service.py +++ b/api/services/workflow_app_service.py @@ -86,12 +86,16 @@ class WorkflowAppService: ), ) if created_by_account: + account = session.scalar(select(Account).where(Account.email == created_by_account)) + if not account: + raise ValueError(f"Account not found: {created_by_account}") + stmt = stmt.join( Account, and_( WorkflowAppLog.created_by == Account.id, WorkflowAppLog.created_by_role == CreatorUserRole.ACCOUNT, - Account.email == created_by_account, + Account.id == account.id, ), ) diff --git a/api/templates/without-brand/invite_member_mail_template_en-US.html b/api/templates/without-brand/invite_member_mail_template_en-US.html index fc7f3679ba..b78a6a0760 100644 --- a/api/templates/without-brand/invite_member_mail_template_en-US.html +++ b/api/templates/without-brand/invite_member_mail_template_en-US.html @@ -75,10 +75,7 @@
-
- - Dify Logo -
+

Dear {{ to }},

{{ inviter_name }} is pleased to invite you to join our workspace on {{application_title}}, a platform specifically designed for LLM application development. On {{application_title}}, you can explore, create, and collaborate to build and operate AI applications.

diff --git a/api/tests/test_containers_integration_tests/services/test_workflow_app_service.py b/api/tests/test_containers_integration_tests/services/test_workflow_app_service.py index 62c9bead86..66bd4d3cd9 100644 --- a/api/tests/test_containers_integration_tests/services/test_workflow_app_service.py +++ b/api/tests/test_containers_integration_tests/services/test_workflow_app_service.py @@ -789,6 +789,31 @@ class TestWorkflowAppService: assert result_account_filter["total"] == 3 assert all(log.created_by_role == CreatorUserRole.ACCOUNT for log in result_account_filter["data"]) + # Test filtering by changed account email + original_email = account.email + new_email = "changed@example.com" + account.email = new_email + db_session_with_containers.commit() + + assert account.email == new_email + + # Results for new email, is expected to be the same as the original email + result_with_new_email = service.get_paginate_workflow_app_logs( + session=db_session_with_containers, app_model=app, created_by_account=new_email, page=1, limit=20 + ) + assert result_with_new_email["total"] == 3 + assert all(log.created_by_role == CreatorUserRole.ACCOUNT for log in result_with_new_email["data"]) + + # Old email unbound, is unexpected input, should raise ValueError + with pytest.raises(ValueError) as exc_info: + service.get_paginate_workflow_app_logs( + session=db_session_with_containers, app_model=app, created_by_account=original_email, page=1, limit=20 + ) + assert "Account not found" in str(exc_info.value) + + account.email = original_email + db_session_with_containers.commit() + # Test filtering by non-existent session ID result_no_session = service.get_paginate_workflow_app_logs( session=db_session_with_containers, @@ -799,15 +824,16 @@ class TestWorkflowAppService: ) assert result_no_session["total"] == 0 - # Test filtering by non-existent account email - result_no_account = service.get_paginate_workflow_app_logs( - session=db_session_with_containers, - app_model=app, - created_by_account="nonexistent@example.com", - page=1, - limit=20, - ) - assert result_no_account["total"] == 0 + # Test filtering by non-existent account email, is unexpected input, should raise ValueError + with pytest.raises(ValueError) as exc_info: + service.get_paginate_workflow_app_logs( + session=db_session_with_containers, + app_model=app, + created_by_account="nonexistent@example.com", + page=1, + limit=20, + ) + assert "Account not found" in str(exc_info.value) def test_get_paginate_workflow_app_logs_with_uuid_keyword_search( self, db_session_with_containers, mock_external_service_dependencies @@ -1057,15 +1083,15 @@ class TestWorkflowAppService: assert len(result_no_session["data"]) == 0 # Test with account email that doesn't exist - result_no_account = service.get_paginate_workflow_app_logs( - session=db_session_with_containers, - app_model=app, - created_by_account="nonexistent@example.com", - page=1, - limit=20, - ) - assert result_no_account["total"] == 0 - assert len(result_no_account["data"]) == 0 + with pytest.raises(ValueError) as exc_info: + service.get_paginate_workflow_app_logs( + session=db_session_with_containers, + app_model=app, + created_by_account="nonexistent@example.com", + page=1, + limit=20, + ) + assert "Account not found" in str(exc_info.value) def test_get_paginate_workflow_app_logs_with_complex_query_combinations( self, db_session_with_containers, mock_external_service_dependencies diff --git a/api/tests/unit_tests/core/rag/datasource/vdb/alibabacloud_mysql/test_alibabacloud_mysql_vector.py b/api/tests/unit_tests/core/rag/datasource/vdb/alibabacloud_mysql/test_alibabacloud_mysql_vector.py index 44fe272c8c..8ccd739e64 100644 --- a/api/tests/unit_tests/core/rag/datasource/vdb/alibabacloud_mysql/test_alibabacloud_mysql_vector.py +++ b/api/tests/unit_tests/core/rag/datasource/vdb/alibabacloud_mysql/test_alibabacloud_mysql_vector.py @@ -110,19 +110,6 @@ class TestAlibabaCloudMySQLVector(unittest.TestCase): assert mock_cursor.execute.call_count >= 3 # CREATE TABLE + 2 indexes mock_redis.set.assert_called_once() - def test_config_validation(self): - """Test configuration validation.""" - # Test missing required fields - with pytest.raises(ValueError): - AlibabaCloudMySQLVectorConfig( - host="", # Empty host should raise error - port=3306, - user="test", - password="test", - database="test", - max_connection=5, - ) - @patch( "core.rag.datasource.vdb.alibabacloud_mysql.alibabacloud_mysql_vector.mysql.connector.pooling.MySQLConnectionPool" ) @@ -718,5 +705,29 @@ class TestAlibabaCloudMySQLVector(unittest.TestCase): mock_cursor.fetchone.side_effect = [{"VERSION()": "8.0.36"}, {"vector_support": True}] +@pytest.mark.parametrize( + "invalid_config_override", + [ + {"host": ""}, # Test empty host + {"port": 0}, # Test invalid port + {"max_connection": 0}, # Test invalid max_connection + ], +) +def test_config_validation_parametrized(invalid_config_override): + """Test configuration validation for various invalid inputs using parametrize.""" + config = { + "host": "localhost", + "port": 3306, + "user": "test", + "password": "test", + "database": "test", + "max_connection": 5, + } + config.update(invalid_config_override) + + with pytest.raises(ValueError): + AlibabaCloudMySQLVectorConfig(**config) + + if __name__ == "__main__": unittest.main() diff --git a/api/tests/unit_tests/core/tools/test_tool_entities.py b/api/tests/unit_tests/core/tools/test_tool_entities.py new file mode 100644 index 0000000000..a5b7e8a9a3 --- /dev/null +++ b/api/tests/unit_tests/core/tools/test_tool_entities.py @@ -0,0 +1,29 @@ +from core.tools.entities.common_entities import I18nObject +from core.tools.entities.tool_entities import ToolEntity, ToolIdentity, ToolInvokeMessage + + +def _make_identity() -> ToolIdentity: + return ToolIdentity( + author="author", + name="tool", + label=I18nObject(en_US="Label"), + provider="builtin", + ) + + +def test_log_message_metadata_none_defaults_to_empty_dict(): + log_message = ToolInvokeMessage.LogMessage( + id="log-1", + label="Log entry", + status=ToolInvokeMessage.LogMessage.LogStatus.START, + data={}, + metadata=None, + ) + + assert log_message.metadata == {} + + +def test_tool_entity_output_schema_none_defaults_to_empty_dict(): + entity = ToolEntity(identity=_make_identity(), output_schema=None) + + assert entity.output_schema == {} diff --git a/api/uv.lock b/api/uv.lock index 87d96fb418..96aee8a97b 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -587,16 +587,16 @@ wheels = [ [[package]] name = "boto3-stubs" -version = "1.40.50" +version = "1.40.51" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore-stubs" }, { name = "types-s3transfer" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/35/c8/06584145c4ccc80e3297a97874bfaa43e6b2fb9f8a69bcc38e29a1457bf5/boto3_stubs-1.40.50.tar.gz", hash = "sha256:29828adfcb8629b5e285468eb89610f1fc71f964ad0913de3049a0a9d5de0be1", size = 100836, upload-time = "2025-10-10T20:32:34.867Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/4d/b07f9ee0fe432fa8ec6dc368ee7a0409e2b6d9df2c5a2a88265c9b6fd878/boto3_stubs-1.40.51.tar.gz", hash = "sha256:0281e820813a310954e15fb7c1d470c24c34c1cccc7b1ddad977fa293a1080a9", size = 100890, upload-time = "2025-10-13T19:25:36.126Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/69/f18c7135dc8a2b74e21b4a2375fa455e4d9e7e47f7838bc175d52005054a/boto3_stubs-1.40.50-py3-none-any.whl", hash = "sha256:01b9c67df62f26371a4a7473c616eece988a5305e7f7cb3fbc014d178685ac4e", size = 69689, upload-time = "2025-10-10T20:32:25.77Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2e/4476431f11fc3bf7a7e0f4f5c275f17607aa127da7c0d8685a4dc6bf6291/boto3_stubs-1.40.51-py3-none-any.whl", hash = "sha256:896d0ffaa298ce1749eea1a54946320a0f4e07c6912f8e1f8c0744a708ee25a4", size = 69709, upload-time = "2025-10-13T19:25:23.116Z" }, ] [package.optional-dependencies] @@ -620,14 +620,14 @@ wheels = [ [[package]] name = "botocore-stubs" -version = "1.40.50" +version = "1.40.51" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "types-awscrt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/4b/86ad2d24ea36eed159c8e1f85a2645bfeedae34ccb8c77ea8c99abbd66d1/botocore_stubs-1.40.50.tar.gz", hash = "sha256:d772b2d3aea6b4e464963fe45b2d504eee7bc3842f047cebbae5492b3993e0fd", size = 42250, upload-time = "2025-10-11T23:08:59.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/ca/429fadb6e037cb7b300d508a0b24b59a71961db12539e21749cbec7e7422/botocore_stubs-1.40.51.tar.gz", hash = "sha256:8ddbeb1f68e39382533bb53f3b968d29e640406016af00ad8bbd6e1a2bd59536", size = 42249, upload-time = "2025-10-13T20:26:57.777Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/c1/4a736155b2d5dd7fdd09af8fba9ed59693c565d6e2bc1b5adc769da36cb5/botocore_stubs-1.40.50-py3-none-any.whl", hash = "sha256:7cb8d636e061e600929cd03339c3bbc162c21435b4bfeb6413cf7b0b612e7de0", size = 66541, upload-time = "2025-10-11T23:08:57.678Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b9/5f1296bc46f293f284a1a6259f3c1f21f4161088dc6f70428698841b56a7/botocore_stubs-1.40.51-py3-none-any.whl", hash = "sha256:9a028104979205c9be0b68bb59ba679e4fe452e017eec3d40f6c2b41c590a73c", size = 66541, upload-time = "2025-10-13T20:26:55.559Z" }, ] [[package]] @@ -1667,7 +1667,7 @@ vdb = [ { name = "tidb-vector", specifier = "==0.0.9" }, { name = "upstash-vector", specifier = "==0.6.0" }, { name = "volcengine-compat", specifier = "~=1.0.0" }, - { name = "weaviate-client", specifier = "~=3.24.0" }, + { name = "weaviate-client", specifier = ">=4.0.0,<5.0.0" }, { name = "xinference-client", specifier = "~=1.2.2" }, ] @@ -6901,16 +6901,20 @@ wheels = [ [[package]] name = "weaviate-client" -version = "3.24.2" +version = "4.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib" }, - { name = "requests" }, + { name = "deprecation" }, + { name = "grpcio" }, + { name = "httpx" }, + { name = "protobuf" }, + { name = "pydantic" }, { name = "validators" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/c1/3285a21d8885f2b09aabb65edb9a8e062a35c2d7175e1bb024fa096582ab/weaviate-client-3.24.2.tar.gz", hash = "sha256:6914c48c9a7e5ad0be9399271f9cb85d6f59ab77476c6d4e56a3925bf149edaa", size = 199332, upload-time = "2023-10-04T08:37:54.26Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/0e/e4582b007427187a9fde55fa575db4b766c81929d2b43a3dd8becce50567/weaviate_client-4.17.0.tar.gz", hash = "sha256:731d58d84b0989df4db399b686357ed285fb95971a492ccca8dec90bb2343c51", size = 769019, upload-time = "2025-09-26T11:20:27.381Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/98/3136d05f93e30cf29e1db280eaadf766df18d812dfe7994bcced653b2340/weaviate_client-3.24.2-py3-none-any.whl", hash = "sha256:bc50ca5fcebcd48de0d00f66700b0cf7c31a97c4cd3d29b4036d77c5d1d9479b", size = 107968, upload-time = "2023-10-04T08:37:52.511Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/2da3a45866da7a935dab8ad07be05dcaee48b3ad4955144583b651929be7/weaviate_client-4.17.0-py3-none-any.whl", hash = "sha256:60e4a355b90537ee1e942ab0b76a94750897a13d9cf13c5a6decbd166d0ca8b5", size = 582763, upload-time = "2025-09-26T11:20:25.864Z" }, ] [[package]] diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index 5253f750b9..5483e2d554 100644 --- a/docker/docker-compose-template.yaml +++ b/docker/docker-compose-template.yaml @@ -329,7 +329,7 @@ services: # The Weaviate vector store. weaviate: - image: semitechnologies/weaviate:1.19.0 + image: semitechnologies/weaviate:1.27.0 profiles: - "" - weaviate diff --git a/docker/docker-compose.middleware.yaml b/docker/docker-compose.middleware.yaml index d350503f27..ebc619a50f 100644 --- a/docker/docker-compose.middleware.yaml +++ b/docker/docker-compose.middleware.yaml @@ -181,7 +181,7 @@ services: # The Weaviate vector store. weaviate: - image: semitechnologies/weaviate:1.19.0 + image: semitechnologies/weaviate:1.27.0 profiles: - "" - weaviate @@ -206,6 +206,7 @@ services: AUTHORIZATION_ADMINLIST_USERS: ${WEAVIATE_AUTHORIZATION_ADMINLIST_USERS:-hello@dify.ai} ports: - "${EXPOSE_WEAVIATE_PORT:-8080}:8080" + - "${EXPOSE_WEAVIATE_GRPC_PORT:-50051}:50051" networks: # create a network between sandbox, api and ssrf_proxy, and can not access outside. diff --git a/docker/docker-compose.override.yml b/docker/docker-compose.override.yml new file mode 100644 index 0000000000..8f2ab1cb43 --- /dev/null +++ b/docker/docker-compose.override.yml @@ -0,0 +1,9 @@ +services: + api: + volumes: + - ../api/core/rag/datasource/vdb/weaviate/weaviate_vector.py:/app/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py:ro + command: > + sh -c " + pip install --no-cache-dir 'weaviate>=4.0.0' && + /bin/bash /entrypoint.sh + " diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 0df648f38f..46b4a750ea 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -936,7 +936,7 @@ services: # The Weaviate vector store. weaviate: - image: semitechnologies/weaviate:1.19.0 + image: semitechnologies/weaviate:1.27.0 profiles: - "" - weaviate diff --git a/web/app/components/app/configuration/config-var/index.tsx b/web/app/components/app/configuration/config-var/index.tsx index 2ac68227e3..b9227c6846 100644 --- a/web/app/components/app/configuration/config-var/index.tsx +++ b/web/app/components/app/configuration/config-var/index.tsx @@ -1,10 +1,11 @@ 'use client' import type { FC } from 'react' -import React, { useState } from 'react' +import React, { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useBoolean } from 'ahooks' import { useContext } from 'use-context-selector' import produce from 'immer' +import { ReactSortable } from 'react-sortablejs' import Panel from '../base/feature-panel' import EditModal from './config-modal' import VarItem from './var-item' @@ -22,6 +23,7 @@ import { useModalContext } from '@/context/modal-context' import { useEventEmitterContextContext } from '@/context/event-emitter' import type { InputVar } from '@/app/components/workflow/types' import { InputVarType } from '@/app/components/workflow/types' +import cn from '@/utils/classnames' export const ADD_EXTERNAL_DATA_TOOL = 'ADD_EXTERNAL_DATA_TOOL' @@ -218,6 +220,16 @@ const ConfigVar: FC = ({ promptVariables, readonly, onPromptVar showEditModal() } + + const promptVariablesWithIds = useMemo(() => promptVariables.map((item) => { + return { + id: item.key, + variable: { ...item }, + } + }), [promptVariables]) + + const canDrag = !readonly && promptVariables.length > 1 + return ( = ({ promptVariables, readonly, onPromptVar )} {hasVar && (
- {promptVariables.map(({ key, name, type, required, config, icon, icon_background }, index) => ( - handleConfig({ type, key, index, name, config, icon, icon_background })} - onRemove={() => handleRemoveVar(index)} - /> - ))} + { onPromptVariablesChange?.(list.map(item => item.variable)) }} + handle='.handle' + ghostClass='opacity-50' + animation={150} + > + {promptVariablesWithIds.map((item, index) => { + const { key, name, type, required, config, icon, icon_background } = item.variable + return ( + handleConfig({ type, key, index, name, config, icon, icon_background })} + onRemove={() => handleRemoveVar(index)} + canDrag={canDrag} + /> + ) + })} +
)} diff --git a/web/app/components/app/configuration/config-var/var-item.tsx b/web/app/components/app/configuration/config-var/var-item.tsx index 78ed4b1031..88cd5d7843 100644 --- a/web/app/components/app/configuration/config-var/var-item.tsx +++ b/web/app/components/app/configuration/config-var/var-item.tsx @@ -3,6 +3,7 @@ import type { FC } from 'react' import React, { useState } from 'react' import { RiDeleteBinLine, + RiDraggable, RiEditLine, } from '@remixicon/react' import type { IInputTypeIconProps } from './input-type-icon' @@ -12,6 +13,7 @@ import Badge from '@/app/components/base/badge' import cn from '@/utils/classnames' type ItemProps = { + className?: string readonly?: boolean name: string label: string @@ -19,9 +21,11 @@ type ItemProps = { type: string onEdit: () => void onRemove: () => void + canDrag?: boolean } const VarItem: FC = ({ + className, readonly, name, label, @@ -29,12 +33,16 @@ const VarItem: FC = ({ type, onEdit, onRemove, + canDrag, }) => { const [isDeleting, setIsDeleting] = useState(false) return ( -
- +
+ + {canDrag && ( + + )}
{name} diff --git a/web/app/components/base/icons/assets/vender/knowledge/index.ts b/web/app/components/base/icons/assets/vender/knowledge/index.ts deleted file mode 100644 index 7ef6f1b8e4..0000000000 --- a/web/app/components/base/icons/assets/vender/knowledge/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export { default as Chunk } from './Chunk' -export { default as Collapse } from './Collapse' -export { default as Divider } from './Divider' -export { default as File } from './File' -export { default as GeneralType } from './GeneralType' -export { default as LayoutRight2LineMod } from './LayoutRight2LineMod' -export { default as OptionCardEffectBlueLight } from './OptionCardEffectBlueLight' -export { default as OptionCardEffectBlue } from './OptionCardEffectBlue' -export { default as OptionCardEffectOrange } from './OptionCardEffectOrange' -export { default as OptionCardEffectPurple } from './OptionCardEffectPurple' -export { default as ParentChildType } from './ParentChildType' -export { default as SelectionMod } from './SelectionMod' -export { default as Watercrawl } from './Watercrawl' diff --git a/web/app/components/base/icons/src/image/llm/BaichuanTextCn.module.css b/web/app/components/base/icons/src/image/llm/BaichuanTextCn.module.css deleted file mode 100644 index 97ab9b22f9..0000000000 --- a/web/app/components/base/icons/src/image/llm/BaichuanTextCn.module.css +++ /dev/null @@ -1,5 +0,0 @@ -.wrapper { - display: inline-flex; - background: url(~@/app/components/base/icons/assets/image/llm/baichuan-text-cn.png) center center no-repeat; - background-size: contain; -} diff --git a/web/app/components/base/icons/src/image/llm/BaichuanTextCn.tsx b/web/app/components/base/icons/src/image/llm/BaichuanTextCn.tsx deleted file mode 100644 index be9a407eb2..0000000000 --- a/web/app/components/base/icons/src/image/llm/BaichuanTextCn.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import cn from '@/utils/classnames' -import s from './BaichuanTextCn.module.css' - -const Icon = ( - { - ref, - className, - ...restProps - }: React.DetailedHTMLProps, HTMLSpanElement> & { - ref?: React.RefObject; - }, -) => - -Icon.displayName = 'BaichuanTextCn' - -export default Icon diff --git a/web/app/components/base/icons/src/image/llm/Minimax.module.css b/web/app/components/base/icons/src/image/llm/Minimax.module.css deleted file mode 100644 index 551ecc3c62..0000000000 --- a/web/app/components/base/icons/src/image/llm/Minimax.module.css +++ /dev/null @@ -1,5 +0,0 @@ -.wrapper { - display: inline-flex; - background: url(~@/app/components/base/icons/assets/image/llm/minimax.png) center center no-repeat; - background-size: contain; -} diff --git a/web/app/components/base/icons/src/image/llm/Minimax.tsx b/web/app/components/base/icons/src/image/llm/Minimax.tsx deleted file mode 100644 index 7df7e3fcbc..0000000000 --- a/web/app/components/base/icons/src/image/llm/Minimax.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import cn from '@/utils/classnames' -import s from './Minimax.module.css' - -const Icon = ( - { - ref, - className, - ...restProps - }: React.DetailedHTMLProps, HTMLSpanElement> & { - ref?: React.RefObject; - }, -) => - -Icon.displayName = 'Minimax' - -export default Icon diff --git a/web/app/components/base/icons/src/image/llm/MinimaxText.module.css b/web/app/components/base/icons/src/image/llm/MinimaxText.module.css deleted file mode 100644 index a63be49e8b..0000000000 --- a/web/app/components/base/icons/src/image/llm/MinimaxText.module.css +++ /dev/null @@ -1,5 +0,0 @@ -.wrapper { - display: inline-flex; - background: url(~@/app/components/base/icons/assets/image/llm/minimax-text.png) center center no-repeat; - background-size: contain; -} diff --git a/web/app/components/base/icons/src/image/llm/MinimaxText.tsx b/web/app/components/base/icons/src/image/llm/MinimaxText.tsx deleted file mode 100644 index 840e8cb439..0000000000 --- a/web/app/components/base/icons/src/image/llm/MinimaxText.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import cn from '@/utils/classnames' -import s from './MinimaxText.module.css' - -const Icon = ( - { - ref, - className, - ...restProps - }: React.DetailedHTMLProps, HTMLSpanElement> & { - ref?: React.RefObject; - }, -) => - -Icon.displayName = 'MinimaxText' - -export default Icon diff --git a/web/app/components/base/icons/src/image/llm/Tongyi.module.css b/web/app/components/base/icons/src/image/llm/Tongyi.module.css deleted file mode 100644 index 3ca440768c..0000000000 --- a/web/app/components/base/icons/src/image/llm/Tongyi.module.css +++ /dev/null @@ -1,5 +0,0 @@ -.wrapper { - display: inline-flex; - background: url(~@/app/components/base/icons/assets/image/llm/tongyi.png) center center no-repeat; - background-size: contain; -} diff --git a/web/app/components/base/icons/src/image/llm/Tongyi.tsx b/web/app/components/base/icons/src/image/llm/Tongyi.tsx deleted file mode 100644 index 2f62f1a355..0000000000 --- a/web/app/components/base/icons/src/image/llm/Tongyi.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import cn from '@/utils/classnames' -import s from './Tongyi.module.css' - -const Icon = ( - { - ref, - className, - ...restProps - }: React.DetailedHTMLProps, HTMLSpanElement> & { - ref?: React.RefObject; - }, -) => - -Icon.displayName = 'Tongyi' - -export default Icon diff --git a/web/app/components/base/icons/src/image/llm/TongyiText.module.css b/web/app/components/base/icons/src/image/llm/TongyiText.module.css deleted file mode 100644 index f713671808..0000000000 --- a/web/app/components/base/icons/src/image/llm/TongyiText.module.css +++ /dev/null @@ -1,5 +0,0 @@ -.wrapper { - display: inline-flex; - background: url(~@/app/components/base/icons/assets/image/llm/tongyi-text.png) center center no-repeat; - background-size: contain; -} diff --git a/web/app/components/base/icons/src/image/llm/TongyiText.tsx b/web/app/components/base/icons/src/image/llm/TongyiText.tsx deleted file mode 100644 index a52f63c248..0000000000 --- a/web/app/components/base/icons/src/image/llm/TongyiText.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import cn from '@/utils/classnames' -import s from './TongyiText.module.css' - -const Icon = ( - { - ref, - className, - ...restProps - }: React.DetailedHTMLProps, HTMLSpanElement> & { - ref?: React.RefObject; - }, -) => - -Icon.displayName = 'TongyiText' - -export default Icon diff --git a/web/app/components/base/icons/src/image/llm/TongyiTextCn.module.css b/web/app/components/base/icons/src/image/llm/TongyiTextCn.module.css deleted file mode 100644 index d07e6e8bc4..0000000000 --- a/web/app/components/base/icons/src/image/llm/TongyiTextCn.module.css +++ /dev/null @@ -1,5 +0,0 @@ -.wrapper { - display: inline-flex; - background: url(~@/app/components/base/icons/assets/image/llm/tongyi-text-cn.png) center center no-repeat; - background-size: contain; -} diff --git a/web/app/components/base/icons/src/image/llm/TongyiTextCn.tsx b/web/app/components/base/icons/src/image/llm/TongyiTextCn.tsx deleted file mode 100644 index c982c73aed..0000000000 --- a/web/app/components/base/icons/src/image/llm/TongyiTextCn.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import cn from '@/utils/classnames' -import s from './TongyiTextCn.module.css' - -const Icon = ( - { - ref, - className, - ...restProps - }: React.DetailedHTMLProps, HTMLSpanElement> & { - ref?: React.RefObject; - }, -) => - -Icon.displayName = 'TongyiTextCn' - -export default Icon diff --git a/web/app/components/base/icons/src/image/llm/Wxyy.module.css b/web/app/components/base/icons/src/image/llm/Wxyy.module.css deleted file mode 100644 index 44344a495f..0000000000 --- a/web/app/components/base/icons/src/image/llm/Wxyy.module.css +++ /dev/null @@ -1,5 +0,0 @@ -.wrapper { - display: inline-flex; - background: url(~@/app/components/base/icons/assets/image/llm/wxyy.png) center center no-repeat; - background-size: contain; -} diff --git a/web/app/components/base/icons/src/image/llm/Wxyy.tsx b/web/app/components/base/icons/src/image/llm/Wxyy.tsx deleted file mode 100644 index a3c494811e..0000000000 --- a/web/app/components/base/icons/src/image/llm/Wxyy.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import cn from '@/utils/classnames' -import s from './Wxyy.module.css' - -const Icon = ( - { - ref, - className, - ...restProps - }: React.DetailedHTMLProps, HTMLSpanElement> & { - ref?: React.RefObject; - }, -) => - -Icon.displayName = 'Wxyy' - -export default Icon diff --git a/web/app/components/base/icons/src/image/llm/WxyyText.module.css b/web/app/components/base/icons/src/image/llm/WxyyText.module.css deleted file mode 100644 index 58a0c62047..0000000000 --- a/web/app/components/base/icons/src/image/llm/WxyyText.module.css +++ /dev/null @@ -1,5 +0,0 @@ -.wrapper { - display: inline-flex; - background: url(~@/app/components/base/icons/assets/image/llm/wxyy-text.png) center center no-repeat; - background-size: contain; -} diff --git a/web/app/components/base/icons/src/image/llm/WxyyText.tsx b/web/app/components/base/icons/src/image/llm/WxyyText.tsx deleted file mode 100644 index e5dd6e8803..0000000000 --- a/web/app/components/base/icons/src/image/llm/WxyyText.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import cn from '@/utils/classnames' -import s from './WxyyText.module.css' - -const Icon = ( - { - ref, - className, - ...restProps - }: React.DetailedHTMLProps, HTMLSpanElement> & { - ref?: React.RefObject; - }, -) => - -Icon.displayName = 'WxyyText' - -export default Icon diff --git a/web/app/components/base/icons/src/image/llm/WxyyTextCn.module.css b/web/app/components/base/icons/src/image/llm/WxyyTextCn.module.css deleted file mode 100644 index fb5839ab07..0000000000 --- a/web/app/components/base/icons/src/image/llm/WxyyTextCn.module.css +++ /dev/null @@ -1,5 +0,0 @@ -.wrapper { - display: inline-flex; - background: url(~@/app/components/base/icons/assets/image/llm/wxyy-text-cn.png) center center no-repeat; - background-size: contain; -} diff --git a/web/app/components/base/icons/src/image/llm/WxyyTextCn.tsx b/web/app/components/base/icons/src/image/llm/WxyyTextCn.tsx deleted file mode 100644 index 32108adab4..0000000000 --- a/web/app/components/base/icons/src/image/llm/WxyyTextCn.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import cn from '@/utils/classnames' -import s from './WxyyTextCn.module.css' - -const Icon = ( - { - ref, - className, - ...restProps - }: React.DetailedHTMLProps, HTMLSpanElement> & { - ref?: React.RefObject; - }, -) => - -Icon.displayName = 'WxyyTextCn' - -export default Icon diff --git a/web/app/components/base/icons/src/image/llm/index.ts b/web/app/components/base/icons/src/image/llm/index.ts deleted file mode 100644 index 3a4e64ac18..0000000000 --- a/web/app/components/base/icons/src/image/llm/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { default as BaichuanTextCn } from './BaichuanTextCn' -export { default as MinimaxText } from './MinimaxText' -export { default as Minimax } from './Minimax' -export { default as TongyiTextCn } from './TongyiTextCn' -export { default as TongyiText } from './TongyiText' -export { default as Tongyi } from './Tongyi' -export { default as WxyyTextCn } from './WxyyTextCn' -export { default as WxyyText } from './WxyyText' -export { default as Wxyy } from './Wxyy' diff --git a/web/app/components/base/icons/src/public/model/Checked.tsx b/web/app/components/base/icons/src/public/model/Checked.tsx deleted file mode 100644 index 7854479cd2..0000000000 --- a/web/app/components/base/icons/src/public/model/Checked.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import data from './Checked.json' -import IconBase from '@/app/components/base/icons/IconBase' -import type { IconData } from '@/app/components/base/icons/IconBase' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject>; - }, -) => - -Icon.displayName = 'Checked' - -export default Icon diff --git a/web/app/components/base/icons/src/public/model/index.ts b/web/app/components/base/icons/src/public/model/index.ts deleted file mode 100644 index 719a6f0309..0000000000 --- a/web/app/components/base/icons/src/public/model/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as Checked } from './Checked' diff --git a/web/app/components/base/icons/src/public/plugins/Google.tsx b/web/app/components/base/icons/src/public/plugins/Google.tsx deleted file mode 100644 index 3e19ecd2f8..0000000000 --- a/web/app/components/base/icons/src/public/plugins/Google.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import data from './Google.json' -import IconBase from '@/app/components/base/icons/IconBase' -import type { IconData } from '@/app/components/base/icons/IconBase' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject>; - }, -) => - -Icon.displayName = 'Google' - -export default Icon diff --git a/web/app/components/base/icons/src/public/plugins/WebReader.tsx b/web/app/components/base/icons/src/public/plugins/WebReader.tsx deleted file mode 100644 index 5606e32f88..0000000000 --- a/web/app/components/base/icons/src/public/plugins/WebReader.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import data from './WebReader.json' -import IconBase from '@/app/components/base/icons/IconBase' -import type { IconData } from '@/app/components/base/icons/IconBase' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject>; - }, -) => - -Icon.displayName = 'WebReader' - -export default Icon diff --git a/web/app/components/base/icons/src/public/plugins/Wikipedia.tsx b/web/app/components/base/icons/src/public/plugins/Wikipedia.tsx deleted file mode 100644 index c2fde5c1f8..0000000000 --- a/web/app/components/base/icons/src/public/plugins/Wikipedia.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import data from './Wikipedia.json' -import IconBase from '@/app/components/base/icons/IconBase' -import type { IconData } from '@/app/components/base/icons/IconBase' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject>; - }, -) => - -Icon.displayName = 'Wikipedia' - -export default Icon diff --git a/web/app/components/base/icons/src/public/plugins/index.ts b/web/app/components/base/icons/src/public/plugins/index.ts deleted file mode 100644 index 87dc37167c..0000000000 --- a/web/app/components/base/icons/src/public/plugins/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { default as Google } from './Google' -export { default as PartnerDark } from './PartnerDark' -export { default as PartnerLight } from './PartnerLight' -export { default as VerifiedDark } from './VerifiedDark' -export { default as VerifiedLight } from './VerifiedLight' -export { default as WebReader } from './WebReader' -export { default as Wikipedia } from './Wikipedia' diff --git a/web/app/components/base/icons/src/public/thought/DataSet.tsx b/web/app/components/base/icons/src/public/thought/DataSet.tsx index f35ff4efbc..e279c77ec7 100644 --- a/web/app/components/base/icons/src/public/thought/DataSet.tsx +++ b/web/app/components/base/icons/src/public/thought/DataSet.tsx @@ -18,3 +18,4 @@ const Icon = ( Icon.displayName = 'DataSet' export default Icon + diff --git a/web/app/components/base/icons/src/public/thought/Loading.tsx b/web/app/components/base/icons/src/public/thought/Loading.tsx deleted file mode 100644 index af959fba40..0000000000 --- a/web/app/components/base/icons/src/public/thought/Loading.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import data from './Loading.json' -import IconBase from '@/app/components/base/icons/IconBase' -import type { IconData } from '@/app/components/base/icons/IconBase' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject>; - }, -) => - -Icon.displayName = 'Loading' - -export default Icon diff --git a/web/app/components/base/icons/src/public/thought/Search.tsx b/web/app/components/base/icons/src/public/thought/Search.tsx deleted file mode 100644 index ecd98048d5..0000000000 --- a/web/app/components/base/icons/src/public/thought/Search.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import data from './Search.json' -import IconBase from '@/app/components/base/icons/IconBase' -import type { IconData } from '@/app/components/base/icons/IconBase' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject>; - }, -) => - -Icon.displayName = 'Search' - -export default Icon diff --git a/web/app/components/base/icons/src/public/thought/ThoughtList.tsx b/web/app/components/base/icons/src/public/thought/ThoughtList.tsx deleted file mode 100644 index e7f0e312ef..0000000000 --- a/web/app/components/base/icons/src/public/thought/ThoughtList.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import data from './ThoughtList.json' -import IconBase from '@/app/components/base/icons/IconBase' -import type { IconData } from '@/app/components/base/icons/IconBase' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject>; - }, -) => - -Icon.displayName = 'ThoughtList' - -export default Icon diff --git a/web/app/components/base/icons/src/public/thought/WebReader.tsx b/web/app/components/base/icons/src/public/thought/WebReader.tsx deleted file mode 100644 index 5606e32f88..0000000000 --- a/web/app/components/base/icons/src/public/thought/WebReader.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import data from './WebReader.json' -import IconBase from '@/app/components/base/icons/IconBase' -import type { IconData } from '@/app/components/base/icons/IconBase' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject>; - }, -) => - -Icon.displayName = 'WebReader' - -export default Icon diff --git a/web/app/components/base/icons/src/public/thought/index.ts b/web/app/components/base/icons/src/public/thought/index.ts index 8a45489dbf..35adcb50bb 100644 --- a/web/app/components/base/icons/src/public/thought/index.ts +++ b/web/app/components/base/icons/src/public/thought/index.ts @@ -1,5 +1,2 @@ export { default as DataSet } from './DataSet' -export { default as Loading } from './Loading' -export { default as Search } from './Search' -export { default as ThoughtList } from './ThoughtList' -export { default as WebReader } from './WebReader' + diff --git a/web/app/components/base/icons/src/vender/line/layout/AlignLeft01.tsx b/web/app/components/base/icons/src/vender/line/layout/AlignLeft01.tsx deleted file mode 100644 index 0761e89f56..0000000000 --- a/web/app/components/base/icons/src/vender/line/layout/AlignLeft01.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import data from './AlignLeft01.json' -import IconBase from '@/app/components/base/icons/IconBase' -import type { IconData } from '@/app/components/base/icons/IconBase' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject>; - }, -) => - -Icon.displayName = 'AlignLeft01' - -export default Icon diff --git a/web/app/components/base/icons/src/vender/line/layout/AlignRight01.tsx b/web/app/components/base/icons/src/vender/line/layout/AlignRight01.tsx deleted file mode 100644 index ffe1889ff8..0000000000 --- a/web/app/components/base/icons/src/vender/line/layout/AlignRight01.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import data from './AlignRight01.json' -import IconBase from '@/app/components/base/icons/IconBase' -import type { IconData } from '@/app/components/base/icons/IconBase' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject>; - }, -) => - -Icon.displayName = 'AlignRight01' - -export default Icon diff --git a/web/app/components/base/icons/src/vender/line/layout/Grid01.tsx b/web/app/components/base/icons/src/vender/line/layout/Grid01.tsx deleted file mode 100644 index bc9b6115be..0000000000 --- a/web/app/components/base/icons/src/vender/line/layout/Grid01.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import data from './Grid01.json' -import IconBase from '@/app/components/base/icons/IconBase' -import type { IconData } from '@/app/components/base/icons/IconBase' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject>; - }, -) => - -Icon.displayName = 'Grid01' - -export default Icon diff --git a/web/app/components/base/icons/src/vender/line/layout/index.ts b/web/app/components/base/icons/src/vender/line/layout/index.ts index 7c12b1f58f..a6aa205faa 100644 --- a/web/app/components/base/icons/src/vender/line/layout/index.ts +++ b/web/app/components/base/icons/src/vender/line/layout/index.ts @@ -1,4 +1 @@ -export { default as AlignLeft01 } from './AlignLeft01' -export { default as AlignRight01 } from './AlignRight01' -export { default as Grid01 } from './Grid01' export { default as LayoutGrid02 } from './LayoutGrid02' diff --git a/web/app/components/base/icons/src/vender/line/mapsAndTravel/Route.tsx b/web/app/components/base/icons/src/vender/line/mapsAndTravel/Route.tsx deleted file mode 100644 index 9cbde4a15e..0000000000 --- a/web/app/components/base/icons/src/vender/line/mapsAndTravel/Route.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import data from './Route.json' -import IconBase from '@/app/components/base/icons/IconBase' -import type { IconData } from '@/app/components/base/icons/IconBase' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject>; - }, -) => - -Icon.displayName = 'Route' - -export default Icon diff --git a/web/app/components/base/icons/src/vender/line/mapsAndTravel/index.ts b/web/app/components/base/icons/src/vender/line/mapsAndTravel/index.ts deleted file mode 100644 index 82cb320ff2..0000000000 --- a/web/app/components/base/icons/src/vender/line/mapsAndTravel/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as Route } from './Route' diff --git a/web/app/components/base/icons/src/vender/line/users/User01.tsx b/web/app/components/base/icons/src/vender/line/users/User01.tsx deleted file mode 100644 index 42f2144b97..0000000000 --- a/web/app/components/base/icons/src/vender/line/users/User01.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import data from './User01.json' -import IconBase from '@/app/components/base/icons/IconBase' -import type { IconData } from '@/app/components/base/icons/IconBase' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject>; - }, -) => - -Icon.displayName = 'User01' - -export default Icon diff --git a/web/app/components/base/icons/src/vender/line/users/Users01.tsx b/web/app/components/base/icons/src/vender/line/users/Users01.tsx deleted file mode 100644 index b63daf7242..0000000000 --- a/web/app/components/base/icons/src/vender/line/users/Users01.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import data from './Users01.json' -import IconBase from '@/app/components/base/icons/IconBase' -import type { IconData } from '@/app/components/base/icons/IconBase' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject>; - }, -) => - -Icon.displayName = 'Users01' - -export default Icon diff --git a/web/app/components/base/icons/src/vender/line/users/index.ts b/web/app/components/base/icons/src/vender/line/users/index.ts deleted file mode 100644 index 9f8a35152f..0000000000 --- a/web/app/components/base/icons/src/vender/line/users/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as User01 } from './User01' -export { default as Users01 } from './Users01' diff --git a/web/app/components/base/icons/src/vender/line/weather/Stars02.tsx b/web/app/components/base/icons/src/vender/line/weather/Stars02.tsx deleted file mode 100644 index 8a42448c70..0000000000 --- a/web/app/components/base/icons/src/vender/line/weather/Stars02.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import data from './Stars02.json' -import IconBase from '@/app/components/base/icons/IconBase' -import type { IconData } from '@/app/components/base/icons/IconBase' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject>; - }, -) => - -Icon.displayName = 'Stars02' - -export default Icon diff --git a/web/app/components/base/icons/src/vender/line/weather/index.ts b/web/app/components/base/icons/src/vender/line/weather/index.ts deleted file mode 100644 index 1a68bce765..0000000000 --- a/web/app/components/base/icons/src/vender/line/weather/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as Stars02 } from './Stars02' diff --git a/web/app/components/base/icons/src/vender/solid/arrows/ChevronDown.tsx b/web/app/components/base/icons/src/vender/solid/arrows/ChevronDown.tsx deleted file mode 100644 index 643ddfbf79..0000000000 --- a/web/app/components/base/icons/src/vender/solid/arrows/ChevronDown.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import data from './ChevronDown.json' -import IconBase from '@/app/components/base/icons/IconBase' -import type { IconData } from '@/app/components/base/icons/IconBase' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject>; - }, -) => - -Icon.displayName = 'ChevronDown' - -export default Icon diff --git a/web/app/components/base/icons/src/vender/solid/arrows/HighPriority.tsx b/web/app/components/base/icons/src/vender/solid/arrows/HighPriority.tsx deleted file mode 100644 index af6fa05e5c..0000000000 --- a/web/app/components/base/icons/src/vender/solid/arrows/HighPriority.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import data from './HighPriority.json' -import IconBase from '@/app/components/base/icons/IconBase' -import type { IconData } from '@/app/components/base/icons/IconBase' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject>; - }, -) => - -Icon.displayName = 'HighPriority' - -export default Icon diff --git a/web/app/components/base/icons/src/vender/solid/arrows/index.ts b/web/app/components/base/icons/src/vender/solid/arrows/index.ts deleted file mode 100644 index 468f0e52d9..0000000000 --- a/web/app/components/base/icons/src/vender/solid/arrows/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as ChevronDown } from './ChevronDown' -export { default as HighPriority } from './HighPriority' diff --git a/web/app/components/base/icons/src/vender/solid/layout/Grid01.tsx b/web/app/components/base/icons/src/vender/solid/layout/Grid01.tsx deleted file mode 100644 index bc9b6115be..0000000000 --- a/web/app/components/base/icons/src/vender/solid/layout/Grid01.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import data from './Grid01.json' -import IconBase from '@/app/components/base/icons/IconBase' -import type { IconData } from '@/app/components/base/icons/IconBase' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject>; - }, -) => - -Icon.displayName = 'Grid01' - -export default Icon diff --git a/web/app/components/base/icons/src/vender/solid/layout/index.ts b/web/app/components/base/icons/src/vender/solid/layout/index.ts deleted file mode 100644 index 73a2513d51..0000000000 --- a/web/app/components/base/icons/src/vender/solid/layout/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as Grid01 } from './Grid01' diff --git a/web/app/components/develop/template/template.en.mdx b/web/app/components/develop/template/template.en.mdx index 38208bb7e0..8cb5f07ae8 100755 --- a/web/app/components/develop/template/template.en.mdx +++ b/web/app/components/develop/template/template.en.mdx @@ -767,8 +767,8 @@ The text generation application offers non-session support and is ideal for tran diff --git a/web/app/components/develop/template/template.ja.mdx b/web/app/components/develop/template/template.ja.mdx index 892197bf56..9df4421865 100755 --- a/web/app/components/develop/template/template.ja.mdx +++ b/web/app/components/develop/template/template.ja.mdx @@ -764,8 +764,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from diff --git a/web/app/components/develop/template/template.zh.mdx b/web/app/components/develop/template/template.zh.mdx index 8d85e9685f..8436b51ce4 100755 --- a/web/app/components/develop/template/template.zh.mdx +++ b/web/app/components/develop/template/template.zh.mdx @@ -728,8 +728,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' diff --git a/web/app/components/develop/template/template_chat.en.mdx b/web/app/components/develop/template/template_chat.en.mdx index 82b50d58ea..df31177127 100644 --- a/web/app/components/develop/template/template_chat.en.mdx +++ b/web/app/components/develop/template/template_chat.en.mdx @@ -199,7 +199,7 @@ Chat applications support session persistence, allowing previous chat history to --header 'Authorization: Bearer {api_key}' \\ --header 'Content-Type: application/json' \\ --data-raw '{ - "inputs": ${JSON.stringify(props.inputs)}, + "inputs": ${JSON.stringify(props.inputs)}, "query": "What are the specs of the iPhone 13 Pro Max?", "response_mode": "streaming", "conversation_id": "", @@ -1182,7 +1182,7 @@ Chat applications support session persistence, allowing previous chat history to --header 'Content-Type: application/json' \\ --data-raw '{ "value": "Updated Value", - "user": "abc-123" + "user": "abc-123" }'`} /> @@ -1599,8 +1599,8 @@ Chat applications support session persistence, allowing previous chat history to diff --git a/web/app/components/develop/template/template_chat.ja.mdx b/web/app/components/develop/template/template_chat.ja.mdx index 69f48d0216..eafa653cad 100644 --- a/web/app/components/develop/template/template_chat.ja.mdx +++ b/web/app/components/develop/template/template_chat.ja.mdx @@ -1586,8 +1586,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from diff --git a/web/app/components/develop/template/template_chat.zh.mdx b/web/app/components/develop/template/template_chat.zh.mdx index 581daef726..fc3fd6d0d2 100644 --- a/web/app/components/develop/template/template_chat.zh.mdx +++ b/web/app/components/develop/template/template_chat.zh.mdx @@ -1188,7 +1188,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' --header 'Content-Type: application/json' \\ --data-raw '{ "value": "Updated Value", - "user": "abc-123" + "user": "abc-123" }'`} /> @@ -1579,8 +1579,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' diff --git a/web/app/components/develop/template/template_workflow.en.mdx b/web/app/components/develop/template/template_workflow.en.mdx index f286773685..a820516e88 100644 --- a/web/app/components/develop/template/template_workflow.en.mdx +++ b/web/app/components/develop/template/template_workflow.en.mdx @@ -1025,8 +1025,8 @@ Workflow applications offers non-session support and is ideal for translation, a diff --git a/web/app/components/develop/template/template_workflow.ja.mdx b/web/app/components/develop/template/template_workflow.ja.mdx index 3b1a8e1c36..ff809b8a9c 100644 --- a/web/app/components/develop/template/template_workflow.ja.mdx +++ b/web/app/components/develop/template/template_workflow.ja.mdx @@ -1021,8 +1021,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from diff --git a/web/app/components/develop/template/template_workflow.zh.mdx b/web/app/components/develop/template/template_workflow.zh.mdx index 2cdc88de5a..0e2b19df83 100644 --- a/web/app/components/develop/template/template_workflow.zh.mdx +++ b/web/app/components/develop/template/template_workflow.zh.mdx @@ -182,7 +182,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 --header 'Authorization: Bearer {api_key}' \\ --header 'Content-Type: application/json' \\ --data-raw '{ - "inputs": ${JSON.stringify(props.inputs)}, + "inputs": ${JSON.stringify(props.inputs)}, "response_mode": "streaming", "user": "abc-123" }'`} @@ -1012,8 +1012,8 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx b/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx index fd1cd01abf..d6f56b8c43 100644 --- a/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx +++ b/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx @@ -118,8 +118,20 @@ const FormItem: FC = ({
{!isArrayLikeType && !isBooleanType && (
-
{typeof payload.label === 'object' ? nodeKey : payload.label}
- {!payload.required && {t('workflow.panel.optional')}} +
+ {typeof payload.label === 'object' ? nodeKey : payload.label} +
+ {payload.hide === true ? ( + + {t('workflow.panel.optional_and_hidden')} + + ) : ( + !payload.required && ( + + {t('workflow.panel.optional')} + + ) + )}
)}
diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx index 447dfe1f40..5e84a518cc 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx @@ -61,7 +61,6 @@ const VarList: FC = ({ return } if (list.some(item => item.variable?.trim() === newKey.trim())) { - console.log('new key', newKey.trim()) setToastHandle(Toast.notify({ type: 'error', message: t('appDebug.varKeyError.keyAlreadyExists', { key: newKey }), diff --git a/web/app/components/workflow/nodes/start/components/var-list.tsx b/web/app/components/workflow/nodes/start/components/var-list.tsx index bbfeed461a..36524b482f 100644 --- a/web/app/components/workflow/nodes/start/components/var-list.tsx +++ b/web/app/components/workflow/nodes/start/components/var-list.tsx @@ -5,7 +5,6 @@ import produce from 'immer' import { useTranslation } from 'react-i18next' import VarItem from './var-item' import { ChangeType, type InputVar, type MoreInfo } from '@/app/components/workflow/types' -import { v4 as uuid4 } from 'uuid' import { ReactSortable } from 'react-sortablejs' import { RiDraggable } from '@remixicon/react' import cn from '@/utils/classnames' @@ -71,9 +70,8 @@ const VarList: FC = ({ }, [list, onChange]) const listWithIds = useMemo(() => list.map((item) => { - const id = uuid4() return { - id, + id: item.variable, variable: { ...item }, } }), [list]) @@ -88,6 +86,8 @@ const VarList: FC = ({ ) } + const canDrag = !readonly && varCount > 1 + return ( = ({ ghostClass='opacity-50' animation={150} > - {list.map((item, index) => { - const canDrag = (() => { - if (readonly) - return false - return varCount > 1 - })() - return ( -
- item.variable)} - canDrag={canDrag} - /> - {canDrag &&
- ) - })} + {listWithIds.map((itemWithId, index) => ( +
+ item.variable)} + canDrag={canDrag} + /> + {canDrag &&
+ ))}
) } diff --git a/web/app/components/workflow/panel/debug-and-preview/index.tsx b/web/app/components/workflow/panel/debug-and-preview/index.tsx index baf4c21dcd..9c64849dc5 100644 --- a/web/app/components/workflow/panel/debug-and-preview/index.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/index.tsx @@ -39,7 +39,7 @@ const DebugAndPreview = () => { const selectedNode = nodes.find(node => node.data.selected) const startNode = nodes.find(node => node.data.type === BlockEnum.Start) const variables = startNode?.data.variables || [] - const visibleVariables = variables.filter(v => v.hide !== true) + const visibleVariables = variables const [showConversationVariableModal, setShowConversationVariableModal] = useState(false) diff --git a/web/app/components/workflow/panel/debug-and-preview/user-input.tsx b/web/app/components/workflow/panel/debug-and-preview/user-input.tsx index 0670d9a52b..b7ed2a54fd 100644 --- a/web/app/components/workflow/panel/debug-and-preview/user-input.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/user-input.tsx @@ -14,10 +14,11 @@ import cn from '@/utils/classnames' const UserInput = () => { const workflowStore = useWorkflowStore() const inputs = useStore(s => s.inputs) + const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel) const nodes = useNodes() const startNode = nodes.find(node => node.data.type === BlockEnum.Start) const variables = startNode?.data.variables || [] - const visibleVariables = variables.filter(v => v.hide !== true) + const visibleVariables = showDebugAndPreviewPanel ? variables : variables.filter(v => v.hide !== true) const handleValueChange = (variable: string, v: string) => { const { diff --git a/web/i18n/de-DE/app.ts b/web/i18n/de-DE/app.ts index 3c99aae7df..480efa6880 100644 --- a/web/i18n/de-DE/app.ts +++ b/web/i18n/de-DE/app.ts @@ -160,6 +160,10 @@ const translation = { title: 'Cloud-Monitor', description: 'Die vollständig verwaltete und wartungsfreie Observability-Plattform von Alibaba Cloud ermöglicht eine sofortige Überwachung, Verfolgung und Bewertung von Dify-Anwendungen.', }, + tencent: { + title: 'Tencent APM', + description: 'Tencent Application Performance Monitoring bietet umfassendes Tracing und multidimensionale Analyse für LLM-Anwendungen.', + }, }, answerIcon: { descriptionInExplore: 'Gibt an, ob das web app Symbol zum Ersetzen 🤖 in Explore verwendet werden soll', diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 0df20fd37e..e021c7ef45 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -354,6 +354,7 @@ const translation = { optional: '(optional)', maximize: 'Maximize Canvas', minimize: 'Exit Full Screen', + optional_and_hidden: '(optional & hidden)', }, nodes: { common: { diff --git a/web/i18n/es-ES/app.ts b/web/i18n/es-ES/app.ts index b56a1dc01a..5e738b0ecf 100644 --- a/web/i18n/es-ES/app.ts +++ b/web/i18n/es-ES/app.ts @@ -163,6 +163,10 @@ const translation = { title: 'Monitor de Nubes', description: 'La plataforma de observabilidad totalmente gestionada y sin mantenimiento proporcionada por Alibaba Cloud, permite la monitorización, trazado y evaluación de aplicaciones Dify de manera inmediata.', }, + tencent: { + title: 'Tencent APM', + description: 'Tencent Application Performance Monitoring proporciona rastreo integral y análisis multidimensional para aplicaciones LLM.', + }, }, answerIcon: { title: 'Usar el icono de la aplicación web para reemplazar 🤖', diff --git a/web/i18n/fa-IR/app.ts b/web/i18n/fa-IR/app.ts index 1ec590757e..d4c71adc6e 100644 --- a/web/i18n/fa-IR/app.ts +++ b/web/i18n/fa-IR/app.ts @@ -171,6 +171,10 @@ const translation = { title: 'نظارت بر ابر', description: 'پلتفرم مشاهده‌پذیری کاملاً مدیریت‌شده و بدون نیاز به نگهداری که توسط Alibaba Cloud ارائه شده، امکان نظارت، ردیابی و ارزیابی برنامه‌های Dify را به‌صورت آماده و با تنظیمات اولیه فراهم می‌کند.', }, + tencent: { + title: 'تنست ای‌پی‌ام', + description: 'نظارت بر عملکرد برنامه‌های Tencent تحلیل‌های جامع و ردیابی چندبعدی برای برنامه‌های LLM ارائه می‌دهد.', + }, }, answerIcon: { descriptionInExplore: 'آیا از نماد web app برای جایگزینی 🤖 در Explore استفاده کنیم یا خیر', diff --git a/web/i18n/fr-FR/app.ts b/web/i18n/fr-FR/app.ts index d39d5dd7be..ee9434e5f2 100644 --- a/web/i18n/fr-FR/app.ts +++ b/web/i18n/fr-FR/app.ts @@ -163,6 +163,10 @@ const translation = { title: 'Surveillance Cloud', description: 'La plateforme d\'observabilité entièrement gérée et sans maintenance fournie par Alibaba Cloud permet une surveillance, un traçage et une évaluation prêts à l\'emploi des applications Dify.', }, + tencent: { + title: 'Tencent APM', + description: 'Tencent Application Performance Monitoring fournit une traçabilité complète et une analyse multidimensionnelle pour les applications LLM.', + }, }, answerIcon: { description: 'S’il faut utiliser l’icône web app pour remplacer 🤖 dans l’application partagée', diff --git a/web/i18n/hi-IN/app.ts b/web/i18n/hi-IN/app.ts index 2be81f268a..211ca738a2 100644 --- a/web/i18n/hi-IN/app.ts +++ b/web/i18n/hi-IN/app.ts @@ -163,6 +163,10 @@ const translation = { title: 'क्लाउड मॉनिटर', description: 'अलीबाबा क्लाउड द्वारा प्रदान की गई पूरी तरह से प्रबंधित और रखरखाव-मुक्त अवलोकन प्लेटफ़ॉर्म, Dify अनुप्रयोगों की स्वचालित निगरानी, ट्रेसिंग और मूल्यांकन का सक्षम बनाता है।', }, + tencent: { + title: 'टेनसेंट एपीएम', + description: 'Tencent एप्लिकेशन परफॉर्मेंस मॉनिटरिंग LLM एप्लिकेशन के लिए व्यापक ट्रेसिंग और बहु-आयामी विश्लेषण प्रदान करता है।', + }, }, answerIcon: { title: 'बदलने 🤖 के लिए web app चिह्न का उपयोग करें', diff --git a/web/i18n/id-ID/app.ts b/web/i18n/id-ID/app.ts index 776ac8ee18..2072bec35e 100644 --- a/web/i18n/id-ID/app.ts +++ b/web/i18n/id-ID/app.ts @@ -155,6 +155,10 @@ const translation = { description: 'Mengonfigurasi penyedia LLMOps Pihak Ketiga dan melacak performa aplikasi.', inUse: 'Sedang digunakan', tracingDescription: 'Tangkap konteks lengkap eksekusi aplikasi, termasuk panggilan LLM, konteks, perintah, permintaan HTTP, dan lainnya, ke platform pelacakan pihak ketiga.', + tencent: { + title: 'Tencent APM', + description: 'Tencent Application Performance Monitoring menyediakan pelacakan komprehensif dan analisis multi-dimensi untuk aplikasi LLM.', + }, }, appSelector: { placeholder: 'Pilih aplikasi...', diff --git a/web/i18n/it-IT/app.ts b/web/i18n/it-IT/app.ts index be7397e191..3c87e65b33 100644 --- a/web/i18n/it-IT/app.ts +++ b/web/i18n/it-IT/app.ts @@ -169,6 +169,10 @@ const translation = { title: 'Monitoraggio Cloud', description: 'La piattaforma di osservabilità completamente gestita e senza manutenzione fornita da Alibaba Cloud consente il monitoraggio, il tracciamento e la valutazione delle applicazioni Dify fin da subito.', }, + tencent: { + title: 'Tencent APM', + description: 'Tencent Application Performance Monitoring fornisce tracciamento completo e analisi multidimensionale per le applicazioni LLM.', + }, }, answerIcon: { description: 'Se utilizzare l\'icona web app per la sostituzione 🤖 nell\'applicazione condivisa', diff --git a/web/i18n/ja-JP/app.ts b/web/i18n/ja-JP/app.ts index 50dfcab043..4625d69c52 100644 --- a/web/i18n/ja-JP/app.ts +++ b/web/i18n/ja-JP/app.ts @@ -175,6 +175,10 @@ const translation = { title: 'クラウドモニター', description: 'Alibaba Cloud が提供する完全管理型でメンテナンスフリーの可観測性プラットフォームは、Dify アプリケーションの即時監視、トレース、評価を可能にします。', }, + tencent: { + title: 'テンセントAPM', + description: 'Tencent アプリケーションパフォーマンスモニタリングは、LLM アプリケーションに対して包括的なトレーシングと多次元分析を提供します。', + }, }, answerIcon: { title: 'Web アプリアイコンを使用して🤖を置き換える', diff --git a/web/i18n/ko-KR/app.ts b/web/i18n/ko-KR/app.ts index 7196ecd490..8c64644563 100644 --- a/web/i18n/ko-KR/app.ts +++ b/web/i18n/ko-KR/app.ts @@ -178,6 +178,10 @@ const translation = { title: '클라우드 모니터', description: '알리바바 클라우드에서 제공하는 완전 관리형 및 유지보수가 필요 없는 가시성 플랫폼은 Dify 애플리케이션의 모니터링, 추적 및 평가를 즉시 사용할 수 있도록 지원합니다.', }, + tencent: { + title: '텐센트 APM', + description: '텐센트 애플리케이션 성능 모니터링은 LLM 애플리케이션에 대한 포괄적인 추적 및 다차원 분석을 제공합니다.', + }, }, answerIcon: { description: diff --git a/web/i18n/pl-PL/app.ts b/web/i18n/pl-PL/app.ts index f18e04dea8..9b06320620 100644 --- a/web/i18n/pl-PL/app.ts +++ b/web/i18n/pl-PL/app.ts @@ -164,6 +164,10 @@ const translation = { title: 'Monitor Chmury', description: 'W pełni zarządzana i wolna od konserwacji platforma obserwowalności oferowana przez Alibaba Cloud umożliwia gotowe monitorowanie, śledzenie i oceny aplikacji Dify.', }, + tencent: { + title: 'Tencent APM', + description: 'Tencent Application Performance Monitoring zapewnia kompleksowe śledzenie i wielowymiarową analizę dla aplikacji LLM.', + }, }, answerIcon: { description: 'Czy w aplikacji udostępnionej ma być używana ikona aplikacji internetowej do zamiany 🤖.', diff --git a/web/i18n/pt-BR/app.ts b/web/i18n/pt-BR/app.ts index 7e238d7f7f..3051268f8f 100644 --- a/web/i18n/pt-BR/app.ts +++ b/web/i18n/pt-BR/app.ts @@ -163,6 +163,10 @@ const translation = { title: 'Monitoramento em Nuvem', description: 'A plataforma de observabilidade totalmente gerenciada e sem manutenção fornecida pela Alibaba Cloud, permite monitoramento, rastreamento e avaliação prontos para uso de aplicações Dify.', }, + tencent: { + title: 'Tencent APM', + description: 'O Monitoramento de Desempenho de Aplicações da Tencent fornece rastreamento abrangente e análise multidimensional para aplicações LLM.', + }, }, answerIcon: { descriptionInExplore: 'Se o ícone do web app deve ser usado para substituir 🤖 no Explore', diff --git a/web/i18n/ro-RO/app.ts b/web/i18n/ro-RO/app.ts index 5cac64668a..53c8de2ef4 100644 --- a/web/i18n/ro-RO/app.ts +++ b/web/i18n/ro-RO/app.ts @@ -163,6 +163,10 @@ const translation = { description: 'Platforma de observabilitate SaaS oferită de Alibaba Cloud permite monitorizarea, urmărirea și evaluarea aplicațiilor Dify din cutie.', title: 'Monitorizarea Cloud', }, + tencent: { + title: 'Tencent APM', + description: 'Monitorizarea Performanței Aplicațiilor Tencent oferă trasabilitate cuprinzătoare și analiză multidimensională pentru aplicațiile LLM.', + }, }, answerIcon: { descriptionInExplore: 'Dacă să utilizați pictograma web app pentru a înlocui 🤖 în Explore', diff --git a/web/i18n/ru-RU/app.ts b/web/i18n/ru-RU/app.ts index 0d466f09b3..86f5a83ec1 100644 --- a/web/i18n/ru-RU/app.ts +++ b/web/i18n/ru-RU/app.ts @@ -171,6 +171,10 @@ const translation = { title: 'Облачный монитор', description: 'Полностью управляемая и не требующая обслуживания платформа наблюдения, предоставляемая Alibaba Cloud, обеспечивает мониторинг, трассировку и оценку приложений Dify из коробки.', }, + tencent: { + title: 'Tencent APM', + description: 'Мониторинг производительности приложений Tencent предоставляет всестороннее отслеживание и многомерный анализ для приложений LLM.', + }, }, answerIcon: { title: 'Использование значка web app для замены 🤖', diff --git a/web/i18n/sl-SI/app.ts b/web/i18n/sl-SI/app.ts index 19a2e38006..d755b371ba 100644 --- a/web/i18n/sl-SI/app.ts +++ b/web/i18n/sl-SI/app.ts @@ -176,6 +176,10 @@ const translation = { title: 'Oblačni nadzor', description: 'Popolnoma upravljana in brez vzdrževanja platforma za opazovanje, ki jo zagotavlja Alibaba Cloud, omogoča takojšnje spremljanje, sledenje in ocenjevanje aplikacij Dify.', }, + tencent: { + description: 'Tencent Application Performance Monitoring zagotavlja celovito sledenje in večdimenzionalno analizo za aplikacije LLM.', + title: 'Tencent APM', + }, }, mermaid: { handDrawn: 'Ročno narisano', diff --git a/web/i18n/th-TH/app.ts b/web/i18n/th-TH/app.ts index 4efdc907f4..18e9511259 100644 --- a/web/i18n/th-TH/app.ts +++ b/web/i18n/th-TH/app.ts @@ -172,6 +172,10 @@ const translation = { title: 'การตรวจสอบคลาวด์', description: 'แพลตฟอร์มการสังเกตการณ์ที่จัดการโดย Alibaba Cloud ซึ่งไม่ต้องดูแลและบำรุงรักษา ช่วยให้สามารถติดตาม ตรวจสอบ และประเมินแอปพลิเคชัน Dify ได้ทันที', }, + tencent: { + title: 'Tencent APM', + description: 'การติดตามประสิทธิภาพแอปพลิเคชันของ Tencent มอบการตรวจสอบแบบครบวงจรและการวิเคราะห์หลายมิติสำหรับแอป LLM', + }, }, mermaid: { handDrawn: 'วาดด้วยมือ', diff --git a/web/i18n/tr-TR/app.ts b/web/i18n/tr-TR/app.ts index 8ed2bcedab..2f78f452a5 100644 --- a/web/i18n/tr-TR/app.ts +++ b/web/i18n/tr-TR/app.ts @@ -167,6 +167,10 @@ const translation = { title: 'Bulut İzleyici', description: 'Alibaba Cloud tarafından sağlanan tamamen yönetilen ve bakım gerektirmeyen gözlemleme platformu, Dify uygulamalarının kutudan çıkar çıkmaz izlenmesi, takip edilmesi ve değerlendirilmesine olanak tanır.', }, + tencent: { + title: 'Tencent APM', + description: 'Tencent Uygulama Performans İzleme, LLM uygulamaları için kapsamlı izleme ve çok boyutlu analiz sağlar.', + }, }, answerIcon: { descriptionInExplore: 'Keşfet\'te değiştirilecek 🤖 web app simgesinin kullanılıp kullanılmayacağı', diff --git a/web/i18n/uk-UA/app.ts b/web/i18n/uk-UA/app.ts index 95e5c9521e..ffd50a7cb4 100644 --- a/web/i18n/uk-UA/app.ts +++ b/web/i18n/uk-UA/app.ts @@ -163,6 +163,10 @@ const translation = { title: 'Моніторинг Хмари', description: 'Повністю керовані та без обслуговування платформи спостереження, надані Alibaba Cloud, дозволяють миттєвий моніторинг, трасування та оцінку застосувань Dify.', }, + tencent: { + title: 'Tencent APM', + description: 'Сервіс моніторингу продуктивності додатків Tencent забезпечує комплексне трасування та багатовимірний аналіз додатків LLM.', + }, }, answerIcon: { title: 'Використовуйте піктограму web app для заміни 🤖', diff --git a/web/i18n/vi-VN/app.ts b/web/i18n/vi-VN/app.ts index 81f76131a4..5efd1af4a6 100644 --- a/web/i18n/vi-VN/app.ts +++ b/web/i18n/vi-VN/app.ts @@ -163,6 +163,10 @@ const translation = { title: 'Giám sát Đám mây', description: 'Nền tảng quan sát được quản lý hoàn toàn và không cần bảo trì do Alibaba Cloud cung cấp, cho phép giám sát, theo dõi và đánh giá các ứng dụng Dify ngay lập tức.', }, + tencent: { + title: 'Tencent APM', + description: 'Giám sát hiệu suất ứng dụng của Tencent cung cấp khả năng theo dõi toàn diện và phân tích đa chiều cho các ứng dụng LLM.', + }, }, answerIcon: { description: 'Có nên sử dụng biểu tượng web app để thay thế 🤖 trong ứng dụng được chia sẻ hay không', diff --git a/web/i18n/zh-Hans/common.ts b/web/i18n/zh-Hans/common.ts index e73ac4cc6b..98ffb49782 100644 --- a/web/i18n/zh-Hans/common.ts +++ b/web/i18n/zh-Hans/common.ts @@ -20,7 +20,7 @@ const translation = { save: '保存', yes: '是', no: '否', - deleteConfirmTitle: '删除?', + deleteConfirmTitle: '删除?', confirmAction: '请确认您的操作。', saveAndEnable: '保存并启用', edit: '编辑', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 8099594af4..4d97dc9e59 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -354,6 +354,7 @@ const translation = { moveToThisNode: '定位至此节点', maximize: '最大化画布', minimize: '退出最大化', + optional_and_hidden: '(选填 & 隐藏)', }, nodes: { common: { diff --git a/web/i18n/zh-Hant/app.ts b/web/i18n/zh-Hant/app.ts index c1d6ebf2cd..c7a69d9b3c 100644 --- a/web/i18n/zh-Hant/app.ts +++ b/web/i18n/zh-Hant/app.ts @@ -162,6 +162,10 @@ const translation = { title: '雲端監控', description: '阿里雲提供的完全管理且無需維護的可觀察性平台,支持即時監控、追蹤和評估 Dify 應用程序。', }, + tencent: { + title: '騰訊 APM', + description: '騰訊應用性能監控為大型語言模型應用提供全面的追蹤和多維分析。', + }, }, answerIcon: { descriptionInExplore: '是否使用 web app 圖示在 Explore 中取代 🤖', diff --git a/web/knip.config.ts b/web/knip.config.ts new file mode 100644 index 0000000000..b9fb43f171 --- /dev/null +++ b/web/knip.config.ts @@ -0,0 +1,288 @@ +import type { KnipConfig } from 'knip' + +/** + * Knip Configuration for Dead Code Detection + * + * This configuration helps identify unused files, exports, and dependencies + * in the Dify web application (Next.js 15 + TypeScript + React 19). + * + * ⚠️ SAFETY FIRST: This configuration is designed to be conservative and + * avoid false positives that could lead to deleting actively used code. + * + * @see https://knip.dev/reference/configuration + */ +const config: KnipConfig = { + // ============================================================================ + // Next.js Framework Configuration + // ============================================================================ + // Configure entry points specific to Next.js application structure. + // These files are automatically treated as entry points by the framework. + next: { + entry: [ + // Next.js App Router pages (must exist for routing) + 'app/**/page.tsx', + 'app/**/layout.tsx', + 'app/**/loading.tsx', + 'app/**/error.tsx', + 'app/**/not-found.tsx', + 'app/**/template.tsx', + 'app/**/default.tsx', + + // Middleware (runs before every route) + 'middleware.ts', + + // Configuration files + 'next.config.js', + 'tailwind.config.js', + 'tailwind-common-config.ts', + 'postcss.config.js', + + // Testing configuration + 'jest.config.ts', + 'jest.setup.ts', + + // Linting configuration + 'eslint.config.mjs', + ], + }, + + // ============================================================================ + // Global Entry Points + // ============================================================================ + // Files that serve as entry points for the application. + // The '!' suffix means these patterns take precedence and are always included. + entry: [ + // Next.js App Router patterns (high priority) + 'app/**/page.tsx!', + 'app/**/layout.tsx!', + 'app/**/loading.tsx!', + 'app/**/error.tsx!', + 'app/**/not-found.tsx!', + 'app/**/template.tsx!', + 'app/**/default.tsx!', + + // Core configuration files + 'middleware.ts!', + 'next.config.js!', + 'tailwind.config.js!', + 'tailwind-common-config.ts!', + 'postcss.config.js!', + + // Testing setup + 'jest.config.ts!', + 'jest.setup.ts!', + + // Linting setup + 'eslint.config.mjs!', + + // ======================================================================== + // 🔒 CRITICAL: Global Initializers and Providers + // ======================================================================== + // These files are imported by root layout.tsx and provide global functionality. + // Even if not directly imported elsewhere, they are essential for app initialization. + + // Browser initialization (runs on client startup) + 'app/components/browser-initializer.tsx!', + 'app/components/sentry-initializer.tsx!', + 'app/components/swr-initializer.tsx!', + + // i18n initialization (server and client) + 'app/components/i18n.tsx!', + 'app/components/i18n-server.tsx!', + + // Route prefix handling (used in root layout) + 'app/routePrefixHandle.tsx!', + + // ======================================================================== + // 🔒 CRITICAL: Context Providers + // ======================================================================== + // Context providers might be used via React Context API and imported dynamically. + // Protecting all context files to prevent breaking the provider chain. + 'context/**/*.ts?(x)!', + + // Component-level contexts (also used via React.useContext) + 'app/components/**/*.context.ts?(x)!', + + // ======================================================================== + // 🔒 CRITICAL: State Management Stores + // ======================================================================== + // Zustand stores might be imported dynamically or via hooks. + // These are often imported at module level, so they should be protected. + 'app/components/**/*.store.ts?(x)!', + 'context/**/*.store.ts?(x)!', + + // ======================================================================== + // 🔒 CRITICAL: Provider Components + // ======================================================================== + // Provider components wrap the app and provide global state/functionality + 'app/components/**/*.provider.ts?(x)!', + 'context/**/*.provider.ts?(x)!', + + // ======================================================================== + // Development tools + // ======================================================================== + // Storybook configuration + '.storybook/**/*', + ], + + // ============================================================================ + // Project Files to Analyze + // ============================================================================ + // Glob patterns for files that should be analyzed for unused code. + // Excludes test files to avoid false positives. + project: [ + '**/*.{js,jsx,ts,tsx,mjs,cjs}', + ], + + // ============================================================================ + // Ignored Files and Directories + // ============================================================================ + // Files and directories that should be completely excluded from analysis. + // These typically contain: + // - Test files + // - Internationalization files (loaded dynamically) + // - Static assets + // - Build outputs + // - External libraries + ignore: [ + // Test files and directories + '**/__tests__/**', + '**/*.spec.{ts,tsx}', + '**/*.test.{ts,tsx}', + + // ======================================================================== + // 🔒 CRITICAL: i18n Files (Dynamically Loaded) + // ======================================================================== + // Internationalization files are loaded dynamically at runtime via i18next. + // Pattern: import(`@/i18n/${locale}/messages`) + // These will NEVER show up in static analysis but are essential! + 'i18n/**', + + // ======================================================================== + // 🔒 CRITICAL: Static Assets + // ======================================================================== + // Static assets are referenced by URL in the browser, not via imports. + // Examples: /logo.png, /icons/*, /embed.js + 'public/**', + + // Build outputs and caches + 'node_modules/**', + '.next/**', + 'coverage/**', + + // Development tools + '**/*.stories.{ts,tsx}', + + // ======================================================================== + // 🔒 Utility scripts (not part of application runtime) + // ======================================================================== + // These scripts are run manually (e.g., pnpm gen-icons, pnpm check-i18n) + // and are not imported by the application code. + 'scripts/**', + 'bin/**', + 'i18n-config/**', + + // Icon generation script (generates components, not used in runtime) + 'app/components/base/icons/script.mjs', + ], + + // ============================================================================ + // Ignored Dependencies + // ============================================================================ + // Dependencies that are used but not directly imported in code. + // These are typically: + // - Build tools + // - Plugins loaded by configuration files + // - CLI tools + ignoreDependencies: [ + // ======================================================================== + // Next.js plugins (loaded by next.config.js) + // ======================================================================== + 'next-pwa', + '@next/bundle-analyzer', + '@next/mdx', + + // ======================================================================== + // Build tools (used by webpack/next.js build process) + // ======================================================================== + 'code-inspector-plugin', + + // ======================================================================== + // Development and translation tools (used by scripts) + // ======================================================================== + 'bing-translate-api', + 'uglify-js', + 'magicast', + ], + + // ============================================================================ + // Export Analysis Configuration + // ============================================================================ + // Configure how exports are analyzed + + // Ignore exports that are only used within the same file + // (e.g., helper functions used internally in the same module) + ignoreExportsUsedInFile: true, + + // ⚠️ SAFETY: Include exports from entry files in the analysis + // This helps find unused public APIs, but be careful with: + // - Context exports (useContext hooks) + // - Store exports (useStore hooks) + // - Type exports (might be used in other files) + includeEntryExports: true, + + // ============================================================================ + // Ignored Binaries + // ============================================================================ + // Binary executables that are used but not listed in package.json + ignoreBinaries: [ + 'only-allow', // Used in preinstall script to enforce pnpm usage + ], + + // ============================================================================ + // Reporting Rules + // ============================================================================ + // Configure what types of issues to report and at what severity level + rules: { + // ======================================================================== + // Unused files are ERRORS + // ======================================================================== + // These should definitely be removed or used. + // However, always manually verify before deleting! + files: 'error', + + // ======================================================================== + // Unused dependencies are WARNINGS + // ======================================================================== + // Dependencies might be: + // - Used in production builds but not in dev + // - Peer dependencies + // - Used by other tools + dependencies: 'warn', + devDependencies: 'warn', + + // ======================================================================== + // Unlisted imports are ERRORS + // ======================================================================== + // Missing from package.json - will break in production! + unlisted: 'error', + + // ======================================================================== + // Unused exports are WARNINGS (not errors!) + // ======================================================================== + // Exports might be: + // - Part of public API for future use + // - Used by external tools + // - Exported for type inference + // ⚠️ ALWAYS manually verify before removing exports! + exports: 'warn', + + // Unused types are warnings (might be part of type definitions) + types: 'warn', + + // Duplicate exports are warnings (could cause confusion but not breaking) + duplicates: 'warn', + }, +} + +export default config diff --git a/web/package.json b/web/package.json index 6ce808291e..562938d5b3 100644 --- a/web/package.json +++ b/web/package.json @@ -2,7 +2,7 @@ "name": "dify-web", "version": "1.9.1", "private": true, - "packageManager": "pnpm@10.18.2", + "packageManager": "pnpm@10.18.3+sha512.bbd16e6d7286fd7e01f6b3c0b3c932cda2965c06a908328f74663f10a9aea51f1129eea615134bf992831b009eabe167ecb7008b597f40ff9bc75946aadfb08d", "engines": { "node": ">=v22.11.0" },