Merge branch 'main' into feat/memory-orchestration-fed

This commit is contained in:
zxhlyh 2025-10-15 16:20:00 +08:00
commit 8a348615bf
108 changed files with 1051 additions and 1017 deletions

View File

@ -1,6 +1,7 @@
#!/bin/bash #!/bin/bash
yq eval '.services.weaviate.ports += ["8080:8080"]' -i docker/docker-compose.yaml 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.qdrant.ports += ["6333:6333"]' -i docker/docker-compose.yaml
yq eval '.services.chroma.ports += ["8000:8000"]' -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 yq eval '.services["milvus-standalone"].ports += ["19530:19530"]' -i docker/docker-compose.yaml

View File

@ -1,5 +1,4 @@
import flask_restx import flask_restx
from flask import Response
from flask_restx import Resource, fields, marshal_with from flask_restx import Resource, fields, marshal_with
from flask_restx._http import HTTPStatus from flask_restx._http import HTTPStatus
from sqlalchemy import select from sqlalchemy import select
@ -156,11 +155,6 @@ class AppApiKeyListResource(BaseApiKeyListResource):
"""Create a new API key for an app""" """Create a new API key for an app"""
return super().post(resource_id) 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_type = "app"
resource_model = App resource_model = App
resource_id_field = "app_id" resource_id_field = "app_id"
@ -177,11 +171,6 @@ class AppApiKeyResource(BaseApiKeyResource):
"""Delete an API key for an app""" """Delete an API key for an app"""
return super().delete(resource_id, api_key_id) 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_type = "app"
resource_model = App resource_model = App
resource_id_field = "app_id" resource_id_field = "app_id"
@ -206,11 +195,6 @@ class DatasetApiKeyListResource(BaseApiKeyListResource):
"""Create a new API key for a dataset""" """Create a new API key for a dataset"""
return super().post(resource_id) 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_type = "dataset"
resource_model = Dataset resource_model = Dataset
resource_id_field = "dataset_id" resource_id_field = "dataset_id"
@ -227,11 +211,6 @@ class DatasetApiKeyResource(BaseApiKeyResource):
"""Delete an API key for a dataset""" """Delete an API key for a dataset"""
return super().delete(resource_id, api_key_id) 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_type = "dataset"
resource_model = Dataset resource_model = Dataset
resource_id_field = "dataset_id" resource_id_field = "dataset_id"

View File

@ -472,6 +472,9 @@ class ProviderConfiguration(BaseModel):
provider_model_credentials_cache.delete() provider_model_credentials_cache.delete()
self.switch_preferred_provider_type(provider_type=ProviderType.CUSTOM, session=session) 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() session.commit()
except Exception: except Exception:

View File

@ -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 datetime
import json import json
import logging
import uuid as _uuid
from typing import Any 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 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 configs import dify_config
from core.rag.datasource.vdb.field import Field 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 extensions.ext_redis import redis_client
from models.dataset import Dataset from models.dataset import Dataset
logger = logging.getLogger(__name__)
class WeaviateConfig(BaseModel): 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 endpoint: str
api_key: str | None = None api_key: str | None = None
batch_size: int = 100 batch_size: int = 100
@model_validator(mode="before") @model_validator(mode="before")
@classmethod @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"]: if not values["endpoint"]:
raise ValueError("config WEAVIATE_ENDPOINT is required") raise ValueError("config WEAVIATE_ENDPOINT is required")
return values return values
class WeaviateVector(BaseVector): 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): 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) super().__init__(collection_name)
self._client = self._init_client(config) self._client = self._init_client(config)
self._attributes = attributes self._attributes = attributes
def _init_client(self, config: WeaviateConfig) -> weaviate.Client: def _init_client(self, config: WeaviateConfig) -> weaviate.WeaviateClient:
auth_config = weaviate.AuthApiKey(api_key=config.api_key or "") """
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: grpc_host = host
client = weaviate.Client( grpc_secure = http_secure
url=config.endpoint, auth_client_secret=auth_config, timeout_config=(5, 60), startup_period=None grpc_port = 443 if grpc_secure else 50051
)
except Exception as exc:
raise ConnectionError("Vector database connection error") from exc
client.batch.configure( client = weaviate.connect_to_custom(
# `batch_size` takes an `int` value to enable auto-batching http_host=host,
# (`None` is used for manual batching) http_port=http_port,
batch_size=config.batch_size, http_secure=http_secure,
# dynamically update the `batch_size` based on import speed grpc_host=grpc_host,
dynamic=True, grpc_port=grpc_port,
# `timeout_retries` takes an `int` value to retry on time outs grpc_secure=grpc_secure,
timeout_retries=3, 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 return client
def get_type(self) -> str: def get_type(self) -> str:
"""Returns the vector database type identifier."""
return VectorType.WEAVIATE return VectorType.WEAVIATE
def get_collection_name(self, dataset: Dataset) -> str: 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: if dataset.index_struct_dict:
class_prefix: str = dataset.index_struct_dict["vector_store"]["class_prefix"] class_prefix: str = dataset.index_struct_dict["vector_store"]["class_prefix"]
if not class_prefix.endswith("_Node"): if not class_prefix.endswith("_Node"):
# original class_prefix
class_prefix += "_Node" class_prefix += "_Node"
return class_prefix return class_prefix
dataset_id = dataset.id dataset_id = dataset.id
return Dataset.gen_collection_name_by_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}} return {"type": self.get_type(), "vector_store": {"class_prefix": self._collection_name}}
def create(self, texts: list[Document], embeddings: list[list[float]], **kwargs): 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() self._create_collection()
# create vector
self.add_texts(texts, embeddings) self.add_texts(texts, embeddings)
def _create_collection(self): 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}" lock_name = f"vector_indexing_lock_{self._collection_name}"
with redis_client.lock(lock_name, timeout=20): with redis_client.lock(lock_name, timeout=20):
collection_exist_cache_key = f"vector_indexing_{self._collection_name}" cache_key = f"vector_indexing_{self._collection_name}"
if redis_client.get(collection_exist_cache_key): if redis_client.get(cache_key):
return return
schema = self._default_schema(self._collection_name)
if not self._client.schema.contains(schema): try:
# create collection if not self._client.collections.exists(self._collection_name):
self._client.schema.create_class(schema) self._client.collections.create(
redis_client.set(collection_exist_cache_key, 1, ex=3600) 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): 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) uuids = self._get_uuids(documents)
texts = [d.page_content for d in documents] texts = [d.page_content for d in documents]
metadatas = [d.metadata 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):
for i, text in enumerate(texts): props: dict[str, Any] = {Field.TEXT_KEY.value: text}
data_properties = {Field.TEXT_KEY: text} meta = metadatas[i] or {}
if metadatas is not None: for k, v in meta.items():
# metadata maybe None props[k] = self._json_serializable(v)
for key, val in (metadatas[i] or {}).items():
data_properties[key] = self._json_serializable(val)
batch.add_data_object( candidate = uuids[i] if uuids else None
data_object=data_properties, uid = candidate if (candidate and self._is_uuid(candidate)) else str(_uuid.uuid4())
class_name=self._collection_name, ids_out.append(uid)
uuid=uuids[i],
vector=embeddings[i] if embeddings else None, 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): batch_size = max(1, int(dify_config.WEAVIATE_BATCH_SIZE or 100))
# check whether the index already exists with col.batch.dynamic() as batch:
schema = self._default_schema(self._collection_name) for obj in objs:
if self._client.schema.contains(schema): batch.add_object(properties=obj.properties, uuid=obj.uuid, vector=obj.vector)
where_filter = {"operator": "Equal", "path": [key], "valueText": value}
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): def delete(self):
# check whether the index already exists """Deletes the entire collection from Weaviate."""
schema = self._default_schema(self._collection_name) if self._client.collections.exists(self._collection_name):
if self._client.schema.contains(schema): self._client.collections.delete(self._collection_name)
self._client.schema.delete_class(self._collection_name)
def text_exists(self, id: str) -> bool: def text_exists(self, id: str) -> bool:
collection_name = self._collection_name """Checks if a document with the given doc_id exists in the collection."""
schema = self._default_schema(self._collection_name) if not self._client.collections.exists(self._collection_name):
# check whether the index already exists
if not self._client.schema.contains(schema):
return False return False
result = (
self._client.query.get(collection_name) col = self._client.collections.use(self._collection_name)
.with_additional(["id"]) res = col.query.fetch_objects(
.with_where( filters=Filter.by_property("doc_id").equal(id),
{ limit=1,
"path": ["doc_id"], return_properties=["doc_id"],
"operator": "Equal",
"valueText": id,
}
)
.with_limit(1)
.do()
) )
if "errors" in result: return len(res.objects) > 0
raise ValueError(f"Error during query: {result['errors']}")
entries = result["data"]["Get"][collection_name] def delete_by_ids(self, ids: list[str]) -> None:
if len(entries) == 0: """
return False 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]): col = self._client.collections.use(self._collection_name)
# check whether the index already exists
schema = self._default_schema(self._collection_name) for uid in ids:
if self._client.schema.contains(schema): try:
for uuid in ids: col.data.delete_by_id(uid)
try: except UnexpectedStatusCodeError as e:
self._client.data_object.delete( if getattr(e, "status_code", None) != 404:
class_name=self._collection_name, raise
uuid=uuid,
)
except weaviate.UnexpectedStatusCodeException as e:
# tolerate not found error
if e.status_code != 404:
raise e
def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]: 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 Performs vector similarity search using the provided query vector.
properties = self._attributes
properties.append(Field.TEXT_KEY)
query_obj = self._client.query.get(collection_name, properties)
vector = {"vector": query_vector} Filters by document IDs if provided and applies score threshold.
document_ids_filter = kwargs.get("document_ids_filter") Returns documents sorted by relevance score.
if document_ids_filter: """
operands = [] if not self._client.collections.exists(self._collection_name):
for document_id_filter in document_ids_filter: return []
operands.append({"path": ["document_id"], "operator": "Equal", "valueText": document_id_filter})
where_filter = {"operator": "Or", "operands": operands} col = self._client.collections.use(self._collection_name)
query_obj = query_obj.with_where(where_filter) props = list({*self._attributes, "document_id", Field.TEXT_KEY.value})
result = (
query_obj.with_near_vector(vector) where = None
.with_limit(kwargs.get("top_k", 4)) doc_ids = kwargs.get("document_ids_filter") or []
.with_additional(["vector", "distance"]) if doc_ids:
.do() 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 = [] docs: list[Document] = []
for res in result["data"]["Get"][collection_name]: for obj in res.objects:
text = res.pop(Field.TEXT_KEY) properties = dict(obj.properties or {})
score = 1 - res["_additional"]["distance"] text = properties.pop(Field.TEXT_KEY.value, "")
docs_and_scores.append((Document(page_content=text, metadata=res), score)) distance = (obj.metadata.distance if obj.metadata else None) or 1.0
score = 1.0 - distance
docs = [] if score > score_threshold:
for doc, score in docs_and_scores: properties["score"] = score
score_threshold = float(kwargs.get("score_threshold") or 0.0) docs.append(Document(page_content=text, metadata=properties))
# check score threshold
if score >= score_threshold: docs.sort(key=lambda d: d.metadata.get("score", 0.0), reverse=True)
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)
return docs return docs
def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]: 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 Performs BM25 full-text search on document content.
content: dict[str, Any] = {"concepts": [query]}
properties = self._attributes Filters by document IDs if provided and returns matching documents with vectors.
properties.append(Field.TEXT_KEY) """
if kwargs.get("search_distance"): if not self._client.collections.exists(self._collection_name):
content["certainty"] = kwargs.get("search_distance") return []
query_obj = self._client.query.get(collection_name, properties)
document_ids_filter = kwargs.get("document_ids_filter") col = self._client.collections.use(self._collection_name)
if document_ids_filter: props = list({*self._attributes, Field.TEXT_KEY.value})
operands = []
for document_id_filter in document_ids_filter: where = None
operands.append({"path": ["document_id"], "operator": "Equal", "valueText": document_id_filter}) doc_ids = kwargs.get("document_ids_filter") or []
where_filter = {"operator": "Or", "operands": operands} if doc_ids:
query_obj = query_obj.with_where(where_filter) ors = [Filter.by_property("document_id").equal(x) for x in doc_ids]
query_obj = query_obj.with_additional(["vector"]) where = ors[0]
properties = ["text"] for f in ors[1:]:
result = query_obj.with_bm25(query=query, properties=properties).with_limit(kwargs.get("top_k", 4)).do() where = where | f
if "errors" in result:
raise ValueError(f"Error during query: {result['errors']}") top_k = int(kwargs.get("top_k", 4))
docs = []
for res in result["data"]["Get"][collection_name]: res = col.query.bm25(
text = res.pop(Field.TEXT_KEY) query=query,
additional = res.pop("_additional") query_properties=[Field.TEXT_KEY.value],
docs.append(Document(page_content=text, vector=additional["vector"], metadata=res)) 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 return docs
def _default_schema(self, index_name: str): def _json_serializable(self, value: Any) -> Any:
return { """Converts values to JSON-serializable format, handling datetime objects."""
"class": index_name,
"properties": [
{
"name": "text",
"dataType": ["text"],
}
],
}
def _json_serializable(self, value: Any):
if isinstance(value, datetime.datetime): if isinstance(value, datetime.datetime):
return value.isoformat() return value.isoformat()
return value return value
class WeaviateVectorFactory(AbstractVectorFactory): class WeaviateVectorFactory(AbstractVectorFactory):
"""Factory class for creating WeaviateVector instances."""
def init_vector(self, dataset: Dataset, attributes: list, embeddings: Embeddings) -> WeaviateVector: 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: if dataset.index_struct_dict:
class_prefix: str = dataset.index_struct_dict["vector_store"]["class_prefix"] class_prefix: str = dataset.index_struct_dict["vector_store"]["class_prefix"]
collection_name = class_prefix collection_name = class_prefix
@ -281,7 +425,6 @@ class WeaviateVectorFactory(AbstractVectorFactory):
dataset_id = dataset.id dataset_id = dataset.id
collection_name = Dataset.gen_collection_name_by_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)) dataset.index_struct = json.dumps(self.gen_index_struct_dict(VectorType.WEAVIATE, collection_name))
return WeaviateVector( return WeaviateVector(
collection_name=collection_name, collection_name=collection_name,
config=WeaviateConfig( config=WeaviateConfig(

View File

@ -25,7 +25,7 @@ class FirecrawlApp:
} }
if params: if params:
json_data.update(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: if response.status_code == 200:
response_data = response.json() response_data = response.json()
data = response_data["data"] data = response_data["data"]
@ -42,7 +42,7 @@ class FirecrawlApp:
json_data = {"url": url} json_data = {"url": url}
if params: if params:
json_data.update(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: if response.status_code == 200:
# There's also another two fields in the response: "success" (bool) and "url" (str) # There's also another two fields in the response: "success" (bool) and "url" (str)
job_id = response.json().get("id") job_id = response.json().get("id")
@ -51,9 +51,25 @@ class FirecrawlApp:
self._handle_error(response, "start crawl job") self._handle_error(response, "start crawl job")
return "" # unreachable 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]: def check_crawl_status(self, job_id) -> dict[str, Any]:
headers = self._prepare_headers() 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: if response.status_code == 200:
crawl_status_response = response.json() crawl_status_response = response.json()
if crawl_status_response.get("status") == "completed": if crawl_status_response.get("status") == "completed":
@ -135,12 +151,16 @@ class FirecrawlApp:
"lang": "en", "lang": "en",
"country": "us", "country": "us",
"timeout": 60000, "timeout": 60000,
"ignoreInvalidURLs": False, "ignoreInvalidURLs": True,
"scrapeOptions": {}, "scrapeOptions": {},
"sources": [
{"type": "web"},
],
"integration": "dify",
} }
if params: if params:
json_data.update(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: if response.status_code == 200:
response_data = response.json() response_data = response.json()
if not response_data.get("success"): if not response_data.get("success"):

View File

@ -189,6 +189,11 @@ class ToolInvokeMessage(BaseModel):
data: Mapping[str, Any] = Field(..., description="Detailed log data") data: Mapping[str, Any] = Field(..., description="Detailed log data")
metadata: Mapping[str, Any] = Field(default_factory=dict, description="The metadata of the log") 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): class RetrieverResourceMessage(BaseModel):
retriever_resources: list[RetrievalSourceMetadata] = Field(..., description="retriever resources") retriever_resources: list[RetrievalSourceMetadata] = Field(..., description="retriever resources")
context: str = Field(..., description="context") context: str = Field(..., description="context")
@ -376,6 +381,11 @@ class ToolEntity(BaseModel):
def set_parameters(cls, v, validation_info: ValidationInfo) -> list[ToolParameter]: def set_parameters(cls, v, validation_info: ValidationInfo) -> list[ToolParameter]:
return v or [] 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): class OAuthSchema(BaseModel):
client_schema: list[ProviderConfig] = Field( client_schema: list[ProviderConfig] = Field(

View File

@ -7,6 +7,7 @@ from collections.abc import Mapping
from functools import singledispatchmethod from functools import singledispatchmethod
from typing import TYPE_CHECKING, final from typing import TYPE_CHECKING, final
from core.model_runtime.entities.llm_entities import LLMUsage
from core.workflow.entities import GraphRuntimeState from core.workflow.entities import GraphRuntimeState
from core.workflow.enums import ErrorStrategy, NodeExecutionType from core.workflow.enums import ErrorStrategy, NodeExecutionType
from core.workflow.graph import Graph 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) node_execution = self._graph_execution.get_or_create_node_execution(event.node_id)
is_initial_attempt = node_execution.retry_count == 0 is_initial_attempt = node_execution.retry_count == 0
node_execution.mark_started(event.id) node_execution.mark_started(event.id)
self._graph_runtime_state.increment_node_run_steps()
# Track in response coordinator for stream ordering # Track in response coordinator for stream ordering
self._response_coordinator.track_node_execution(event.node_id, event.id) 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 = self._graph_execution.get_or_create_node_execution(event.node_id)
node_execution.mark_taken() node_execution.mark_taken()
self._accumulate_node_usage(event.node_run_result.llm_usage)
# Store outputs in variable pool # Store outputs in variable pool
self._store_node_outputs(event.node_id, event.node_run_result.outputs) self._store_node_outputs(event.node_id, event.node_run_result.outputs)
@ -212,6 +216,8 @@ class EventHandler:
node_execution.mark_failed(event.error) node_execution.mark_failed(event.error)
self._graph_execution.record_node_failure() self._graph_execution.record_node_failure()
self._accumulate_node_usage(event.node_run_result.llm_usage)
result = self._error_handler.handle_node_failure(event) result = self._error_handler.handle_node_failure(event)
if result: if result:
@ -235,6 +241,8 @@ class EventHandler:
node_execution = self._graph_execution.get_or_create_node_execution(event.node_id) node_execution = self._graph_execution.get_or_create_node_execution(event.node_id)
node_execution.mark_taken() 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) # Persist outputs produced by the exception strategy (e.g. default values)
self._store_node_outputs(event.node_id, event.node_run_result.outputs) 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.enqueue_node(event.node_id)
self._state_manager.start_execution(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: def _store_node_outputs(self, node_id: str, outputs: Mapping[str, object]) -> None:
""" """
Store node outputs in the variable pool. Store node outputs in the variable pool.

View File

@ -214,7 +214,7 @@ vdb = [
"tidb-vector==0.0.9", "tidb-vector==0.0.9",
"upstash-vector==0.6.0", "upstash-vector==0.6.0",
"volcengine-compat~=1.0.0", "volcengine-compat~=1.0.0",
"weaviate-client~=3.24.0", "weaviate-client>=4.0.0,<5.0.0",
"xinference-client~=1.2.2", "xinference-client~=1.2.2",
"mo-vector~=0.1.13", "mo-vector~=0.1.13",
"mysql-connector-python>=9.3.0", "mysql-connector-python>=9.3.0",

View File

@ -1,8 +1,11 @@
import datetime import datetime
import logging import logging
import time import time
from collections.abc import Sequence
import click import click
from sqlalchemy import select
from sqlalchemy.orm import Session, sessionmaker
import app import app
from configs import dify_config from configs import dify_config
@ -35,50 +38,53 @@ def clean_workflow_runlogs_precise():
retention_days = dify_config.WORKFLOW_LOG_RETENTION_DAYS retention_days = dify_config.WORKFLOW_LOG_RETENTION_DAYS
cutoff_date = datetime.datetime.now() - datetime.timedelta(days=retention_days) cutoff_date = datetime.datetime.now() - datetime.timedelta(days=retention_days)
session_factory = sessionmaker(db.engine, expire_on_commit=False)
try: try:
total_workflow_runs = db.session.query(WorkflowRun).where(WorkflowRun.created_at < cutoff_date).count() with session_factory.begin() as session:
if total_workflow_runs == 0: total_workflow_runs = session.query(WorkflowRun).where(WorkflowRun.created_at < cutoff_date).count()
logger.info("No expired workflow run logs found") if total_workflow_runs == 0:
return logger.info("No expired workflow run logs found")
logger.info("Found %s expired workflow run logs to clean", total_workflow_runs) return
logger.info("Found %s expired workflow run logs to clean", total_workflow_runs)
total_deleted = 0 total_deleted = 0
failed_batches = 0 failed_batches = 0
batch_count = 0 batch_count = 0
while True: while True:
workflow_runs = ( with session_factory.begin() as session:
db.session.query(WorkflowRun.id).where(WorkflowRun.created_at < cutoff_date).limit(BATCH_SIZE).all() 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: if not workflow_run_ids:
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)
break 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: else:
# Calculate incremental delay times: 5, 10, 15 minutes failed_batches += 1
retry_delay_minutes = failed_batches * 5 if failed_batches >= MAX_RETRIES:
logger.warning("Batch deletion failed, retrying in %s minutes...", retry_delay_minutes) logger.error("Failed to delete batch after %s retries, aborting cleanup for today", MAX_RETRIES)
time.sleep(retry_delay_minutes * 60) break
continue 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) logger.info("Cleanup completed: %s expired workflow run logs deleted", total_deleted)
except Exception: except Exception:
db.session.rollback()
logger.exception("Unexpected error in workflow log cleanup") logger.exception("Unexpected error in workflow log cleanup")
raise 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")) 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: def _delete_batch(session: Session, workflow_run_ids: Sequence[str], attempt_count: int) -> bool:
"""Delete a single batch with a retry mechanism and complete cascading deletion""" """Delete a single batch of workflow runs and all related data within a nested transaction."""
try: try:
with db.session.begin_nested(): with session.begin_nested():
message_data = ( 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)) .where(Message.workflow_run_id.in_(workflow_run_ids))
.all() .all()
) )
message_id_list = [msg.id for msg in message_data] 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}) conversation_id_list = list({msg.conversation_id for msg in message_data if msg.conversation_id})
if message_id_list: if message_id_list:
db.session.query(AppAnnotationHitHistory).where( message_related_models = [
AppAnnotationHitHistory.message_id.in_(message_id_list) AppAnnotationHitHistory,
).delete(synchronize_session=False) 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 synchronize_session=False
) )
db.session.query(MessageChain).where(MessageChain.message_id.in_(message_id_list)).delete( session.query(WorkflowAppLog).where(WorkflowAppLog.workflow_run_id.in_(workflow_run_ids)).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(
synchronize_session=False synchronize_session=False
) )
db.session.query(WorkflowNodeExecutionModel).where( session.query(WorkflowNodeExecutionModel).where(
WorkflowNodeExecutionModel.workflow_run_id.in_(workflow_run_ids) WorkflowNodeExecutionModel.workflow_run_id.in_(workflow_run_ids)
).delete(synchronize_session=False) ).delete(synchronize_session=False)
if conversation_id_list: if conversation_id_list:
db.session.query(ConversationVariable).where( session.query(ConversationVariable).where(
ConversationVariable.conversation_id.in_(conversation_id_list) ConversationVariable.conversation_id.in_(conversation_id_list)
).delete(synchronize_session=False) ).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 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: except Exception:
db.session.rollback()
logger.exception("Batch deletion failed (attempt %s)", attempt_count + 1) logger.exception("Batch deletion failed (attempt %s)", attempt_count + 1)
return False return False

View File

@ -23,6 +23,7 @@ class CrawlOptions:
only_main_content: bool = False only_main_content: bool = False
includes: str | None = None includes: str | None = None
excludes: str | None = None excludes: str | None = None
prompt: str | None = None
max_depth: int | None = None max_depth: int | None = None
use_sitemap: bool = True use_sitemap: bool = True
@ -70,6 +71,7 @@ class WebsiteCrawlApiRequest:
only_main_content=self.options.get("only_main_content", False), only_main_content=self.options.get("only_main_content", False),
includes=self.options.get("includes"), includes=self.options.get("includes"),
excludes=self.options.get("excludes"), excludes=self.options.get("excludes"),
prompt=self.options.get("prompt"),
max_depth=self.options.get("max_depth"), max_depth=self.options.get("max_depth"),
use_sitemap=self.options.get("use_sitemap", True), 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]: 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")) firecrawl_app = FirecrawlApp(api_key=api_key, base_url=config.get("base_url"))
params: dict[str, Any]
if not request.options.crawl_sub_pages: if not request.options.crawl_sub_pages:
params = { params = {
"includePaths": [], "includePaths": [],
@ -188,8 +191,10 @@ class WebsiteService:
"limit": request.options.limit, "limit": request.options.limit,
"scrapeOptions": {"onlyMainContent": request.options.only_main_content}, "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) job_id = firecrawl_app.crawl_url(request.url, params)
website_crawl_time_cache_key = f"website_crawl_{job_id}" website_crawl_time_cache_key = f"website_crawl_{job_id}"

View File

@ -86,12 +86,16 @@ class WorkflowAppService:
), ),
) )
if created_by_account: 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( stmt = stmt.join(
Account, Account,
and_( and_(
WorkflowAppLog.created_by == Account.id, WorkflowAppLog.created_by == Account.id,
WorkflowAppLog.created_by_role == CreatorUserRole.ACCOUNT, WorkflowAppLog.created_by_role == CreatorUserRole.ACCOUNT,
Account.email == created_by_account, Account.id == account.id,
), ),
) )

View File

@ -75,10 +75,7 @@
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<div class="header"> <div class="header"></div>
<!-- Optional: Add a logo or a header image here -->
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo">
</div>
<div class="content"> <div class="content">
<p class="content1">Dear {{ to }},</p> <p class="content1">Dear {{ to }},</p>
<p class="content2">{{ 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.</p> <p class="content2">{{ 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.</p>

View File

@ -789,6 +789,31 @@ class TestWorkflowAppService:
assert result_account_filter["total"] == 3 assert result_account_filter["total"] == 3
assert all(log.created_by_role == CreatorUserRole.ACCOUNT for log in result_account_filter["data"]) 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 # Test filtering by non-existent session ID
result_no_session = service.get_paginate_workflow_app_logs( result_no_session = service.get_paginate_workflow_app_logs(
session=db_session_with_containers, session=db_session_with_containers,
@ -799,15 +824,16 @@ class TestWorkflowAppService:
) )
assert result_no_session["total"] == 0 assert result_no_session["total"] == 0
# Test filtering by non-existent account email # Test filtering by non-existent account email, is unexpected input, should raise ValueError
result_no_account = service.get_paginate_workflow_app_logs( with pytest.raises(ValueError) as exc_info:
session=db_session_with_containers, service.get_paginate_workflow_app_logs(
app_model=app, session=db_session_with_containers,
created_by_account="nonexistent@example.com", app_model=app,
page=1, created_by_account="nonexistent@example.com",
limit=20, page=1,
) limit=20,
assert result_no_account["total"] == 0 )
assert "Account not found" in str(exc_info.value)
def test_get_paginate_workflow_app_logs_with_uuid_keyword_search( def test_get_paginate_workflow_app_logs_with_uuid_keyword_search(
self, db_session_with_containers, mock_external_service_dependencies self, db_session_with_containers, mock_external_service_dependencies
@ -1057,15 +1083,15 @@ class TestWorkflowAppService:
assert len(result_no_session["data"]) == 0 assert len(result_no_session["data"]) == 0
# Test with account email that doesn't exist # Test with account email that doesn't exist
result_no_account = service.get_paginate_workflow_app_logs( with pytest.raises(ValueError) as exc_info:
session=db_session_with_containers, service.get_paginate_workflow_app_logs(
app_model=app, session=db_session_with_containers,
created_by_account="nonexistent@example.com", app_model=app,
page=1, created_by_account="nonexistent@example.com",
limit=20, page=1,
) limit=20,
assert result_no_account["total"] == 0 )
assert len(result_no_account["data"]) == 0 assert "Account not found" in str(exc_info.value)
def test_get_paginate_workflow_app_logs_with_complex_query_combinations( def test_get_paginate_workflow_app_logs_with_complex_query_combinations(
self, db_session_with_containers, mock_external_service_dependencies self, db_session_with_containers, mock_external_service_dependencies

View File

@ -110,19 +110,6 @@ class TestAlibabaCloudMySQLVector(unittest.TestCase):
assert mock_cursor.execute.call_count >= 3 # CREATE TABLE + 2 indexes assert mock_cursor.execute.call_count >= 3 # CREATE TABLE + 2 indexes
mock_redis.set.assert_called_once() 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( @patch(
"core.rag.datasource.vdb.alibabacloud_mysql.alibabacloud_mysql_vector.mysql.connector.pooling.MySQLConnectionPool" "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}] 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__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -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 == {}

26
api/uv.lock generated
View File

@ -587,16 +587,16 @@ wheels = [
[[package]] [[package]]
name = "boto3-stubs" name = "boto3-stubs"
version = "1.40.50" version = "1.40.51"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "botocore-stubs" }, { name = "botocore-stubs" },
{ name = "types-s3transfer" }, { name = "types-s3transfer" },
{ name = "typing-extensions", marker = "python_full_version < '3.12'" }, { 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 = [ 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] [package.optional-dependencies]
@ -620,14 +620,14 @@ wheels = [
[[package]] [[package]]
name = "botocore-stubs" name = "botocore-stubs"
version = "1.40.50" version = "1.40.51"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "types-awscrt" }, { 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 = [ 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]] [[package]]
@ -1667,7 +1667,7 @@ vdb = [
{ name = "tidb-vector", specifier = "==0.0.9" }, { name = "tidb-vector", specifier = "==0.0.9" },
{ name = "upstash-vector", specifier = "==0.6.0" }, { name = "upstash-vector", specifier = "==0.6.0" },
{ name = "volcengine-compat", specifier = "~=1.0.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" }, { name = "xinference-client", specifier = "~=1.2.2" },
] ]
@ -6901,16 +6901,20 @@ wheels = [
[[package]] [[package]]
name = "weaviate-client" name = "weaviate-client"
version = "3.24.2" version = "4.17.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "authlib" }, { name = "authlib" },
{ name = "requests" }, { name = "deprecation" },
{ name = "grpcio" },
{ name = "httpx" },
{ name = "protobuf" },
{ name = "pydantic" },
{ name = "validators" }, { 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 = [ 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]] [[package]]

View File

@ -329,7 +329,7 @@ services:
# The Weaviate vector store. # The Weaviate vector store.
weaviate: weaviate:
image: semitechnologies/weaviate:1.19.0 image: semitechnologies/weaviate:1.27.0
profiles: profiles:
- "" - ""
- weaviate - weaviate

View File

@ -181,7 +181,7 @@ services:
# The Weaviate vector store. # The Weaviate vector store.
weaviate: weaviate:
image: semitechnologies/weaviate:1.19.0 image: semitechnologies/weaviate:1.27.0
profiles: profiles:
- "" - ""
- weaviate - weaviate
@ -206,6 +206,7 @@ services:
AUTHORIZATION_ADMINLIST_USERS: ${WEAVIATE_AUTHORIZATION_ADMINLIST_USERS:-hello@dify.ai} AUTHORIZATION_ADMINLIST_USERS: ${WEAVIATE_AUTHORIZATION_ADMINLIST_USERS:-hello@dify.ai}
ports: ports:
- "${EXPOSE_WEAVIATE_PORT:-8080}:8080" - "${EXPOSE_WEAVIATE_PORT:-8080}:8080"
- "${EXPOSE_WEAVIATE_GRPC_PORT:-50051}:50051"
networks: networks:
# create a network between sandbox, api and ssrf_proxy, and can not access outside. # create a network between sandbox, api and ssrf_proxy, and can not access outside.

View File

@ -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
"

View File

@ -936,7 +936,7 @@ services:
# The Weaviate vector store. # The Weaviate vector store.
weaviate: weaviate:
image: semitechnologies/weaviate:1.19.0 image: semitechnologies/weaviate:1.27.0
profiles: profiles:
- "" - ""
- weaviate - weaviate

View File

@ -1,10 +1,11 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useState } from 'react' import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import produce from 'immer' import produce from 'immer'
import { ReactSortable } from 'react-sortablejs'
import Panel from '../base/feature-panel' import Panel from '../base/feature-panel'
import EditModal from './config-modal' import EditModal from './config-modal'
import VarItem from './var-item' import VarItem from './var-item'
@ -22,6 +23,7 @@ import { useModalContext } from '@/context/modal-context'
import { useEventEmitterContextContext } from '@/context/event-emitter' import { useEventEmitterContextContext } from '@/context/event-emitter'
import type { InputVar } from '@/app/components/workflow/types' import type { InputVar } from '@/app/components/workflow/types'
import { InputVarType } 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' export const ADD_EXTERNAL_DATA_TOOL = 'ADD_EXTERNAL_DATA_TOOL'
@ -218,6 +220,16 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
showEditModal() showEditModal()
} }
const promptVariablesWithIds = useMemo(() => promptVariables.map((item) => {
return {
id: item.key,
variable: { ...item },
}
}), [promptVariables])
const canDrag = !readonly && promptVariables.length > 1
return ( return (
<Panel <Panel
className="mt-2" className="mt-2"
@ -245,18 +257,32 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
)} )}
{hasVar && ( {hasVar && (
<div className='mt-1 px-3 pb-3'> <div className='mt-1 px-3 pb-3'>
{promptVariables.map(({ key, name, type, required, config, icon, icon_background }, index) => ( <ReactSortable
<VarItem className='space-y-1'
key={index} list={promptVariablesWithIds}
readonly={readonly} setList={(list) => { onPromptVariablesChange?.(list.map(item => item.variable)) }}
name={key} handle='.handle'
label={name} ghostClass='opacity-50'
required={!!required} animation={150}
type={type} >
onEdit={() => handleConfig({ type, key, index, name, config, icon, icon_background })} {promptVariablesWithIds.map((item, index) => {
onRemove={() => handleRemoveVar(index)} const { key, name, type, required, config, icon, icon_background } = item.variable
/> return (
))} <VarItem
className={cn(canDrag && 'handle')}
key={key}
readonly={readonly}
name={key}
label={name}
required={!!required}
type={type}
onEdit={() => handleConfig({ type, key, index, name, config, icon, icon_background })}
onRemove={() => handleRemoveVar(index)}
canDrag={canDrag}
/>
)
})}
</ReactSortable>
</div> </div>
)} )}

View File

@ -3,6 +3,7 @@ import type { FC } from 'react'
import React, { useState } from 'react' import React, { useState } from 'react'
import { import {
RiDeleteBinLine, RiDeleteBinLine,
RiDraggable,
RiEditLine, RiEditLine,
} from '@remixicon/react' } from '@remixicon/react'
import type { IInputTypeIconProps } from './input-type-icon' import type { IInputTypeIconProps } from './input-type-icon'
@ -12,6 +13,7 @@ import Badge from '@/app/components/base/badge'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
type ItemProps = { type ItemProps = {
className?: string
readonly?: boolean readonly?: boolean
name: string name: string
label: string label: string
@ -19,9 +21,11 @@ type ItemProps = {
type: string type: string
onEdit: () => void onEdit: () => void
onRemove: () => void onRemove: () => void
canDrag?: boolean
} }
const VarItem: FC<ItemProps> = ({ const VarItem: FC<ItemProps> = ({
className,
readonly, readonly,
name, name,
label, label,
@ -29,12 +33,16 @@ const VarItem: FC<ItemProps> = ({
type, type,
onEdit, onEdit,
onRemove, onRemove,
canDrag,
}) => { }) => {
const [isDeleting, setIsDeleting] = useState(false) const [isDeleting, setIsDeleting] = useState(false)
return ( return (
<div className={cn('group relative mb-1 flex h-[34px] w-full items-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg pl-2.5 pr-3 shadow-xs last-of-type:mb-0 hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm', isDeleting && 'border-state-destructive-border hover:bg-state-destructive-hover', readonly && 'cursor-not-allowed opacity-30')}> <div className={cn('group relative mb-1 flex h-[34px] w-full items-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg pl-2.5 pr-3 shadow-xs last-of-type:mb-0 hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm', isDeleting && 'border-state-destructive-border hover:bg-state-destructive-hover', readonly && 'cursor-not-allowed opacity-30', className)}>
<VarIcon className='mr-1 h-4 w-4 shrink-0 text-text-accent' /> <VarIcon className={cn('mr-1 h-4 w-4 shrink-0 text-text-accent', canDrag && 'group-hover:opacity-0')} />
{canDrag && (
<RiDraggable className='absolute left-3 top-3 hidden h-3 w-3 cursor-pointer text-text-tertiary group-hover:block' />
)}
<div className='flex w-0 grow items-center'> <div className='flex w-0 grow items-center'>
<div className='truncate' title={`${name} · ${label}`}> <div className='truncate' title={`${name} · ${label}`}>
<span className='system-sm-medium text-text-secondary'>{name}</span> <span className='system-sm-medium text-text-secondary'>{name}</span>

View File

@ -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'

View File

@ -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;
}

View File

@ -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<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> & {
ref?: React.RefObject<HTMLSpanElement>;
},
) => <span className={cn(s.wrapper, className)} {...restProps} ref={ref} />
Icon.displayName = 'BaichuanTextCn'
export default Icon

View File

@ -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;
}

View File

@ -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<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> & {
ref?: React.RefObject<HTMLSpanElement>;
},
) => <span className={cn(s.wrapper, className)} {...restProps} ref={ref} />
Icon.displayName = 'Minimax'
export default Icon

View File

@ -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;
}

View File

@ -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<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> & {
ref?: React.RefObject<HTMLSpanElement>;
},
) => <span className={cn(s.wrapper, className)} {...restProps} ref={ref} />
Icon.displayName = 'MinimaxText'
export default Icon

View File

@ -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;
}

View File

@ -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<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> & {
ref?: React.RefObject<HTMLSpanElement>;
},
) => <span className={cn(s.wrapper, className)} {...restProps} ref={ref} />
Icon.displayName = 'Tongyi'
export default Icon

View File

@ -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;
}

View File

@ -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<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> & {
ref?: React.RefObject<HTMLSpanElement>;
},
) => <span className={cn(s.wrapper, className)} {...restProps} ref={ref} />
Icon.displayName = 'TongyiText'
export default Icon

View File

@ -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;
}

View File

@ -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<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> & {
ref?: React.RefObject<HTMLSpanElement>;
},
) => <span className={cn(s.wrapper, className)} {...restProps} ref={ref} />
Icon.displayName = 'TongyiTextCn'
export default Icon

View File

@ -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;
}

View File

@ -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<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> & {
ref?: React.RefObject<HTMLSpanElement>;
},
) => <span className={cn(s.wrapper, className)} {...restProps} ref={ref} />
Icon.displayName = 'Wxyy'
export default Icon

View File

@ -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;
}

View File

@ -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<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> & {
ref?: React.RefObject<HTMLSpanElement>;
},
) => <span className={cn(s.wrapper, className)} {...restProps} ref={ref} />
Icon.displayName = 'WxyyText'
export default Icon

View File

@ -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;
}

View File

@ -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<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> & {
ref?: React.RefObject<HTMLSpanElement>;
},
) => <span className={cn(s.wrapper, className)} {...restProps} ref={ref} />
Icon.displayName = 'WxyyTextCn'
export default Icon

View File

@ -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'

View File

@ -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<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'Checked'
export default Icon

View File

@ -1 +0,0 @@
export { default as Checked } from './Checked'

View File

@ -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<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'Google'
export default Icon

View File

@ -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<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'WebReader'
export default Icon

View File

@ -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<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'Wikipedia'
export default Icon

View File

@ -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'

View File

@ -18,3 +18,4 @@ const Icon = (
Icon.displayName = 'DataSet' Icon.displayName = 'DataSet'
export default Icon export default Icon

View File

@ -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<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'Loading'
export default Icon

View File

@ -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<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'Search'
export default Icon

View File

@ -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<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'ThoughtList'
export default Icon

View File

@ -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<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'WebReader'
export default Icon

View File

@ -1,5 +1,2 @@
export { default as DataSet } from './DataSet' 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'

View File

@ -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<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'AlignLeft01'
export default Icon

View File

@ -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<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'AlignRight01'
export default Icon

View File

@ -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<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'Grid01'
export default Icon

View File

@ -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' export { default as LayoutGrid02 } from './LayoutGrid02'

View File

@ -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<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'Route'
export default Icon

View File

@ -1 +0,0 @@
export { default as Route } from './Route'

View File

@ -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<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'User01'
export default Icon

View File

@ -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<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'Users01'
export default Icon

View File

@ -1,2 +0,0 @@
export { default as User01 } from './User01'
export { default as Users01 } from './Users01'

View File

@ -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<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'Stars02'
export default Icon

View File

@ -1 +0,0 @@
export { default as Stars02 } from './Stars02'

View File

@ -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<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'ChevronDown'
export default Icon

View File

@ -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<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'HighPriority'
export default Icon

View File

@ -1,2 +0,0 @@
export { default as ChevronDown } from './ChevronDown'
export { default as HighPriority } from './HighPriority'

View File

@ -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<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'Grid01'
export default Icon

View File

@ -1 +0,0 @@
export { default as Grid01 } from './Grid01'

View File

@ -767,8 +767,8 @@ The text generation application offers non-session support and is ideal for tran
<Col> <Col>
<CodeGroup <CodeGroup
title="Request" title="Request"
tag="POST" tag="GET"
label="/meta" label="/site"
targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\ targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\
-H 'Authorization: Bearer {api_key}'`} -H 'Authorization: Bearer {api_key}'`}
/> />

View File

@ -764,8 +764,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<Col> <Col>
<CodeGroup <CodeGroup
title="Request" title="Request"
tag="POST" tag="GET"
label="/meta" label="/site"
targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\ targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\
-H 'Authorization: Bearer {api_key}'`} -H 'Authorization: Bearer {api_key}'`}
/> />

View File

@ -728,8 +728,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
<Col> <Col>
<CodeGroup <CodeGroup
title="Request" title="Request"
tag="POST" tag="GET"
label="/meta" label="/site"
targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\ targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\
-H 'Authorization: Bearer {api_key}'`} -H 'Authorization: Bearer {api_key}'`}
/> />

View File

@ -199,7 +199,7 @@ Chat applications support session persistence, allowing previous chat history to
--header 'Authorization: Bearer {api_key}' \\ --header 'Authorization: Bearer {api_key}' \\
--header 'Content-Type: application/json' \\ --header 'Content-Type: application/json' \\
--data-raw '{ --data-raw '{
"inputs": ${JSON.stringify(props.inputs)}, "inputs": ${JSON.stringify(props.inputs)},
"query": "What are the specs of the iPhone 13 Pro Max?", "query": "What are the specs of the iPhone 13 Pro Max?",
"response_mode": "streaming", "response_mode": "streaming",
"conversation_id": "", "conversation_id": "",
@ -1182,7 +1182,7 @@ Chat applications support session persistence, allowing previous chat history to
--header 'Content-Type: application/json' \\ --header 'Content-Type: application/json' \\
--data-raw '{ --data-raw '{
"value": "Updated Value", "value": "Updated Value",
"user": "abc-123" "user": "abc-123"
}'`} }'`}
/> />
@ -1599,8 +1599,8 @@ Chat applications support session persistence, allowing previous chat history to
<Col> <Col>
<CodeGroup <CodeGroup
title="Request" title="Request"
tag="POST" tag="GET"
label="/meta" label="/site"
targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\ targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\
-H 'Authorization: Bearer {api_key}'`} -H 'Authorization: Bearer {api_key}'`}
/> />

View File

@ -1586,8 +1586,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<Col> <Col>
<CodeGroup <CodeGroup
title="リクエスト" title="リクエスト"
tag="POST" tag="GET"
label="/meta" label="/site"
targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\ targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\
-H 'Authorization: Bearer {api_key}'`} -H 'Authorization: Bearer {api_key}'`}
/> />

View File

@ -1188,7 +1188,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
--header 'Content-Type: application/json' \\ --header 'Content-Type: application/json' \\
--data-raw '{ --data-raw '{
"value": "Updated Value", "value": "Updated Value",
"user": "abc-123" "user": "abc-123"
}'`} }'`}
/> />
@ -1579,8 +1579,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
<Col> <Col>
<CodeGroup <CodeGroup
title="Request" title="Request"
tag="POST" tag="GET"
label="/meta" label="/site"
targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\ targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\
-H 'Authorization: Bearer {api_key}'`} -H 'Authorization: Bearer {api_key}'`}
/> />

View File

@ -1025,8 +1025,8 @@ Workflow applications offers non-session support and is ideal for translation, a
<Col> <Col>
<CodeGroup <CodeGroup
title="Request" title="Request"
tag="POST" tag="GET"
label="/meta" label="/site"
targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\ targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\
-H 'Authorization: Bearer {api_key}'`} -H 'Authorization: Bearer {api_key}'`}
/> />

View File

@ -1021,8 +1021,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<Col> <Col>
<CodeGroup <CodeGroup
title="Request" title="Request"
tag="POST" tag="GET"
label="/meta" label="/site"
targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\ targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\
-H 'Authorization: Bearer {api_key}'`} -H 'Authorization: Bearer {api_key}'`}
/> />

View File

@ -182,7 +182,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
--header 'Authorization: Bearer {api_key}' \\ --header 'Authorization: Bearer {api_key}' \\
--header 'Content-Type: application/json' \\ --header 'Content-Type: application/json' \\
--data-raw '{ --data-raw '{
"inputs": ${JSON.stringify(props.inputs)}, "inputs": ${JSON.stringify(props.inputs)},
"response_mode": "streaming", "response_mode": "streaming",
"user": "abc-123" "user": "abc-123"
}'`} }'`}
@ -1012,8 +1012,8 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
<Col> <Col>
<CodeGroup <CodeGroup
title="Request" title="Request"
tag="POST" tag="GET"
label="/meta" label="/site"
targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\ targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\
-H 'Authorization: Bearer {api_key}'`} -H 'Authorization: Bearer {api_key}'`}
/> />

View File

@ -118,8 +118,20 @@ const FormItem: FC<Props> = ({
<div className={cn(className)}> <div className={cn(className)}>
{!isArrayLikeType && !isBooleanType && ( {!isArrayLikeType && !isBooleanType && (
<div className='system-sm-semibold mb-1 flex h-6 items-center gap-1 text-text-secondary'> <div className='system-sm-semibold mb-1 flex h-6 items-center gap-1 text-text-secondary'>
<div className='truncate'>{typeof payload.label === 'object' ? nodeKey : payload.label}</div> <div className='truncate'>
{!payload.required && <span className='system-xs-regular text-text-tertiary'>{t('workflow.panel.optional')}</span>} {typeof payload.label === 'object' ? nodeKey : payload.label}
</div>
{payload.hide === true ? (
<span className='system-xs-regular text-text-tertiary'>
{t('workflow.panel.optional_and_hidden')}
</span>
) : (
!payload.required && (
<span className='system-xs-regular text-text-tertiary'>
{t('workflow.panel.optional')}
</span>
)
)}
</div> </div>
)} )}
<div className='grow'> <div className='grow'>

View File

@ -61,7 +61,6 @@ const VarList: FC<Props> = ({
return return
} }
if (list.some(item => item.variable?.trim() === newKey.trim())) { if (list.some(item => item.variable?.trim() === newKey.trim())) {
console.log('new key', newKey.trim())
setToastHandle(Toast.notify({ setToastHandle(Toast.notify({
type: 'error', type: 'error',
message: t('appDebug.varKeyError.keyAlreadyExists', { key: newKey }), message: t('appDebug.varKeyError.keyAlreadyExists', { key: newKey }),

View File

@ -5,7 +5,6 @@ import produce from 'immer'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import VarItem from './var-item' import VarItem from './var-item'
import { ChangeType, type InputVar, type MoreInfo } from '@/app/components/workflow/types' import { ChangeType, type InputVar, type MoreInfo } from '@/app/components/workflow/types'
import { v4 as uuid4 } from 'uuid'
import { ReactSortable } from 'react-sortablejs' import { ReactSortable } from 'react-sortablejs'
import { RiDraggable } from '@remixicon/react' import { RiDraggable } from '@remixicon/react'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
@ -71,9 +70,8 @@ const VarList: FC<Props> = ({
}, [list, onChange]) }, [list, onChange])
const listWithIds = useMemo(() => list.map((item) => { const listWithIds = useMemo(() => list.map((item) => {
const id = uuid4()
return { return {
id, id: item.variable,
variable: { ...item }, variable: { ...item },
} }
}), [list]) }), [list])
@ -88,6 +86,8 @@ const VarList: FC<Props> = ({
) )
} }
const canDrag = !readonly && varCount > 1
return ( return (
<ReactSortable <ReactSortable
className='space-y-1' className='space-y-1'
@ -97,30 +97,23 @@ const VarList: FC<Props> = ({
ghostClass='opacity-50' ghostClass='opacity-50'
animation={150} animation={150}
> >
{list.map((item, index) => { {listWithIds.map((itemWithId, index) => (
const canDrag = (() => { <div key={itemWithId.id} className='group relative'>
if (readonly) <VarItem
return false className={cn(canDrag && 'handle')}
return varCount > 1 readonly={readonly}
})() payload={itemWithId.variable}
return ( onChange={handleVarChange(index)}
<div key={index} className='group relative'> onRemove={handleVarRemove(index)}
<VarItem varKeys={list.map(item => item.variable)}
className={cn(canDrag && 'handle')} canDrag={canDrag}
readonly={readonly} />
payload={item} {canDrag && <RiDraggable className={cn(
onChange={handleVarChange(index)} 'handle absolute left-3 top-2.5 hidden h-3 w-3 cursor-pointer text-text-tertiary',
onRemove={handleVarRemove(index)} 'group-hover:block',
varKeys={list.map(item => item.variable)} )} />}
canDrag={canDrag} </div>
/> ))}
{canDrag && <RiDraggable className={cn(
'handle absolute left-3 top-2.5 hidden h-3 w-3 cursor-pointer text-text-tertiary',
'group-hover:block',
)} />}
</div>
)
})}
</ReactSortable> </ReactSortable>
) )
} }

View File

@ -39,7 +39,7 @@ const DebugAndPreview = () => {
const selectedNode = nodes.find(node => node.data.selected) const selectedNode = nodes.find(node => node.data.selected)
const startNode = nodes.find(node => node.data.type === BlockEnum.Start) const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const variables = startNode?.data.variables || [] const variables = startNode?.data.variables || []
const visibleVariables = variables.filter(v => v.hide !== true) const visibleVariables = variables
const [showConversationVariableModal, setShowConversationVariableModal] = useState(false) const [showConversationVariableModal, setShowConversationVariableModal] = useState(false)

View File

@ -14,10 +14,11 @@ import cn from '@/utils/classnames'
const UserInput = () => { const UserInput = () => {
const workflowStore = useWorkflowStore() const workflowStore = useWorkflowStore()
const inputs = useStore(s => s.inputs) const inputs = useStore(s => s.inputs)
const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel)
const nodes = useNodes<StartNodeType>() const nodes = useNodes<StartNodeType>()
const startNode = nodes.find(node => node.data.type === BlockEnum.Start) const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const variables = startNode?.data.variables || [] 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 handleValueChange = (variable: string, v: string) => {
const { const {

View File

@ -160,6 +160,10 @@ const translation = {
title: 'Cloud-Monitor', 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.', 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: { answerIcon: {
descriptionInExplore: 'Gibt an, ob das web app Symbol zum Ersetzen 🤖 in Explore verwendet werden soll', descriptionInExplore: 'Gibt an, ob das web app Symbol zum Ersetzen 🤖 in Explore verwendet werden soll',

View File

@ -354,6 +354,7 @@ const translation = {
optional: '(optional)', optional: '(optional)',
maximize: 'Maximize Canvas', maximize: 'Maximize Canvas',
minimize: 'Exit Full Screen', minimize: 'Exit Full Screen',
optional_and_hidden: '(optional & hidden)',
}, },
nodes: { nodes: {
common: { common: {

View File

@ -163,6 +163,10 @@ const translation = {
title: 'Monitor de Nubes', 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.', 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: { answerIcon: {
title: 'Usar el icono de la aplicación web para reemplazar 🤖', title: 'Usar el icono de la aplicación web para reemplazar 🤖',

View File

@ -171,6 +171,10 @@ const translation = {
title: 'نظارت بر ابر', title: 'نظارت بر ابر',
description: 'پلتفرم مشاهده‌پذیری کاملاً مدیریت‌شده و بدون نیاز به نگهداری که توسط Alibaba Cloud ارائه شده، امکان نظارت، ردیابی و ارزیابی برنامه‌های Dify را به‌صورت آماده و با تنظیمات اولیه فراهم می‌کند.', description: 'پلتفرم مشاهده‌پذیری کاملاً مدیریت‌شده و بدون نیاز به نگهداری که توسط Alibaba Cloud ارائه شده، امکان نظارت، ردیابی و ارزیابی برنامه‌های Dify را به‌صورت آماده و با تنظیمات اولیه فراهم می‌کند.',
}, },
tencent: {
title: 'تنست ای‌پی‌ام',
description: 'نظارت بر عملکرد برنامه‌های Tencent تحلیل‌های جامع و ردیابی چندبعدی برای برنامه‌های LLM ارائه می‌دهد.',
},
}, },
answerIcon: { answerIcon: {
descriptionInExplore: 'آیا از نماد web app برای جایگزینی 🤖 در Explore استفاده کنیم یا خیر', descriptionInExplore: 'آیا از نماد web app برای جایگزینی 🤖 در Explore استفاده کنیم یا خیر',

View File

@ -163,6 +163,10 @@ const translation = {
title: 'Surveillance Cloud', 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.', 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: { answerIcon: {
description: 'Sil faut utiliser licône web app pour remplacer 🤖 dans lapplication partagée', description: 'Sil faut utiliser licône web app pour remplacer 🤖 dans lapplication partagée',

View File

@ -163,6 +163,10 @@ const translation = {
title: 'क्लाउड मॉनिटर', title: 'क्लाउड मॉनिटर',
description: 'अलीबाबा क्लाउड द्वारा प्रदान की गई पूरी तरह से प्रबंधित और रखरखाव-मुक्त अवलोकन प्लेटफ़ॉर्म, Dify अनुप्रयोगों की स्वचालित निगरानी, ट्रेसिंग और मूल्यांकन का सक्षम बनाता है।', description: 'अलीबाबा क्लाउड द्वारा प्रदान की गई पूरी तरह से प्रबंधित और रखरखाव-मुक्त अवलोकन प्लेटफ़ॉर्म, Dify अनुप्रयोगों की स्वचालित निगरानी, ट्रेसिंग और मूल्यांकन का सक्षम बनाता है।',
}, },
tencent: {
title: 'टेनसेंट एपीएम',
description: 'Tencent एप्लिकेशन परफॉर्मेंस मॉनिटरिंग LLM एप्लिकेशन के लिए व्यापक ट्रेसिंग और बहु-आयामी विश्लेषण प्रदान करता है।',
},
}, },
answerIcon: { answerIcon: {
title: 'बदलने 🤖 के लिए web app चिह्न का उपयोग करें', title: 'बदलने 🤖 के लिए web app चिह्न का उपयोग करें',

View File

@ -155,6 +155,10 @@ const translation = {
description: 'Mengonfigurasi penyedia LLMOps Pihak Ketiga dan melacak performa aplikasi.', description: 'Mengonfigurasi penyedia LLMOps Pihak Ketiga dan melacak performa aplikasi.',
inUse: 'Sedang digunakan', inUse: 'Sedang digunakan',
tracingDescription: 'Tangkap konteks lengkap eksekusi aplikasi, termasuk panggilan LLM, konteks, perintah, permintaan HTTP, dan lainnya, ke platform pelacakan pihak ketiga.', 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: { appSelector: {
placeholder: 'Pilih aplikasi...', placeholder: 'Pilih aplikasi...',

View File

@ -169,6 +169,10 @@ const translation = {
title: 'Monitoraggio Cloud', 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.', 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: { answerIcon: {
description: 'Se utilizzare l\'icona web app per la sostituzione 🤖 nell\'applicazione condivisa', description: 'Se utilizzare l\'icona web app per la sostituzione 🤖 nell\'applicazione condivisa',

View File

@ -175,6 +175,10 @@ const translation = {
title: 'クラウドモニター', title: 'クラウドモニター',
description: 'Alibaba Cloud が提供する完全管理型でメンテナンスフリーの可観測性プラットフォームは、Dify アプリケーションの即時監視、トレース、評価を可能にします。', description: 'Alibaba Cloud が提供する完全管理型でメンテナンスフリーの可観測性プラットフォームは、Dify アプリケーションの即時監視、トレース、評価を可能にします。',
}, },
tencent: {
title: 'テンセントAPM',
description: 'Tencent アプリケーションパフォーマンスモニタリングは、LLM アプリケーションに対して包括的なトレーシングと多次元分析を提供します。',
},
}, },
answerIcon: { answerIcon: {
title: 'Web アプリアイコンを使用して🤖を置き換える', title: 'Web アプリアイコンを使用して🤖を置き換える',

View File

@ -178,6 +178,10 @@ const translation = {
title: '클라우드 모니터', title: '클라우드 모니터',
description: '알리바바 클라우드에서 제공하는 완전 관리형 및 유지보수가 필요 없는 가시성 플랫폼은 Dify 애플리케이션의 모니터링, 추적 및 평가를 즉시 사용할 수 있도록 지원합니다.', description: '알리바바 클라우드에서 제공하는 완전 관리형 및 유지보수가 필요 없는 가시성 플랫폼은 Dify 애플리케이션의 모니터링, 추적 및 평가를 즉시 사용할 수 있도록 지원합니다.',
}, },
tencent: {
title: '텐센트 APM',
description: '텐센트 애플리케이션 성능 모니터링은 LLM 애플리케이션에 대한 포괄적인 추적 및 다차원 분석을 제공합니다.',
},
}, },
answerIcon: { answerIcon: {
description: description:

View File

@ -164,6 +164,10 @@ const translation = {
title: 'Monitor Chmury', 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.', 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: { answerIcon: {
description: 'Czy w aplikacji udostępnionej ma być używana ikona aplikacji internetowej do zamiany 🤖.', description: 'Czy w aplikacji udostępnionej ma być używana ikona aplikacji internetowej do zamiany 🤖.',

View File

@ -163,6 +163,10 @@ const translation = {
title: 'Monitoramento em Nuvem', 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.', 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: { answerIcon: {
descriptionInExplore: 'Se o ícone do web app deve ser usado para substituir 🤖 no Explore', descriptionInExplore: 'Se o ícone do web app deve ser usado para substituir 🤖 no Explore',

View File

@ -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.', description: 'Platforma de observabilitate SaaS oferită de Alibaba Cloud permite monitorizarea, urmărirea și evaluarea aplicațiilor Dify din cutie.',
title: 'Monitorizarea Cloud', 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: { answerIcon: {
descriptionInExplore: 'Dacă să utilizați pictograma web app pentru a înlocui 🤖 în Explore', descriptionInExplore: 'Dacă să utilizați pictograma web app pentru a înlocui 🤖 în Explore',

View File

@ -171,6 +171,10 @@ const translation = {
title: 'Облачный монитор', title: 'Облачный монитор',
description: 'Полностью управляемая и не требующая обслуживания платформа наблюдения, предоставляемая Alibaba Cloud, обеспечивает мониторинг, трассировку и оценку приложений Dify из коробки.', description: 'Полностью управляемая и не требующая обслуживания платформа наблюдения, предоставляемая Alibaba Cloud, обеспечивает мониторинг, трассировку и оценку приложений Dify из коробки.',
}, },
tencent: {
title: 'Tencent APM',
description: 'Мониторинг производительности приложений Tencent предоставляет всестороннее отслеживание и многомерный анализ для приложений LLM.',
},
}, },
answerIcon: { answerIcon: {
title: 'Использование значка web app для замены 🤖', title: 'Использование значка web app для замены 🤖',

View File

@ -176,6 +176,10 @@ const translation = {
title: 'Oblačni nadzor', 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.', 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: { mermaid: {
handDrawn: 'Ročno narisano', handDrawn: 'Ročno narisano',

View File

@ -172,6 +172,10 @@ const translation = {
title: 'การตรวจสอบคลาวด์', title: 'การตรวจสอบคลาวด์',
description: 'แพลตฟอร์มการสังเกตการณ์ที่จัดการโดย Alibaba Cloud ซึ่งไม่ต้องดูแลและบำรุงรักษา ช่วยให้สามารถติดตาม ตรวจสอบ และประเมินแอปพลิเคชัน Dify ได้ทันที', description: 'แพลตฟอร์มการสังเกตการณ์ที่จัดการโดย Alibaba Cloud ซึ่งไม่ต้องดูแลและบำรุงรักษา ช่วยให้สามารถติดตาม ตรวจสอบ และประเมินแอปพลิเคชัน Dify ได้ทันที',
}, },
tencent: {
title: 'Tencent APM',
description: 'การติดตามประสิทธิภาพแอปพลิเคชันของ Tencent มอบการตรวจสอบแบบครบวงจรและการวิเคราะห์หลายมิติสำหรับแอป LLM',
},
}, },
mermaid: { mermaid: {
handDrawn: 'วาดด้วยมือ', handDrawn: 'วาดด้วยมือ',

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