Merge branch 'langgenius:main' into feat/rediscluster-support-custom-user

pull/21244/head
homejim 11 months ago committed by GitHub
commit 3c9103bac8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

1
.gitignore vendored

@ -179,6 +179,7 @@ docker/volumes/pgvecto_rs/data/*
docker/volumes/couchbase/* docker/volumes/couchbase/*
docker/volumes/oceanbase/* docker/volumes/oceanbase/*
docker/volumes/plugin_daemon/* docker/volumes/plugin_daemon/*
docker/volumes/matrixone/*
!docker/volumes/oceanbase/init.d !docker/volumes/oceanbase/init.d
docker/nginx/conf.d/default.conf docker/nginx/conf.d/default.conf

@ -137,7 +137,7 @@ WEB_API_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,*
CONSOLE_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,* CONSOLE_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,*
# Vector database configuration # Vector database configuration
# support: weaviate, qdrant, milvus, myscale, relyt, pgvecto_rs, pgvector, pgvector, chroma, opensearch, tidb_vector, couchbase, vikingdb, upstash, lindorm, oceanbase, opengauss, tablestore # support: weaviate, qdrant, milvus, myscale, relyt, pgvecto_rs, pgvector, pgvector, chroma, opensearch, tidb_vector, couchbase, vikingdb, upstash, lindorm, oceanbase, opengauss, tablestore, matrixone
VECTOR_STORE=weaviate VECTOR_STORE=weaviate
# Weaviate configuration # Weaviate configuration
@ -294,6 +294,13 @@ VIKINGDB_SCHEMA=http
VIKINGDB_CONNECTION_TIMEOUT=30 VIKINGDB_CONNECTION_TIMEOUT=30
VIKINGDB_SOCKET_TIMEOUT=30 VIKINGDB_SOCKET_TIMEOUT=30
# Matrixone configration
MATRIXONE_HOST=127.0.0.1
MATRIXONE_PORT=6001
MATRIXONE_USER=dump
MATRIXONE_PASSWORD=111
MATRIXONE_DATABASE=dify
# Lindorm configuration # Lindorm configuration
LINDORM_URL=http://ld-*******************-proxy-search-pub.lindorm.aliyuncs.com:30070 LINDORM_URL=http://ld-*******************-proxy-search-pub.lindorm.aliyuncs.com:30070
LINDORM_USERNAME=admin LINDORM_USERNAME=admin
@ -332,9 +339,11 @@ PROMPT_GENERATION_MAX_TOKENS=512
CODE_GENERATION_MAX_TOKENS=1024 CODE_GENERATION_MAX_TOKENS=1024
PLUGIN_BASED_TOKEN_COUNTING_ENABLED=false PLUGIN_BASED_TOKEN_COUNTING_ENABLED=false
# Mail configuration, support: resend, smtp # Mail configuration, support: resend, smtp, sendgrid
MAIL_TYPE= MAIL_TYPE=
# If using SendGrid, use the 'from' field for authentication if necessary.
MAIL_DEFAULT_SEND_FROM=no-reply <no-reply@dify.ai> MAIL_DEFAULT_SEND_FROM=no-reply <no-reply@dify.ai>
# resend configuration
RESEND_API_KEY= RESEND_API_KEY=
RESEND_API_URL=https://api.resend.com RESEND_API_URL=https://api.resend.com
# smtp configuration # smtp configuration
@ -344,7 +353,8 @@ SMTP_USERNAME=123
SMTP_PASSWORD=abc SMTP_PASSWORD=abc
SMTP_USE_TLS=true SMTP_USE_TLS=true
SMTP_OPPORTUNISTIC_TLS=false SMTP_OPPORTUNISTIC_TLS=false
# Sendgid configuration
SENDGRID_API_KEY=
# Sentry configuration # Sentry configuration
SENTRY_DSN= SENTRY_DSN=

@ -281,6 +281,7 @@ def migrate_knowledge_vector_database():
VectorType.ELASTICSEARCH, VectorType.ELASTICSEARCH,
VectorType.OPENGAUSS, VectorType.OPENGAUSS,
VectorType.TABLESTORE, VectorType.TABLESTORE,
VectorType.MATRIXONE,
} }
lower_collection_vector_types = { lower_collection_vector_types = {
VectorType.ANALYTICDB, VectorType.ANALYTICDB,

@ -609,7 +609,7 @@ class MailConfig(BaseSettings):
""" """
MAIL_TYPE: Optional[str] = Field( MAIL_TYPE: Optional[str] = Field(
description="Email service provider type ('smtp' or 'resend'), default to None.", description="Email service provider type ('smtp' or 'resend' or 'sendGrid), default to None.",
default=None, default=None,
) )
@ -663,6 +663,11 @@ class MailConfig(BaseSettings):
default=50, default=50,
) )
SENDGRID_API_KEY: Optional[str] = Field(
description="API key for SendGrid service",
default=None,
)
class RagEtlConfig(BaseSettings): class RagEtlConfig(BaseSettings):
""" """

@ -24,6 +24,7 @@ from .vdb.couchbase_config import CouchbaseConfig
from .vdb.elasticsearch_config import ElasticsearchConfig from .vdb.elasticsearch_config import ElasticsearchConfig
from .vdb.huawei_cloud_config import HuaweiCloudConfig from .vdb.huawei_cloud_config import HuaweiCloudConfig
from .vdb.lindorm_config import LindormConfig from .vdb.lindorm_config import LindormConfig
from .vdb.matrixone_config import MatrixoneConfig
from .vdb.milvus_config import MilvusConfig from .vdb.milvus_config import MilvusConfig
from .vdb.myscale_config import MyScaleConfig from .vdb.myscale_config import MyScaleConfig
from .vdb.oceanbase_config import OceanBaseVectorConfig from .vdb.oceanbase_config import OceanBaseVectorConfig
@ -323,5 +324,6 @@ class MiddlewareConfig(
OpenGaussConfig, OpenGaussConfig,
TableStoreConfig, TableStoreConfig,
DatasetQueueMonitorConfig, DatasetQueueMonitorConfig,
MatrixoneConfig,
): ):
pass pass

@ -0,0 +1,14 @@
from pydantic import BaseModel, Field
class MatrixoneConfig(BaseModel):
"""Matrixone vector database configuration."""
MATRIXONE_HOST: str = Field(default="localhost", description="Host address of the Matrixone server")
MATRIXONE_PORT: int = Field(default=6001, description="Port number of the Matrixone server")
MATRIXONE_USER: str = Field(default="dump", description="Username for authenticating with Matrixone")
MATRIXONE_PASSWORD: str = Field(default="111", description="Password for authenticating with Matrixone")
MATRIXONE_DATABASE: str = Field(default="dify", description="Name of the Matrixone database to connect to")
MATRIXONE_METRIC: str = Field(
default="l2", description="Distance metric type for vector similarity search (cosine or l2)"
)

@ -208,7 +208,7 @@ class AnnotationBatchImportApi(Resource):
if len(request.files) > 1: if len(request.files) > 1:
raise TooManyFilesError() raise TooManyFilesError()
# check file type # check file type
if not file.filename or not file.filename.endswith(".csv"): if not file.filename or not file.filename.lower().endswith(".csv"):
raise ValueError("Invalid file type. Only CSV files are allowed") raise ValueError("Invalid file type. Only CSV files are allowed")
return AppAnnotationService.batch_import_app_annotations(app_id, file) return AppAnnotationService.batch_import_app_annotations(app_id, file)

@ -17,6 +17,8 @@ from libs.login import login_required
from models import Account from models import Account
from models.model import App from models.model import App
from services.app_dsl_service import AppDslService, ImportStatus from services.app_dsl_service import AppDslService, ImportStatus
from services.enterprise.enterprise_service import EnterpriseService
from services.feature_service import FeatureService
class AppImportApi(Resource): class AppImportApi(Resource):
@ -60,7 +62,9 @@ class AppImportApi(Resource):
app_id=args.get("app_id"), app_id=args.get("app_id"),
) )
session.commit() session.commit()
if result.app_id and FeatureService.get_system_features().webapp_auth.enabled:
# update web app setting as private
EnterpriseService.WebAppAuth.update_app_access_mode(result.app_id, "private")
# Return appropriate status code based on result # Return appropriate status code based on result
status = result.status status = result.status
if status == ImportStatus.FAILED.value: if status == ImportStatus.FAILED.value:

@ -34,6 +34,20 @@ class WorkflowAppLogApi(Resource):
parser.add_argument( parser.add_argument(
"created_at__after", type=str, location="args", help="Filter logs created after this timestamp" "created_at__after", type=str, location="args", help="Filter logs created after this timestamp"
) )
parser.add_argument(
"created_by_end_user_session_id",
type=str,
location="args",
required=False,
default=None,
)
parser.add_argument(
"created_by_account",
type=str,
location="args",
required=False,
default=None,
)
parser.add_argument("page", type=int_range(1, 99999), default=1, location="args") parser.add_argument("page", type=int_range(1, 99999), default=1, location="args")
parser.add_argument("limit", type=int_range(1, 100), default=20, location="args") parser.add_argument("limit", type=int_range(1, 100), default=20, location="args")
args = parser.parse_args() args = parser.parse_args()
@ -57,6 +71,8 @@ class WorkflowAppLogApi(Resource):
created_at_after=args.created_at__after, created_at_after=args.created_at__after,
page=args.page, page=args.page,
limit=args.limit, limit=args.limit,
created_by_end_user_session_id=args.created_by_end_user_session_id,
created_by_account=args.created_by_account,
) )
return workflow_app_log_pagination return workflow_app_log_pagination

@ -686,6 +686,7 @@ class DatasetRetrievalSettingApi(Resource):
| VectorType.TABLESTORE | VectorType.TABLESTORE
| VectorType.HUAWEI_CLOUD | VectorType.HUAWEI_CLOUD
| VectorType.TENCENT | VectorType.TENCENT
| VectorType.MATRIXONE
): ):
return { return {
"retrieval_method": [ "retrieval_method": [
@ -733,6 +734,7 @@ class DatasetRetrievalSettingMockApi(Resource):
| VectorType.TABLESTORE | VectorType.TABLESTORE
| VectorType.TENCENT | VectorType.TENCENT
| VectorType.HUAWEI_CLOUD | VectorType.HUAWEI_CLOUD
| VectorType.MATRIXONE
): ):
return { return {
"retrieval_method": [ "retrieval_method": [

@ -374,7 +374,7 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
if len(request.files) > 1: if len(request.files) > 1:
raise TooManyFilesError() raise TooManyFilesError()
# check file type # check file type
if not file.filename or not file.filename.endswith(".csv"): if not file.filename or not file.filename.lower().endswith(".csv"):
raise ValueError("Invalid file type. Only CSV files are allowed") raise ValueError("Invalid file type. Only CSV files are allowed")
try: try:

@ -15,7 +15,7 @@ class LoadBalancingCredentialsValidateApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
def post(self, provider: str): def post(self, provider: str):
if not TenantAccountRole.is_privileged_role(current_user.current_tenant.current_role): if not TenantAccountRole.is_privileged_role(current_user.current_role):
raise Forbidden() raise Forbidden()
tenant_id = current_user.current_tenant_id tenant_id = current_user.current_tenant_id
@ -64,7 +64,7 @@ class LoadBalancingConfigCredentialsValidateApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
def post(self, provider: str, config_id: str): def post(self, provider: str, config_id: str):
if not TenantAccountRole.is_privileged_role(current_user.current_tenant.current_role): if not TenantAccountRole.is_privileged_role(current_user.current_role):
raise Forbidden() raise Forbidden()
tenant_id = current_user.current_tenant_id tenant_id = current_user.current_tenant_id

@ -135,6 +135,20 @@ class WorkflowAppLogApi(Resource):
parser.add_argument("status", type=str, choices=["succeeded", "failed", "stopped"], location="args") parser.add_argument("status", type=str, choices=["succeeded", "failed", "stopped"], location="args")
parser.add_argument("created_at__before", type=str, location="args") parser.add_argument("created_at__before", type=str, location="args")
parser.add_argument("created_at__after", type=str, location="args") parser.add_argument("created_at__after", type=str, location="args")
parser.add_argument(
"created_by_end_user_session_id",
type=str,
location="args",
required=False,
default=None,
)
parser.add_argument(
"created_by_account",
type=str,
location="args",
required=False,
default=None,
)
parser.add_argument("page", type=int_range(1, 99999), default=1, location="args") parser.add_argument("page", type=int_range(1, 99999), default=1, location="args")
parser.add_argument("limit", type=int_range(1, 100), default=20, location="args") parser.add_argument("limit", type=int_range(1, 100), default=20, location="args")
args = parser.parse_args() args = parser.parse_args()
@ -158,6 +172,8 @@ class WorkflowAppLogApi(Resource):
created_at_after=args.created_at__after, created_at_after=args.created_at__after,
page=args.page, page=args.page,
limit=args.limit, limit=args.limit,
created_by_end_user_session_id=args.created_by_end_user_session_id,
created_by_account=args.created_by_account,
) )
return workflow_app_log_pagination return workflow_app_log_pagination

@ -367,6 +367,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
:param user: account or end user :param user: account or end user
:param invoke_from: invoke from source :param invoke_from: invoke from source
:param application_generate_entity: application generate entity :param application_generate_entity: application generate entity
:param workflow_execution_repository: repository for workflow execution
:param workflow_node_execution_repository: repository for workflow node execution :param workflow_node_execution_repository: repository for workflow node execution
:param conversation: conversation :param conversation: conversation
:param stream: is stream :param stream: is stream

@ -195,6 +195,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
:param user: account or end user :param user: account or end user
:param application_generate_entity: application generate entity :param application_generate_entity: application generate entity
:param invoke_from: invoke from source :param invoke_from: invoke from source
:param workflow_execution_repository: repository for workflow execution
:param workflow_node_execution_repository: repository for workflow node execution :param workflow_node_execution_repository: repository for workflow node execution
:param streaming: is stream :param streaming: is stream
:param workflow_thread_pool_id: workflow thread pool id :param workflow_thread_pool_id: workflow thread pool id

@ -251,7 +251,7 @@ class OpsTraceManager:
provider_config_map[tracing_provider]["trace_instance"], provider_config_map[tracing_provider]["trace_instance"],
provider_config_map[tracing_provider]["config_class"], provider_config_map[tracing_provider]["config_class"],
) )
decrypt_trace_config_key = str(decrypt_trace_config) decrypt_trace_config_key = json.dumps(decrypt_trace_config, sort_keys=True)
tracing_instance = cls.ops_trace_instances_cache.get(decrypt_trace_config_key) tracing_instance = cls.ops_trace_instances_cache.get(decrypt_trace_config_key)
if tracing_instance is None: if tracing_instance is None:
# create new tracing_instance and update the cache if it absent # create new tracing_instance and update the cache if it absent

@ -156,9 +156,23 @@ class PluginInstallTaskStartResponse(BaseModel):
task_id: str = Field(description="The ID of the install task.") task_id: str = Field(description="The ID of the install task.")
class PluginUploadResponse(BaseModel): class PluginVerification(BaseModel):
"""
Verification of the plugin.
"""
class AuthorizedCategory(StrEnum):
Langgenius = "langgenius"
Partner = "partner"
Community = "community"
authorized_category: AuthorizedCategory = Field(description="The authorized category of the plugin.")
class PluginDecodeResponse(BaseModel):
unique_identifier: str = Field(description="The unique identifier of the plugin.") unique_identifier: str = Field(description="The unique identifier of the plugin.")
manifest: PluginDeclaration manifest: PluginDeclaration
verification: Optional[PluginVerification] = Field(default=None, description="Basic verification information")
class PluginOAuthAuthorizationUrlResponse(BaseModel): class PluginOAuthAuthorizationUrlResponse(BaseModel):

@ -10,10 +10,10 @@ from core.plugin.entities.plugin import (
PluginInstallationSource, PluginInstallationSource,
) )
from core.plugin.entities.plugin_daemon import ( from core.plugin.entities.plugin_daemon import (
PluginDecodeResponse,
PluginInstallTask, PluginInstallTask,
PluginInstallTaskStartResponse, PluginInstallTaskStartResponse,
PluginListResponse, PluginListResponse,
PluginUploadResponse,
) )
from core.plugin.impl.base import BasePluginClient from core.plugin.impl.base import BasePluginClient
@ -53,7 +53,7 @@ class PluginInstaller(BasePluginClient):
tenant_id: str, tenant_id: str,
pkg: bytes, pkg: bytes,
verify_signature: bool = False, verify_signature: bool = False,
) -> PluginUploadResponse: ) -> PluginDecodeResponse:
""" """
Upload a plugin package and return the plugin unique identifier. Upload a plugin package and return the plugin unique identifier.
""" """
@ -68,7 +68,7 @@ class PluginInstaller(BasePluginClient):
return self._request_with_plugin_daemon_response( return self._request_with_plugin_daemon_response(
"POST", "POST",
f"plugin/{tenant_id}/management/install/upload/package", f"plugin/{tenant_id}/management/install/upload/package",
PluginUploadResponse, PluginDecodeResponse,
files=body, files=body,
data=data, data=data,
) )
@ -176,6 +176,18 @@ class PluginInstaller(BasePluginClient):
params={"plugin_unique_identifier": plugin_unique_identifier}, params={"plugin_unique_identifier": plugin_unique_identifier},
) )
def decode_plugin_from_identifier(self, tenant_id: str, plugin_unique_identifier: str) -> PluginDecodeResponse:
"""
Decode a plugin from an identifier.
"""
return self._request_with_plugin_daemon_response(
"GET",
f"plugin/{tenant_id}/management/decode/from_identifier",
PluginDecodeResponse,
data={"plugin_unique_identifier": plugin_unique_identifier},
headers={"Content-Type": "application/json"},
)
def fetch_plugin_installation_by_ids( def fetch_plugin_installation_by_ids(
self, tenant_id: str, plugin_ids: Sequence[str] self, tenant_id: str, plugin_ids: Sequence[str]
) -> Sequence[PluginInstallation]: ) -> Sequence[PluginInstallation]:

@ -0,0 +1,233 @@
import json
import logging
import uuid
from functools import wraps
from typing import Any, Optional
from mo_vector.client import MoVectorClient # type: ignore
from pydantic import BaseModel, model_validator
from configs import dify_config
from core.rag.datasource.vdb.vector_base import BaseVector
from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory
from core.rag.datasource.vdb.vector_type import VectorType
from core.rag.embedding.embedding_base import Embeddings
from core.rag.models.document import Document
from extensions.ext_redis import redis_client
from models.dataset import Dataset
logger = logging.getLogger(__name__)
class MatrixoneConfig(BaseModel):
host: str = "localhost"
port: int = 6001
user: str = "dump"
password: str = "111"
database: str = "dify"
metric: str = "l2"
@model_validator(mode="before")
@classmethod
def validate_config(cls, values: dict) -> dict:
if not values["host"]:
raise ValueError("config host is required")
if not values["port"]:
raise ValueError("config port is required")
if not values["user"]:
raise ValueError("config user is required")
if not values["password"]:
raise ValueError("config password is required")
if not values["database"]:
raise ValueError("config database is required")
return values
def ensure_client(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
if self.client is None:
self.client = self._get_client(None, False)
return func(self, *args, **kwargs)
return wrapper
class MatrixoneVector(BaseVector):
"""
Matrixone vector storage implementation.
"""
def __init__(self, collection_name: str, config: MatrixoneConfig):
super().__init__(collection_name)
self.config = config
self.collection_name = collection_name.lower()
self.client = None
@property
def collection_name(self):
return self._collection_name
@collection_name.setter
def collection_name(self, value):
self._collection_name = value
def get_type(self) -> str:
return VectorType.MATRIXONE
def create(self, texts: list[Document], embeddings: list[list[float]], **kwargs):
if self.client is None:
self.client = self._get_client(len(embeddings[0]), True)
return self.add_texts(texts, embeddings)
def _get_client(self, dimension: Optional[int] = None, create_table: bool = False) -> MoVectorClient:
"""
Create a new client for the collection.
The collection will be created if it doesn't exist.
"""
lock_name = f"vector_indexing_lock_{self._collection_name}"
with redis_client.lock(lock_name, timeout=20):
client = MoVectorClient(
connection_string=f"mysql+pymysql://{self.config.user}:{self.config.password}@{self.config.host}:{self.config.port}/{self.config.database}",
table_name=self.collection_name,
vector_dimension=dimension,
create_table=create_table,
)
collection_exist_cache_key = f"vector_indexing_{self._collection_name}"
if redis_client.get(collection_exist_cache_key):
return client
try:
client.create_full_text_index()
except Exception as e:
logger.exception("Failed to create full text index")
redis_client.set(collection_exist_cache_key, 1, ex=3600)
return client
def add_texts(self, documents: list[Document], embeddings: list[list[float]], **kwargs):
if self.client is None:
self.client = self._get_client(len(embeddings[0]), True)
assert self.client is not None
ids = []
for _, doc in enumerate(documents):
if doc.metadata is not None:
doc_id = doc.metadata.get("doc_id", str(uuid.uuid4()))
ids.append(doc_id)
self.client.insert(
texts=[doc.page_content for doc in documents],
embeddings=embeddings,
metadatas=[doc.metadata for doc in documents],
ids=ids,
)
return ids
@ensure_client
def text_exists(self, id: str) -> bool:
assert self.client is not None
result = self.client.get(ids=[id])
return len(result) > 0
@ensure_client
def delete_by_ids(self, ids: list[str]) -> None:
assert self.client is not None
if not ids:
return
self.client.delete(ids=ids)
@ensure_client
def get_ids_by_metadata_field(self, key: str, value: str):
assert self.client is not None
results = self.client.query_by_metadata(filter={key: value})
return [result.id for result in results]
@ensure_client
def delete_by_metadata_field(self, key: str, value: str) -> None:
assert self.client is not None
self.client.delete(filter={key: value})
@ensure_client
def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]:
assert self.client is not None
top_k = kwargs.get("top_k", 5)
document_ids_filter = kwargs.get("document_ids_filter")
filter = None
if document_ids_filter:
filter = {"document_id": {"$in": document_ids_filter}}
results = self.client.query(
query_vector=query_vector,
k=top_k,
filter=filter,
)
docs = []
# TODO: add the score threshold to the query
for result in results:
metadata = result.metadata
docs.append(
Document(
page_content=result.document,
metadata=metadata,
)
)
return docs
@ensure_client
def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]:
assert self.client is not None
top_k = kwargs.get("top_k", 5)
document_ids_filter = kwargs.get("document_ids_filter")
filter = None
if document_ids_filter:
filter = {"document_id": {"$in": document_ids_filter}}
score_threshold = float(kwargs.get("score_threshold", 0.0))
results = self.client.full_text_query(
keywords=[query],
k=top_k,
filter=filter,
)
docs = []
for result in results:
metadata = result.metadata
if isinstance(metadata, str):
import json
metadata = json.loads(metadata)
score = 1 - result.distance
if score >= score_threshold:
metadata["score"] = score
docs.append(
Document(
page_content=result.document,
metadata=metadata,
)
)
return docs
@ensure_client
def delete(self) -> None:
assert self.client is not None
self.client.delete()
class MatrixoneVectorFactory(AbstractVectorFactory):
def init_vector(self, dataset: Dataset, attributes: list, embeddings: Embeddings) -> MatrixoneVector:
if dataset.index_struct_dict:
class_prefix: str = dataset.index_struct_dict["vector_store"]["class_prefix"]
collection_name = class_prefix
else:
dataset_id = dataset.id
collection_name = Dataset.gen_collection_name_by_id(dataset_id)
dataset.index_struct = json.dumps(self.gen_index_struct_dict(VectorType.MATRIXONE, collection_name))
config = MatrixoneConfig(
host=dify_config.MATRIXONE_HOST or "localhost",
port=dify_config.MATRIXONE_PORT or 6001,
user=dify_config.MATRIXONE_USER or "dump",
password=dify_config.MATRIXONE_PASSWORD or "111",
database=dify_config.MATRIXONE_DATABASE or "dify",
metric=dify_config.MATRIXONE_METRIC or "l2",
)
return MatrixoneVector(collection_name=collection_name, config=config)

@ -164,6 +164,10 @@ class Vector:
from core.rag.datasource.vdb.huawei.huawei_cloud_vector import HuaweiCloudVectorFactory from core.rag.datasource.vdb.huawei.huawei_cloud_vector import HuaweiCloudVectorFactory
return HuaweiCloudVectorFactory return HuaweiCloudVectorFactory
case VectorType.MATRIXONE:
from core.rag.datasource.vdb.matrixone.matrixone_vector import MatrixoneVectorFactory
return MatrixoneVectorFactory
case _: case _:
raise ValueError(f"Vector store {vector_type} is not supported.") raise ValueError(f"Vector store {vector_type} is not supported.")

@ -29,3 +29,4 @@ class VectorType(StrEnum):
OPENGAUSS = "opengauss" OPENGAUSS = "opengauss"
TABLESTORE = "tablestore" TABLESTORE = "tablestore"
HUAWEI_CLOUD = "huawei_cloud" HUAWEI_CLOUD = "huawei_cloud"
MATRIXONE = "matrixone"

@ -45,7 +45,8 @@ class WeaviateVector(BaseVector):
# by changing the connection timeout to pypi.org from 1 second to 0.001 seconds. # by changing the connection timeout to pypi.org from 1 second to 0.001 seconds.
# TODO: This can be removed once weaviate-client is updated to 3.26.7 or higher, # TODO: This can be removed once weaviate-client is updated to 3.26.7 or higher,
# which does not contain the deprecation check. # which does not contain the deprecation check.
weaviate.connect.connection.PYPI_TIMEOUT = 0.001 if hasattr(weaviate.connect.connection, "PYPI_TIMEOUT"):
weaviate.connect.connection.PYPI_TIMEOUT = 0.001
try: try:
client = weaviate.Client( client = weaviate.Client(

@ -104,7 +104,7 @@ class QAIndexProcessor(BaseIndexProcessor):
def format_by_template(self, file: FileStorage, **kwargs) -> list[Document]: def format_by_template(self, file: FileStorage, **kwargs) -> list[Document]:
# check file type # check file type
if not file.filename or not file.filename.endswith(".csv"): if not file.filename or not file.filename.lower().endswith(".csv"):
raise ValueError("Invalid file type. Only CSV files are allowed") raise ValueError("Invalid file type. Only CSV files are allowed")
try: try:

@ -6,7 +6,7 @@ import json
import logging import logging
from typing import Optional, Union from typing import Optional, Union
from sqlalchemy import func, select from sqlalchemy import select
from sqlalchemy.engine import Engine from sqlalchemy.engine import Engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
@ -146,20 +146,7 @@ class SQLAlchemyWorkflowExecutionRepository(WorkflowExecutionRepository):
db_model.workflow_id = domain_model.workflow_id db_model.workflow_id = domain_model.workflow_id
db_model.triggered_from = self._triggered_from db_model.triggered_from = self._triggered_from
# Check if this is a new record # No sequence number generation needed anymore
with self._session_factory() as session:
existing = session.scalar(select(WorkflowRun).where(WorkflowRun.id == domain_model.id_))
if not existing:
# For new records, get the next sequence number
stmt = select(func.max(WorkflowRun.sequence_number)).where(
WorkflowRun.app_id == self._app_id,
WorkflowRun.tenant_id == self._tenant_id,
)
max_sequence = session.scalar(stmt)
db_model.sequence_number = (max_sequence or 0) + 1
else:
# For updates, keep the existing sequence number
db_model.sequence_number = existing.sequence_number
db_model.type = domain_model.workflow_type db_model.type = domain_model.workflow_type
db_model.version = domain_model.workflow_version db_model.version = domain_model.workflow_version

@ -57,7 +57,6 @@ class StreamProcessor(ABC):
# The branch_identify parameter is added to ensure that # The branch_identify parameter is added to ensure that
# only nodes in the correct logical branch are included. # only nodes in the correct logical branch are included.
reachable_node_ids.append(edge.target_node_id)
ids = self._fetch_node_ids_in_reachable_branch(edge.target_node_id, run_result.edge_source_handle) ids = self._fetch_node_ids_in_reachable_branch(edge.target_node_id, run_result.edge_source_handle)
reachable_node_ids.extend(ids) reachable_node_ids.extend(ids)
else: else:
@ -74,6 +73,8 @@ class StreamProcessor(ABC):
self._remove_node_ids_in_unreachable_branch(node_id, reachable_node_ids) self._remove_node_ids_in_unreachable_branch(node_id, reachable_node_ids)
def _fetch_node_ids_in_reachable_branch(self, node_id: str, branch_identify: Optional[str] = None) -> list[str]: def _fetch_node_ids_in_reachable_branch(self, node_id: str, branch_identify: Optional[str] = None) -> list[str]:
if node_id not in self.rest_node_ids:
self.rest_node_ids.append(node_id)
node_ids = [] node_ids = []
for edge in self.graph.edge_mapping.get(node_id, []): for edge in self.graph.edge_mapping.get(node_id, []):
if edge.target_node_id == self.graph.root_node_id: if edge.target_node_id == self.graph.root_node_id:

@ -8,4 +8,5 @@ EMPTY_VALUE_MAPPING = {
SegmentType.ARRAY_STRING: [], SegmentType.ARRAY_STRING: [],
SegmentType.ARRAY_NUMBER: [], SegmentType.ARRAY_NUMBER: [],
SegmentType.ARRAY_OBJECT: [], SegmentType.ARRAY_OBJECT: [],
SegmentType.ARRAY_FILE: [],
} }

@ -1,5 +1,6 @@
from typing import Any from typing import Any
from core.file import File
from core.variables import SegmentType from core.variables import SegmentType
from .enums import Operation from .enums import Operation
@ -85,6 +86,8 @@ def is_input_value_valid(*, variable_type: SegmentType, operation: Operation, va
return isinstance(value, int | float) return isinstance(value, int | float)
case SegmentType.ARRAY_OBJECT if operation == Operation.APPEND: case SegmentType.ARRAY_OBJECT if operation == Operation.APPEND:
return isinstance(value, dict) return isinstance(value, dict)
case SegmentType.ARRAY_FILE if operation == Operation.APPEND:
return isinstance(value, File)
# Array & Extend / Overwrite # Array & Extend / Overwrite
case SegmentType.ARRAY_ANY if operation in {Operation.EXTEND, Operation.OVER_WRITE}: case SegmentType.ARRAY_ANY if operation in {Operation.EXTEND, Operation.OVER_WRITE}:
@ -95,6 +98,8 @@ def is_input_value_valid(*, variable_type: SegmentType, operation: Operation, va
return isinstance(value, list) and all(isinstance(item, int | float) for item in value) return isinstance(value, list) and all(isinstance(item, int | float) for item in value)
case SegmentType.ARRAY_OBJECT if operation in {Operation.EXTEND, Operation.OVER_WRITE}: case SegmentType.ARRAY_OBJECT if operation in {Operation.EXTEND, Operation.OVER_WRITE}:
return isinstance(value, list) and all(isinstance(item, dict) for item in value) return isinstance(value, list) and all(isinstance(item, dict) for item in value)
case SegmentType.ARRAY_FILE if operation in {Operation.EXTEND, Operation.OVER_WRITE}:
return isinstance(value, list) and all(isinstance(item, File) for item in value)
case _: case _:
return False return False

@ -54,6 +54,15 @@ class Mail:
use_tls=dify_config.SMTP_USE_TLS, use_tls=dify_config.SMTP_USE_TLS,
opportunistic_tls=dify_config.SMTP_OPPORTUNISTIC_TLS, opportunistic_tls=dify_config.SMTP_OPPORTUNISTIC_TLS,
) )
case "sendgrid":
from libs.sendgrid import SendGridClient
if not dify_config.SENDGRID_API_KEY:
raise ValueError("SENDGRID_API_KEY is required for SendGrid mail type")
self._client = SendGridClient(
sendgrid_api_key=dify_config.SENDGRID_API_KEY, _from=dify_config.MAIL_DEFAULT_SEND_FROM or ""
)
case _: case _:
raise ValueError("Unsupported mail type {}".format(mail_type)) raise ValueError("Unsupported mail type {}".format(mail_type))

@ -101,6 +101,8 @@ def _build_variable_from_mapping(*, mapping: Mapping[str, Any], selector: Sequen
result = ArrayNumberVariable.model_validate(mapping) result = ArrayNumberVariable.model_validate(mapping)
case SegmentType.ARRAY_OBJECT if isinstance(value, list): case SegmentType.ARRAY_OBJECT if isinstance(value, list):
result = ArrayObjectVariable.model_validate(mapping) result = ArrayObjectVariable.model_validate(mapping)
case SegmentType.ARRAY_FILE if isinstance(value, list):
result = ArrayFileVariable.model_validate(mapping)
case _: case _:
raise VariableError(f"not supported value type {value_type}") raise VariableError(f"not supported value type {value_type}")
if result.size > dify_config.MAX_VARIABLE_SIZE: if result.size > dify_config.MAX_VARIABLE_SIZE:

@ -19,7 +19,6 @@ workflow_run_for_log_fields = {
workflow_run_for_list_fields = { workflow_run_for_list_fields = {
"id": fields.String, "id": fields.String,
"sequence_number": fields.Integer,
"version": fields.String, "version": fields.String,
"status": fields.String, "status": fields.String,
"elapsed_time": fields.Float, "elapsed_time": fields.Float,
@ -36,7 +35,6 @@ advanced_chat_workflow_run_for_list_fields = {
"id": fields.String, "id": fields.String,
"conversation_id": fields.String, "conversation_id": fields.String,
"message_id": fields.String, "message_id": fields.String,
"sequence_number": fields.Integer,
"version": fields.String, "version": fields.String,
"status": fields.String, "status": fields.String,
"elapsed_time": fields.Float, "elapsed_time": fields.Float,
@ -63,7 +61,6 @@ workflow_run_pagination_fields = {
workflow_run_detail_fields = { workflow_run_detail_fields = {
"id": fields.String, "id": fields.String,
"sequence_number": fields.Integer,
"version": fields.String, "version": fields.String,
"graph": fields.Raw(attribute="graph_dict"), "graph": fields.Raw(attribute="graph_dict"),
"inputs": fields.Raw(attribute="inputs_dict"), "inputs": fields.Raw(attribute="inputs_dict"),

@ -0,0 +1,42 @@
import logging
import sendgrid # type: ignore
from python_http_client.exceptions import ForbiddenError, UnauthorizedError
from sendgrid.helpers.mail import Content, Email, Mail, To # type: ignore
class SendGridClient:
def __init__(self, sendgrid_api_key: str, _from: str):
self.sendgrid_api_key = sendgrid_api_key
self._from = _from
def send(self, mail: dict):
logging.debug("Sending email with SendGrid")
try:
_to = mail["to"]
if not _to:
raise ValueError("SendGridClient: Cannot send email: recipient address is missing.")
sg = sendgrid.SendGridAPIClient(api_key=self.sendgrid_api_key)
from_email = Email(self._from)
to_email = To(_to)
subject = mail["subject"]
content = Content("text/html", mail["html"])
mail = Mail(from_email, to_email, subject, content)
mail_json = mail.get() # type: ignore
response = sg.client.mail.send.post(request_body=mail_json)
logging.debug(response.status_code)
logging.debug(response.body)
logging.debug(response.headers)
except TimeoutError as e:
logging.exception("SendGridClient Timeout occurred while sending email")
raise
except (UnauthorizedError, ForbiddenError) as e:
logging.exception("SendGridClient Authentication failed. Verify that your credentials and the 'from")
raise
except Exception as e:
logging.exception(f"SendGridClient Unexpected error occurred while sending email to {_to}")
raise

@ -0,0 +1,66 @@
"""remove sequence_number from workflow_runs
Revision ID: 0ab65e1cc7fa
Revises: 4474872b0ee6
Create Date: 2025-06-19 16:33:13.377215
"""
from alembic import op
import models as models
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0ab65e1cc7fa'
down_revision = '4474872b0ee6'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('workflow_runs', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('workflow_run_tenant_app_sequence_idx'))
batch_op.drop_column('sequence_number')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
# WARNING: This downgrade CANNOT recover the original sequence_number values!
# The original sequence numbers are permanently lost after the upgrade.
# This downgrade will regenerate sequence numbers based on created_at order,
# which may result in different values than the original sequence numbers.
#
# If you need to preserve original sequence numbers, use the alternative
# migration approach that creates a backup table before removal.
# Step 1: Add sequence_number column as nullable first
with op.batch_alter_table('workflow_runs', schema=None) as batch_op:
batch_op.add_column(sa.Column('sequence_number', sa.INTEGER(), autoincrement=False, nullable=True))
# Step 2: Populate sequence_number values based on created_at order within each app
# NOTE: This recreates sequence numbering logic but values will be different
# from the original sequence numbers that were removed in the upgrade
connection = op.get_bind()
connection.execute(sa.text("""
UPDATE workflow_runs
SET sequence_number = subquery.row_num
FROM (
SELECT id, ROW_NUMBER() OVER (
PARTITION BY tenant_id, app_id
ORDER BY created_at, id
) as row_num
FROM workflow_runs
) subquery
WHERE workflow_runs.id = subquery.id
"""))
# Step 3: Make the column NOT NULL and add the index
with op.batch_alter_table('workflow_runs', schema=None) as batch_op:
batch_op.alter_column('sequence_number', nullable=False)
batch_op.create_index(batch_op.f('workflow_run_tenant_app_sequence_idx'), ['tenant_id', 'app_id', 'sequence_number'], unique=False)
# ### end Alembic commands ###

@ -10,7 +10,6 @@ from core.plugin.entities.plugin import GenericProviderID
from core.tools.entities.tool_entities import ToolProviderType from core.tools.entities.tool_entities import ToolProviderType
from core.tools.signature import sign_tool_file from core.tools.signature import sign_tool_file
from core.workflow.entities.workflow_execution import WorkflowExecutionStatus from core.workflow.entities.workflow_execution import WorkflowExecutionStatus
from services.plugin.plugin_service import PluginService
if TYPE_CHECKING: if TYPE_CHECKING:
from models.workflow import Workflow from models.workflow import Workflow
@ -169,6 +168,7 @@ class App(Base):
@property @property
def deleted_tools(self) -> list: def deleted_tools(self) -> list:
from core.tools.tool_manager import ToolManager from core.tools.tool_manager import ToolManager
from services.plugin.plugin_service import PluginService
# get agent mode tools # get agent mode tools
app_model_config = self.app_model_config app_model_config = self.app_model_config

@ -386,7 +386,7 @@ class WorkflowRun(Base):
- id (uuid) Run ID - id (uuid) Run ID
- tenant_id (uuid) Workspace ID - tenant_id (uuid) Workspace ID
- app_id (uuid) App ID - app_id (uuid) App ID
- sequence_number (int) Auto-increment sequence number, incremented within the App, starting from 1
- workflow_id (uuid) Workflow ID - workflow_id (uuid) Workflow ID
- type (string) Workflow type - type (string) Workflow type
- triggered_from (string) Trigger source - triggered_from (string) Trigger source
@ -419,13 +419,12 @@ class WorkflowRun(Base):
__table_args__ = ( __table_args__ = (
db.PrimaryKeyConstraint("id", name="workflow_run_pkey"), db.PrimaryKeyConstraint("id", name="workflow_run_pkey"),
db.Index("workflow_run_triggerd_from_idx", "tenant_id", "app_id", "triggered_from"), db.Index("workflow_run_triggerd_from_idx", "tenant_id", "app_id", "triggered_from"),
db.Index("workflow_run_tenant_app_sequence_idx", "tenant_id", "app_id", "sequence_number"),
) )
id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
tenant_id: Mapped[str] = mapped_column(StringUUID) tenant_id: Mapped[str] = mapped_column(StringUUID)
app_id: Mapped[str] = mapped_column(StringUUID) app_id: Mapped[str] = mapped_column(StringUUID)
sequence_number: Mapped[int] = mapped_column()
workflow_id: Mapped[str] = mapped_column(StringUUID) workflow_id: Mapped[str] = mapped_column(StringUUID)
type: Mapped[str] = mapped_column(db.String(255)) type: Mapped[str] = mapped_column(db.String(255))
triggered_from: Mapped[str] = mapped_column(db.String(255)) triggered_from: Mapped[str] = mapped_column(db.String(255))
@ -485,7 +484,6 @@ class WorkflowRun(Base):
"id": self.id, "id": self.id,
"tenant_id": self.tenant_id, "tenant_id": self.tenant_id,
"app_id": self.app_id, "app_id": self.app_id,
"sequence_number": self.sequence_number,
"workflow_id": self.workflow_id, "workflow_id": self.workflow_id,
"type": self.type, "type": self.type,
"triggered_from": self.triggered_from, "triggered_from": self.triggered_from,
@ -511,7 +509,6 @@ class WorkflowRun(Base):
id=data.get("id"), id=data.get("id"),
tenant_id=data.get("tenant_id"), tenant_id=data.get("tenant_id"),
app_id=data.get("app_id"), app_id=data.get("app_id"),
sequence_number=data.get("sequence_number"),
workflow_id=data.get("workflow_id"), workflow_id=data.get("workflow_id"),
type=data.get("type"), type=data.get("type"),
triggered_from=data.get("triggered_from"), triggered_from=data.get("triggered_from"),

@ -18,4 +18,3 @@ ignore_missing_imports=True
[mypy-flask_restful.inputs] [mypy-flask_restful.inputs]
ignore_missing_imports=True ignore_missing_imports=True

@ -81,6 +81,7 @@ dependencies = [
"weave~=0.51.0", "weave~=0.51.0",
"yarl~=1.18.3", "yarl~=1.18.3",
"webvtt-py~=0.5.1", "webvtt-py~=0.5.1",
"sendgrid~=6.12.3",
] ]
# Before adding new dependency, consider place it in # Before adding new dependency, consider place it in
# alphabet order (a-z) and suitable group. # alphabet order (a-z) and suitable group.
@ -202,4 +203,5 @@ vdb = [
"volcengine-compat~=1.0.0", "volcengine-compat~=1.0.0",
"weaviate-client~=3.24.0", "weaviate-client~=3.24.0",
"xinference-client~=1.2.2", "xinference-client~=1.2.2",
"mo-vector~=0.1.13",
] ]

@ -1402,16 +1402,16 @@ class DocumentService:
knowledge_config.embedding_model, # type: ignore knowledge_config.embedding_model, # type: ignore
) )
dataset_collection_binding_id = dataset_collection_binding.id dataset_collection_binding_id = dataset_collection_binding.id
if knowledge_config.retrieval_model: if knowledge_config.retrieval_model:
retrieval_model = knowledge_config.retrieval_model retrieval_model = knowledge_config.retrieval_model
else: else:
retrieval_model = RetrievalModel( retrieval_model = RetrievalModel(
search_method=RetrievalMethod.SEMANTIC_SEARCH.value, search_method=RetrievalMethod.SEMANTIC_SEARCH.value,
reranking_enable=False, reranking_enable=False,
reranking_model=RerankingModel(reranking_provider_name="", reranking_model_name=""), reranking_model=RerankingModel(reranking_provider_name="", reranking_model_name=""),
top_k=2, top_k=2,
score_threshold_enabled=False, score_threshold_enabled=False,
) )
# save dataset # save dataset
dataset = Dataset( dataset = Dataset(
tenant_id=tenant_id, tenant_id=tenant_id,

@ -101,7 +101,7 @@ class WeightModel(BaseModel):
class RetrievalModel(BaseModel): class RetrievalModel(BaseModel):
search_method: Literal["hybrid_search", "semantic_search", "full_text_search"] search_method: Literal["hybrid_search", "semantic_search", "full_text_search", "keyword_search"]
reranking_enable: bool reranking_enable: bool
reranking_model: Optional[RerankingModel] = None reranking_model: Optional[RerankingModel] = None
reranking_mode: Optional[str] = None reranking_mode: Optional[str] = None

@ -0,0 +1,5 @@
from services.errors.base import BaseServiceError
class PluginInstallationForbiddenError(BaseServiceError):
pass

@ -88,6 +88,26 @@ class WebAppAuthModel(BaseModel):
allow_email_password_login: bool = False allow_email_password_login: bool = False
class PluginInstallationScope(StrEnum):
NONE = "none"
OFFICIAL_ONLY = "official_only"
OFFICIAL_AND_SPECIFIC_PARTNERS = "official_and_specific_partners"
ALL = "all"
class PluginInstallationPermissionModel(BaseModel):
# Plugin installation scope possible values:
# none: prohibit all plugin installations
# official_only: allow only Dify official plugins
# official_and_specific_partners: allow official and specific partner plugins
# all: allow installation of all plugins
plugin_installation_scope: PluginInstallationScope = PluginInstallationScope.ALL
# If True, restrict plugin installation to the marketplace only
# Equivalent to ForceEnablePluginVerification
restrict_to_marketplace_only: bool = False
class FeatureModel(BaseModel): class FeatureModel(BaseModel):
billing: BillingModel = BillingModel() billing: BillingModel = BillingModel()
education: EducationModel = EducationModel() education: EducationModel = EducationModel()
@ -128,6 +148,7 @@ class SystemFeatureModel(BaseModel):
license: LicenseModel = LicenseModel() license: LicenseModel = LicenseModel()
branding: BrandingModel = BrandingModel() branding: BrandingModel = BrandingModel()
webapp_auth: WebAppAuthModel = WebAppAuthModel() webapp_auth: WebAppAuthModel = WebAppAuthModel()
plugin_installation_permission: PluginInstallationPermissionModel = PluginInstallationPermissionModel()
class FeatureService: class FeatureService:
@ -291,3 +312,12 @@ class FeatureService:
features.license.workspaces.enabled = license_info["workspaces"]["enabled"] features.license.workspaces.enabled = license_info["workspaces"]["enabled"]
features.license.workspaces.limit = license_info["workspaces"]["limit"] features.license.workspaces.limit = license_info["workspaces"]["limit"]
features.license.workspaces.size = license_info["workspaces"]["used"] features.license.workspaces.size = license_info["workspaces"]["used"]
if "PluginInstallationPermission" in enterprise_info:
plugin_installation_info = enterprise_info["PluginInstallationPermission"]
features.plugin_installation_permission.plugin_installation_scope = plugin_installation_info[
"pluginInstallationScope"
]
features.plugin_installation_permission.restrict_to_marketplace_only = plugin_installation_info[
"restrictToMarketplaceOnly"
]

@ -3,7 +3,7 @@ import logging
import click import click
from core.entities import DEFAULT_PLUGIN_ID from core.plugin.entities.plugin import GenericProviderID, ModelProviderID, ToolProviderID
from models.engine import db from models.engine import db
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -12,17 +12,17 @@ logger = logging.getLogger(__name__)
class PluginDataMigration: class PluginDataMigration:
@classmethod @classmethod
def migrate(cls) -> None: def migrate(cls) -> None:
cls.migrate_db_records("providers", "provider_name") # large table cls.migrate_db_records("providers", "provider_name", ModelProviderID) # large table
cls.migrate_db_records("provider_models", "provider_name") cls.migrate_db_records("provider_models", "provider_name", ModelProviderID)
cls.migrate_db_records("provider_orders", "provider_name") cls.migrate_db_records("provider_orders", "provider_name", ModelProviderID)
cls.migrate_db_records("tenant_default_models", "provider_name") cls.migrate_db_records("tenant_default_models", "provider_name", ModelProviderID)
cls.migrate_db_records("tenant_preferred_model_providers", "provider_name") cls.migrate_db_records("tenant_preferred_model_providers", "provider_name", ModelProviderID)
cls.migrate_db_records("provider_model_settings", "provider_name") cls.migrate_db_records("provider_model_settings", "provider_name", ModelProviderID)
cls.migrate_db_records("load_balancing_model_configs", "provider_name") cls.migrate_db_records("load_balancing_model_configs", "provider_name", ModelProviderID)
cls.migrate_datasets() cls.migrate_datasets()
cls.migrate_db_records("embeddings", "provider_name") # large table cls.migrate_db_records("embeddings", "provider_name", ModelProviderID) # large table
cls.migrate_db_records("dataset_collection_bindings", "provider_name") cls.migrate_db_records("dataset_collection_bindings", "provider_name", ModelProviderID)
cls.migrate_db_records("tool_builtin_providers", "provider") cls.migrate_db_records("tool_builtin_providers", "provider_name", ToolProviderID)
@classmethod @classmethod
def migrate_datasets(cls) -> None: def migrate_datasets(cls) -> None:
@ -66,9 +66,10 @@ limit 1000"""
fg="white", fg="white",
) )
) )
retrieval_model["reranking_model"]["reranking_provider_name"] = ( # update google to langgenius/gemini/google etc.
f"{DEFAULT_PLUGIN_ID}/{retrieval_model['reranking_model']['reranking_provider_name']}/{retrieval_model['reranking_model']['reranking_provider_name']}" retrieval_model["reranking_model"]["reranking_provider_name"] = ModelProviderID(
) retrieval_model["reranking_model"]["reranking_provider_name"]
).to_string()
retrieval_model_changed = True retrieval_model_changed = True
click.echo( click.echo(
@ -86,9 +87,11 @@ limit 1000"""
update_retrieval_model_sql = ", retrieval_model = :retrieval_model" update_retrieval_model_sql = ", retrieval_model = :retrieval_model"
params["retrieval_model"] = json.dumps(retrieval_model) params["retrieval_model"] = json.dumps(retrieval_model)
params["provider_name"] = ModelProviderID(provider_name).to_string()
sql = f"""update {table_name} sql = f"""update {table_name}
set {provider_column_name} = set {provider_column_name} =
concat('{DEFAULT_PLUGIN_ID}/', {provider_column_name}, '/', {provider_column_name}) :provider_name
{update_retrieval_model_sql} {update_retrieval_model_sql}
where id = :record_id""" where id = :record_id"""
conn.execute(db.text(sql), params) conn.execute(db.text(sql), params)
@ -122,7 +125,9 @@ limit 1000"""
) )
@classmethod @classmethod
def migrate_db_records(cls, table_name: str, provider_column_name: str) -> None: def migrate_db_records(
cls, table_name: str, provider_column_name: str, provider_cls: type[GenericProviderID]
) -> None:
click.echo(click.style(f"Migrating [{table_name}] data for plugin", fg="white")) click.echo(click.style(f"Migrating [{table_name}] data for plugin", fg="white"))
processed_count = 0 processed_count = 0
@ -166,7 +171,8 @@ limit 1000"""
) )
try: try:
updated_value = f"{DEFAULT_PLUGIN_ID}/{provider_name}/{provider_name}" # update jina to langgenius/jina_tool/jina etc.
updated_value = provider_cls(provider_name).to_string()
batch_updates.append((updated_value, record_id)) batch_updates.append((updated_value, record_id))
except Exception as e: except Exception as e:
failed_ids.append(record_id) failed_ids.append(record_id)

@ -17,11 +17,18 @@ from core.plugin.entities.plugin import (
PluginInstallation, PluginInstallation,
PluginInstallationSource, PluginInstallationSource,
) )
from core.plugin.entities.plugin_daemon import PluginInstallTask, PluginListResponse, PluginUploadResponse from core.plugin.entities.plugin_daemon import (
PluginDecodeResponse,
PluginInstallTask,
PluginListResponse,
PluginVerification,
)
from core.plugin.impl.asset import PluginAssetManager from core.plugin.impl.asset import PluginAssetManager
from core.plugin.impl.debugging import PluginDebuggingClient from core.plugin.impl.debugging import PluginDebuggingClient
from core.plugin.impl.plugin import PluginInstaller from core.plugin.impl.plugin import PluginInstaller
from extensions.ext_redis import redis_client from extensions.ext_redis import redis_client
from services.errors.plugin import PluginInstallationForbiddenError
from services.feature_service import FeatureService, PluginInstallationScope
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -86,6 +93,42 @@ class PluginService:
logger.exception("failed to fetch latest plugin version") logger.exception("failed to fetch latest plugin version")
return result return result
@staticmethod
def _check_marketplace_only_permission():
"""
Check if the marketplace only permission is enabled
"""
features = FeatureService.get_system_features()
if features.plugin_installation_permission.restrict_to_marketplace_only:
raise PluginInstallationForbiddenError("Plugin installation is restricted to marketplace only")
@staticmethod
def _check_plugin_installation_scope(plugin_verification: Optional[PluginVerification]):
"""
Check the plugin installation scope
"""
features = FeatureService.get_system_features()
match features.plugin_installation_permission.plugin_installation_scope:
case PluginInstallationScope.OFFICIAL_ONLY:
if (
plugin_verification is None
or plugin_verification.authorized_category != PluginVerification.AuthorizedCategory.Langgenius
):
raise PluginInstallationForbiddenError("Plugin installation is restricted to official only")
case PluginInstallationScope.OFFICIAL_AND_SPECIFIC_PARTNERS:
if plugin_verification is None or plugin_verification.authorized_category not in [
PluginVerification.AuthorizedCategory.Langgenius,
PluginVerification.AuthorizedCategory.Partner,
]:
raise PluginInstallationForbiddenError(
"Plugin installation is restricted to official and specific partners"
)
case PluginInstallationScope.NONE:
raise PluginInstallationForbiddenError("Installing plugins is not allowed")
case PluginInstallationScope.ALL:
pass
@staticmethod @staticmethod
def get_debugging_key(tenant_id: str) -> str: def get_debugging_key(tenant_id: str) -> str:
""" """
@ -208,6 +251,8 @@ class PluginService:
# check if plugin pkg is already downloaded # check if plugin pkg is already downloaded
manager = PluginInstaller() manager = PluginInstaller()
features = FeatureService.get_system_features()
try: try:
manager.fetch_plugin_manifest(tenant_id, new_plugin_unique_identifier) manager.fetch_plugin_manifest(tenant_id, new_plugin_unique_identifier)
# already downloaded, skip, and record install event # already downloaded, skip, and record install event
@ -215,7 +260,14 @@ class PluginService:
except Exception: except Exception:
# plugin not installed, download and upload pkg # plugin not installed, download and upload pkg
pkg = download_plugin_pkg(new_plugin_unique_identifier) pkg = download_plugin_pkg(new_plugin_unique_identifier)
manager.upload_pkg(tenant_id, pkg, verify_signature=False) response = manager.upload_pkg(
tenant_id,
pkg,
verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only,
)
# check if the plugin is available to install
PluginService._check_plugin_installation_scope(response.verification)
return manager.upgrade_plugin( return manager.upgrade_plugin(
tenant_id, tenant_id,
@ -239,6 +291,7 @@ class PluginService:
""" """
Upgrade plugin with github Upgrade plugin with github
""" """
PluginService._check_marketplace_only_permission()
manager = PluginInstaller() manager = PluginInstaller()
return manager.upgrade_plugin( return manager.upgrade_plugin(
tenant_id, tenant_id,
@ -253,33 +306,43 @@ class PluginService:
) )
@staticmethod @staticmethod
def upload_pkg(tenant_id: str, pkg: bytes, verify_signature: bool = False) -> PluginUploadResponse: def upload_pkg(tenant_id: str, pkg: bytes, verify_signature: bool = False) -> PluginDecodeResponse:
""" """
Upload plugin package files Upload plugin package files
returns: plugin_unique_identifier returns: plugin_unique_identifier
""" """
PluginService._check_marketplace_only_permission()
manager = PluginInstaller() manager = PluginInstaller()
return manager.upload_pkg(tenant_id, pkg, verify_signature) features = FeatureService.get_system_features()
response = manager.upload_pkg(
tenant_id,
pkg,
verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only,
)
return response
@staticmethod @staticmethod
def upload_pkg_from_github( def upload_pkg_from_github(
tenant_id: str, repo: str, version: str, package: str, verify_signature: bool = False tenant_id: str, repo: str, version: str, package: str, verify_signature: bool = False
) -> PluginUploadResponse: ) -> PluginDecodeResponse:
""" """
Install plugin from github release package files, Install plugin from github release package files,
returns plugin_unique_identifier returns plugin_unique_identifier
""" """
PluginService._check_marketplace_only_permission()
pkg = download_with_size_limit( pkg = download_with_size_limit(
f"https://github.com/{repo}/releases/download/{version}/{package}", dify_config.PLUGIN_MAX_PACKAGE_SIZE f"https://github.com/{repo}/releases/download/{version}/{package}", dify_config.PLUGIN_MAX_PACKAGE_SIZE
) )
features = FeatureService.get_system_features()
manager = PluginInstaller() manager = PluginInstaller()
return manager.upload_pkg( response = manager.upload_pkg(
tenant_id, tenant_id,
pkg, pkg,
verify_signature, verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only,
) )
return response
@staticmethod @staticmethod
def upload_bundle( def upload_bundle(
@ -289,11 +352,15 @@ class PluginService:
Upload a plugin bundle and return the dependencies. Upload a plugin bundle and return the dependencies.
""" """
manager = PluginInstaller() manager = PluginInstaller()
PluginService._check_marketplace_only_permission()
return manager.upload_bundle(tenant_id, bundle, verify_signature) return manager.upload_bundle(tenant_id, bundle, verify_signature)
@staticmethod @staticmethod
def install_from_local_pkg(tenant_id: str, plugin_unique_identifiers: Sequence[str]): def install_from_local_pkg(tenant_id: str, plugin_unique_identifiers: Sequence[str]):
PluginService._check_marketplace_only_permission()
manager = PluginInstaller() manager = PluginInstaller()
return manager.install_from_identifiers( return manager.install_from_identifiers(
tenant_id, tenant_id,
plugin_unique_identifiers, plugin_unique_identifiers,
@ -307,6 +374,8 @@ class PluginService:
Install plugin from github release package files, Install plugin from github release package files,
returns plugin_unique_identifier returns plugin_unique_identifier
""" """
PluginService._check_marketplace_only_permission()
manager = PluginInstaller() manager = PluginInstaller()
return manager.install_from_identifiers( return manager.install_from_identifiers(
tenant_id, tenant_id,
@ -322,28 +391,33 @@ class PluginService:
) )
@staticmethod @staticmethod
def fetch_marketplace_pkg( def fetch_marketplace_pkg(tenant_id: str, plugin_unique_identifier: str) -> PluginDeclaration:
tenant_id: str, plugin_unique_identifier: str, verify_signature: bool = False
) -> PluginDeclaration:
""" """
Fetch marketplace package Fetch marketplace package
""" """
if not dify_config.MARKETPLACE_ENABLED: if not dify_config.MARKETPLACE_ENABLED:
raise ValueError("marketplace is not enabled") raise ValueError("marketplace is not enabled")
features = FeatureService.get_system_features()
manager = PluginInstaller() manager = PluginInstaller()
try: try:
declaration = manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier) declaration = manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier)
except Exception: except Exception:
pkg = download_plugin_pkg(plugin_unique_identifier) pkg = download_plugin_pkg(plugin_unique_identifier)
declaration = manager.upload_pkg(tenant_id, pkg, verify_signature).manifest response = manager.upload_pkg(
tenant_id,
pkg,
verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only,
)
# check if the plugin is available to install
PluginService._check_plugin_installation_scope(response.verification)
declaration = response.manifest
return declaration return declaration
@staticmethod @staticmethod
def install_from_marketplace_pkg( def install_from_marketplace_pkg(tenant_id: str, plugin_unique_identifiers: Sequence[str]):
tenant_id: str, plugin_unique_identifiers: Sequence[str], verify_signature: bool = False
):
""" """
Install plugin from marketplace package files, Install plugin from marketplace package files,
returns installation task id returns installation task id
@ -353,15 +427,26 @@ class PluginService:
manager = PluginInstaller() manager = PluginInstaller()
features = FeatureService.get_system_features()
# check if already downloaded # check if already downloaded
for plugin_unique_identifier in plugin_unique_identifiers: for plugin_unique_identifier in plugin_unique_identifiers:
try: try:
manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier) manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier)
plugin_decode_response = manager.decode_plugin_from_identifier(tenant_id, plugin_unique_identifier)
# check if the plugin is available to install
PluginService._check_plugin_installation_scope(plugin_decode_response.verification)
# already downloaded, skip # already downloaded, skip
except Exception: except Exception:
# plugin not installed, download and upload pkg # plugin not installed, download and upload pkg
pkg = download_plugin_pkg(plugin_unique_identifier) pkg = download_plugin_pkg(plugin_unique_identifier)
manager.upload_pkg(tenant_id, pkg, verify_signature) response = manager.upload_pkg(
tenant_id,
pkg,
verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only,
)
# check if the plugin is available to install
PluginService._check_plugin_installation_scope(response.verification)
return manager.install_from_identifiers( return manager.install_from_identifiers(
tenant_id, tenant_id,

@ -5,7 +5,7 @@ from sqlalchemy import and_, func, or_, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from core.workflow.entities.workflow_execution import WorkflowExecutionStatus from core.workflow.entities.workflow_execution import WorkflowExecutionStatus
from models import App, EndUser, WorkflowAppLog, WorkflowRun from models import Account, App, EndUser, WorkflowAppLog, WorkflowRun
from models.enums import CreatorUserRole from models.enums import CreatorUserRole
@ -21,6 +21,8 @@ class WorkflowAppService:
created_at_after: datetime | None = None, created_at_after: datetime | None = None,
page: int = 1, page: int = 1,
limit: int = 20, limit: int = 20,
created_by_end_user_session_id: str | None = None,
created_by_account: str | None = None,
) -> dict: ) -> dict:
""" """
Get paginate workflow app logs using SQLAlchemy 2.0 style Get paginate workflow app logs using SQLAlchemy 2.0 style
@ -32,6 +34,8 @@ class WorkflowAppService:
:param created_at_after: filter logs created after this timestamp :param created_at_after: filter logs created after this timestamp
:param page: page number :param page: page number
:param limit: items per page :param limit: items per page
:param created_by_end_user_session_id: filter by end user session id
:param created_by_account: filter by account email
:return: Pagination object :return: Pagination object
""" """
# Build base statement using SQLAlchemy 2.0 style # Build base statement using SQLAlchemy 2.0 style
@ -71,6 +75,26 @@ class WorkflowAppService:
if created_at_after: if created_at_after:
stmt = stmt.where(WorkflowAppLog.created_at >= created_at_after) stmt = stmt.where(WorkflowAppLog.created_at >= created_at_after)
# Filter by end user session id or account email
if created_by_end_user_session_id:
stmt = stmt.join(
EndUser,
and_(
WorkflowAppLog.created_by == EndUser.id,
WorkflowAppLog.created_by_role == CreatorUserRole.END_USER,
EndUser.session_id == created_by_end_user_session_id,
),
)
if created_by_account:
stmt = stmt.join(
Account,
and_(
WorkflowAppLog.created_by == Account.id,
WorkflowAppLog.created_by_role == CreatorUserRole.ACCOUNT,
Account.email == created_by_account,
),
)
stmt = stmt.order_by(WorkflowAppLog.created_at.desc()) stmt = stmt.order_by(WorkflowAppLog.created_at.desc())
# Get total count using the same filters # Get total count using the same filters

@ -0,0 +1,25 @@
from core.rag.datasource.vdb.matrixone.matrixone_vector import MatrixoneConfig, MatrixoneVector
from tests.integration_tests.vdb.test_vector_store import (
AbstractVectorTest,
get_example_text,
setup_mock_redis,
)
class MatrixoneVectorTest(AbstractVectorTest):
def __init__(self):
super().__init__()
self.vector = MatrixoneVector(
collection_name=self.collection_name,
config=MatrixoneConfig(
host="localhost", port=6001, user="dump", password="111", database="dify", metric="l2"
),
)
def get_ids_by_metadata_field(self):
ids = self.vector.get_ids_by_metadata_field(key="document_id", value=self.example_doc_id)
assert len(ids) == 1
def test_matrixone_vector(setup_mock_redis):
MatrixoneVectorTest().run_all_tests()

@ -163,7 +163,6 @@ def real_workflow_run():
workflow_run.tenant_id = "test-tenant-id" workflow_run.tenant_id = "test-tenant-id"
workflow_run.app_id = "test-app-id" workflow_run.app_id = "test-app-id"
workflow_run.workflow_id = "test-workflow-id" workflow_run.workflow_id = "test-workflow-id"
workflow_run.sequence_number = 1
workflow_run.type = "chat" workflow_run.type = "chat"
workflow_run.triggered_from = "app-run" workflow_run.triggered_from = "app-run"
workflow_run.version = "1.0" workflow_run.version = "1.0"

File diff suppressed because it is too large Load Diff

@ -399,7 +399,7 @@ SUPABASE_URL=your-server-url
# ------------------------------ # ------------------------------
# The type of vector store to use. # The type of vector store to use.
# Supported values are `weaviate`, `qdrant`, `milvus`, `myscale`, `relyt`, `pgvector`, `pgvecto-rs`, `chroma`, `opensearch`, `oracle`, `tencent`, `elasticsearch`, `elasticsearch-ja`, `analyticdb`, `couchbase`, `vikingdb`, `oceanbase`, `opengauss`, `tablestore`,`vastbase`,`tidb`,`tidb_on_qdrant`,`baidu`,`lindorm`,`huawei_cloud`,`upstash`. # Supported values are `weaviate`, `qdrant`, `milvus`, `myscale`, `relyt`, `pgvector`, `pgvecto-rs`, `chroma`, `opensearch`, `oracle`, `tencent`, `elasticsearch`, `elasticsearch-ja`, `analyticdb`, `couchbase`, `vikingdb`, `oceanbase`, `opengauss`, `tablestore`,`vastbase`,`tidb`,`tidb_on_qdrant`,`baidu`,`lindorm`,`huawei_cloud`,`upstash`, `matrixone`.
VECTOR_STORE=weaviate VECTOR_STORE=weaviate
# The Weaviate endpoint URL. Only available when VECTOR_STORE is `weaviate`. # The Weaviate endpoint URL. Only available when VECTOR_STORE is `weaviate`.
@ -490,6 +490,13 @@ TIDB_VECTOR_USER=
TIDB_VECTOR_PASSWORD= TIDB_VECTOR_PASSWORD=
TIDB_VECTOR_DATABASE=dify TIDB_VECTOR_DATABASE=dify
# Matrixone vector configurations.
MATRIXONE_HOST=matrixone
MATRIXONE_PORT=6001
MATRIXONE_USER=dump
MATRIXONE_PASSWORD=111
MATRIXONE_DATABASE=dify
# Tidb on qdrant configuration, only available when VECTOR_STORE is `tidb_on_qdrant` # Tidb on qdrant configuration, only available when VECTOR_STORE is `tidb_on_qdrant`
TIDB_ON_QDRANT_URL=http://127.0.0.1 TIDB_ON_QDRANT_URL=http://127.0.0.1
TIDB_ON_QDRANT_API_KEY=dify TIDB_ON_QDRANT_API_KEY=dify
@ -719,10 +726,11 @@ NOTION_INTERNAL_SECRET=
# Mail related configuration # Mail related configuration
# ------------------------------ # ------------------------------
# Mail type, support: resend, smtp # Mail type, support: resend, smtp, sendgrid
MAIL_TYPE=resend MAIL_TYPE=resend
# Default send from email address, if not specified # Default send from email address, if not specified
# If using SendGrid, use the 'from' field for authentication if necessary.
MAIL_DEFAULT_SEND_FROM= MAIL_DEFAULT_SEND_FROM=
# API-Key for the Resend email provider, used when MAIL_TYPE is `resend`. # API-Key for the Resend email provider, used when MAIL_TYPE is `resend`.
@ -738,6 +746,9 @@ SMTP_PASSWORD=
SMTP_USE_TLS=true SMTP_USE_TLS=true
SMTP_OPPORTUNISTIC_TLS=false SMTP_OPPORTUNISTIC_TLS=false
# Sendgid configuration
SENDGRID_API_KEY=
# ------------------------------ # ------------------------------
# Others Configuration # Others Configuration
# ------------------------------ # ------------------------------
@ -815,7 +826,8 @@ TEXT_GENERATION_TIMEOUT_MS=60000
# Environment Variables for db Service # Environment Variables for db Service
# ------------------------------ # ------------------------------
PGUSER=${DB_USERNAME} # The name of the default postgres user.
POSTGRES_USER=${DB_USERNAME}
# The password for the default postgres user. # The password for the default postgres user.
POSTGRES_PASSWORD=${DB_PASSWORD} POSTGRES_PASSWORD=${DB_PASSWORD}
# The name of the default postgres database. # The name of the default postgres database.

@ -84,7 +84,7 @@ services:
image: postgres:15-alpine image: postgres:15-alpine
restart: always restart: always
environment: environment:
PGUSER: ${PGUSER:-postgres} POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-difyai123456} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-difyai123456}
POSTGRES_DB: ${POSTGRES_DB:-dify} POSTGRES_DB: ${POSTGRES_DB:-dify}
PGDATA: ${PGDATA:-/var/lib/postgresql/data/pgdata} PGDATA: ${PGDATA:-/var/lib/postgresql/data/pgdata}
@ -617,6 +617,18 @@ services:
ports: ports:
- ${MYSCALE_PORT:-8123}:${MYSCALE_PORT:-8123} - ${MYSCALE_PORT:-8123}:${MYSCALE_PORT:-8123}
# Matrixone vector store.
matrixone:
hostname: matrixone
image: matrixorigin/matrixone:2.1.1
profiles:
- matrixone
restart: always
volumes:
- ./volumes/matrixone/data:/mo-data
ports:
- ${MATRIXONE_PORT:-6001}:${MATRIXONE_PORT:-6001}
# https://www.elastic.co/guide/en/elasticsearch/reference/current/settings.html # https://www.elastic.co/guide/en/elasticsearch/reference/current/settings.html
# https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html#docker-prod-prerequisites # https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html#docker-prod-prerequisites
elasticsearch: elasticsearch:

@ -195,6 +195,11 @@ x-shared-env: &shared-api-worker-env
TIDB_VECTOR_USER: ${TIDB_VECTOR_USER:-} TIDB_VECTOR_USER: ${TIDB_VECTOR_USER:-}
TIDB_VECTOR_PASSWORD: ${TIDB_VECTOR_PASSWORD:-} TIDB_VECTOR_PASSWORD: ${TIDB_VECTOR_PASSWORD:-}
TIDB_VECTOR_DATABASE: ${TIDB_VECTOR_DATABASE:-dify} TIDB_VECTOR_DATABASE: ${TIDB_VECTOR_DATABASE:-dify}
MATRIXONE_HOST: ${MATRIXONE_HOST:-matrixone}
MATRIXONE_PORT: ${MATRIXONE_PORT:-6001}
MATRIXONE_USER: ${MATRIXONE_USER:-dump}
MATRIXONE_PASSWORD: ${MATRIXONE_PASSWORD:-111}
MATRIXONE_DATABASE: ${MATRIXONE_DATABASE:-dify}
TIDB_ON_QDRANT_URL: ${TIDB_ON_QDRANT_URL:-http://127.0.0.1} TIDB_ON_QDRANT_URL: ${TIDB_ON_QDRANT_URL:-http://127.0.0.1}
TIDB_ON_QDRANT_API_KEY: ${TIDB_ON_QDRANT_API_KEY:-dify} TIDB_ON_QDRANT_API_KEY: ${TIDB_ON_QDRANT_API_KEY:-dify}
TIDB_ON_QDRANT_CLIENT_TIMEOUT: ${TIDB_ON_QDRANT_CLIENT_TIMEOUT:-20} TIDB_ON_QDRANT_CLIENT_TIMEOUT: ${TIDB_ON_QDRANT_CLIENT_TIMEOUT:-20}
@ -322,6 +327,7 @@ x-shared-env: &shared-api-worker-env
SMTP_PASSWORD: ${SMTP_PASSWORD:-} SMTP_PASSWORD: ${SMTP_PASSWORD:-}
SMTP_USE_TLS: ${SMTP_USE_TLS:-true} SMTP_USE_TLS: ${SMTP_USE_TLS:-true}
SMTP_OPPORTUNISTIC_TLS: ${SMTP_OPPORTUNISTIC_TLS:-false} SMTP_OPPORTUNISTIC_TLS: ${SMTP_OPPORTUNISTIC_TLS:-false}
SENDGRID_API_KEY: ${SENDGRID_API_KEY:-}
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-4000} INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-4000}
INVITE_EXPIRY_HOURS: ${INVITE_EXPIRY_HOURS:-72} INVITE_EXPIRY_HOURS: ${INVITE_EXPIRY_HOURS:-72}
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES: ${RESET_PASSWORD_TOKEN_EXPIRY_MINUTES:-5} RESET_PASSWORD_TOKEN_EXPIRY_MINUTES: ${RESET_PASSWORD_TOKEN_EXPIRY_MINUTES:-5}
@ -356,7 +362,7 @@ x-shared-env: &shared-api-worker-env
MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10} MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10}
MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-99} MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-99}
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000} TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}
PGUSER: ${PGUSER:-${DB_USERNAME}} POSTGRES_USER: ${POSTGRES_USER:-${DB_USERNAME}}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-${DB_PASSWORD}} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-${DB_PASSWORD}}
POSTGRES_DB: ${POSTGRES_DB:-${DB_DATABASE}} POSTGRES_DB: ${POSTGRES_DB:-${DB_DATABASE}}
PGDATA: ${PGDATA:-/var/lib/postgresql/data/pgdata} PGDATA: ${PGDATA:-/var/lib/postgresql/data/pgdata}
@ -591,7 +597,7 @@ services:
image: postgres:15-alpine image: postgres:15-alpine
restart: always restart: always
environment: environment:
PGUSER: ${PGUSER:-postgres} POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-difyai123456} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-difyai123456}
POSTGRES_DB: ${POSTGRES_DB:-dify} POSTGRES_DB: ${POSTGRES_DB:-dify}
PGDATA: ${PGDATA:-/var/lib/postgresql/data/pgdata} PGDATA: ${PGDATA:-/var/lib/postgresql/data/pgdata}
@ -1124,6 +1130,18 @@ services:
ports: ports:
- ${MYSCALE_PORT:-8123}:${MYSCALE_PORT:-8123} - ${MYSCALE_PORT:-8123}:${MYSCALE_PORT:-8123}
# Matrixone vector store.
matrixone:
hostname: matrixone
image: matrixorigin/matrixone:2.1.1
profiles:
- matrixone
restart: always
volumes:
- ./volumes/matrixone/data:/mo-data
ports:
- ${MATRIXONE_PORT:-6001}:${MATRIXONE_PORT:-6001}
# https://www.elastic.co/guide/en/elasticsearch/reference/current/settings.html # https://www.elastic.co/guide/en/elasticsearch/reference/current/settings.html
# https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html#docker-prod-prerequisites # https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html#docker-prod-prerequisites
elasticsearch: elasticsearch:

@ -1,7 +1,7 @@
# ------------------------------ # ------------------------------
# Environment Variables for db Service # Environment Variables for db Service
# ------------------------------ # ------------------------------
PGUSER=postgres POSTGRES_USER=postgres
# The password for the default postgres user. # The password for the default postgres user.
POSTGRES_PASSWORD=difyai123456 POSTGRES_PASSWORD=difyai123456
# The name of the default postgres database. # The name of the default postgres database.

@ -81,7 +81,7 @@ const Datasets = ({
currentContainer?.removeEventListener('scroll', onScroll) currentContainer?.removeEventListener('scroll', onScroll)
onScroll.cancel() onScroll.cancel()
} }
}, [onScroll]) }, [containerRef, onScroll])
return ( return (
<nav className='grid shrink-0 grow grid-cols-1 content-start gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'> <nav className='grid shrink-0 grow grid-cols-1 content-start gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'>

@ -5,34 +5,34 @@ import {
RiAddLine, RiAddLine,
RiArrowRightLine, RiArrowRightLine,
} from '@remixicon/react' } from '@remixicon/react'
import Link from 'next/link'
const CreateAppCard = ( type CreateAppCardProps = {
{ ref?: React.Ref<HTMLAnchorElement>
ref, }
..._
}, const CreateAppCard = ({ ref }: CreateAppCardProps) => {
) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className='bg-background-default-dimm flex min-h-[160px] flex-col rounded-xl border-[0.5px] <div className='bg-background-default-dimm flex min-h-[160px] flex-col rounded-xl border-[0.5px]
border-components-panel-border transition-all duration-200 ease-in-out' border-components-panel-border transition-all duration-200 ease-in-out'
> >
<a ref={ref} className='group flex grow cursor-pointer items-start p-4' href={`${basePath}/datasets/create`}> <Link ref={ref} className='group flex grow cursor-pointer items-start p-4' href={`${basePath}/datasets/create`}>
<div className='flex items-center gap-3'> <div className='flex items-center gap-3'>
<div className='flex h-10 w-10 items-center justify-center rounded-lg border border-dashed border-divider-regular bg-background-default-lighter <div className='flex h-10 w-10 items-center justify-center rounded-lg border border-dashed border-divider-regular bg-background-default-lighter
p-2 group-hover:border-solid group-hover:border-effects-highlight group-hover:bg-background-default-dodge' p-2 group-hover:border-solid group-hover:border-effects-highlight group-hover:bg-background-default-dodge'
> >
<RiAddLine className='h-4 w-4 text-text-tertiary group-hover:text-text-accent'/> <RiAddLine className='h-4 w-4 text-text-tertiary group-hover:text-text-accent' />
</div> </div>
<div className='system-md-semibold text-text-secondary group-hover:text-text-accent'>{t('dataset.createDataset')}</div> <div className='system-md-semibold text-text-secondary group-hover:text-text-accent'>{t('dataset.createDataset')}</div>
</div> </div>
</a> </Link>
<div className='system-xs-regular p-4 pt-0 text-text-tertiary'>{t('dataset.createDatasetIntro')}</div> <div className='system-xs-regular p-4 pt-0 text-text-tertiary'>{t('dataset.createDatasetIntro')}</div>
<a className='group flex cursor-pointer items-center gap-1 rounded-b-xl border-t-[0.5px] border-divider-subtle p-4' href={`${basePath}/datasets/connect`}> <Link className='group flex cursor-pointer items-center gap-1 rounded-b-xl border-t-[0.5px] border-divider-subtle p-4' href={`${basePath}/datasets/connect`}>
<div className='system-xs-medium text-text-tertiary group-hover:text-text-accent'>{t('dataset.connectDataset')}</div> <div className='system-xs-medium text-text-tertiary group-hover:text-text-accent'>{t('dataset.connectDataset')}</div>
<RiArrowRightLine className='h-3.5 w-3.5 text-text-tertiary group-hover:text-text-accent' /> <RiArrowRightLine className='h-3.5 w-3.5 text-text-tertiary group-hover:text-text-accent' />
</a> </Link>
</div> </div>
) )
} }

@ -8,15 +8,17 @@ import { useRouter } from 'next/navigation'
import { useEffect } from 'react' import { useEffect } from 'react'
export default function DatasetsLayout({ children }: { children: React.ReactNode }) { export default function DatasetsLayout({ children }: { children: React.ReactNode }) {
const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext() const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, currentWorkspace, isLoadingCurrentWorkspace } = useAppContext()
const router = useRouter() const router = useRouter()
useEffect(() => { useEffect(() => {
if (!isCurrentWorkspaceEditor && !isCurrentWorkspaceDatasetOperator) if (isLoadingCurrentWorkspace || !currentWorkspace.id)
return
if (!(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator))
router.replace('/apps') router.replace('/apps')
}, [isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, router]) }, [isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, isLoadingCurrentWorkspace, currentWorkspace, router])
if (!isCurrentWorkspaceEditor && !isCurrentWorkspaceDatasetOperator) if (isLoadingCurrentWorkspace || !(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator))
return <Loading type='app' /> return <Loading type='app' />
return ( return (
<ExternalKnowledgeApiProvider> <ExternalKnowledgeApiProvider>

@ -314,10 +314,10 @@ const AppPublisher = ({
{!isAppAccessSet && <p className='system-xs-regular mt-1 text-text-warning'>{t('app.publishApp.notSetDesc')}</p>} {!isAppAccessSet && <p className='system-xs-regular mt-1 text-text-warning'>{t('app.publishApp.notSetDesc')}</p>}
</div>} </div>}
<div className='flex flex-col gap-y-1 border-t-[0.5px] border-t-divider-regular p-4 pt-3'> <div className='flex flex-col gap-y-1 border-t-[0.5px] border-t-divider-regular p-4 pt-3'>
<Tooltip triggerClassName='flex' disabled={!systemFeatures.webapp_auth.enabled || userCanAccessApp?.result} popupContent={t('app.noAccessPermission')} asChild={false}> <Tooltip triggerClassName='flex' disabled={!systemFeatures.webapp_auth.enabled || appDetail?.access_mode === AccessMode.EXTERNAL_MEMBERS || userCanAccessApp?.result} popupContent={t('app.noAccessPermission')} asChild={false}>
<SuggestedAction <SuggestedAction
className='flex-1' className='flex-1'
disabled={!publishedAt || (systemFeatures.webapp_auth.enabled && !userCanAccessApp?.result)} disabled={!publishedAt || (systemFeatures.webapp_auth.enabled && appDetail?.access_mode !== AccessMode.EXTERNAL_MEMBERS && !userCanAccessApp?.result)}
link={appURL} link={appURL}
icon={<RiPlayCircleLine className='h-4 w-4' />} icon={<RiPlayCircleLine className='h-4 w-4' />}
> >
@ -326,10 +326,10 @@ const AppPublisher = ({
</Tooltip> </Tooltip>
{appDetail?.mode === 'workflow' || appDetail?.mode === 'completion' {appDetail?.mode === 'workflow' || appDetail?.mode === 'completion'
? ( ? (
<Tooltip triggerClassName='flex' disabled={!systemFeatures.webapp_auth.enabled || userCanAccessApp?.result} popupContent={t('app.noAccessPermission')} asChild={false}> <Tooltip triggerClassName='flex' disabled={!systemFeatures.webapp_auth.enabled || appDetail.access_mode === AccessMode.EXTERNAL_MEMBERS || userCanAccessApp?.result} popupContent={t('app.noAccessPermission')} asChild={false}>
<SuggestedAction <SuggestedAction
className='flex-1' className='flex-1'
disabled={!publishedAt || (systemFeatures.webapp_auth.enabled && !userCanAccessApp?.result)} disabled={!publishedAt || (systemFeatures.webapp_auth.enabled && appDetail.access_mode !== AccessMode.EXTERNAL_MEMBERS && !userCanAccessApp?.result)}
link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`} link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`}
icon={<RiPlayList2Line className='h-4 w-4' />} icon={<RiPlayList2Line className='h-4 w-4' />}
> >

@ -1,4 +1,4 @@
import { memo, useEffect, useMemo, useRef, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import ReactEcharts from 'echarts-for-react' import ReactEcharts from 'echarts-for-react'
import SyntaxHighlighter from 'react-syntax-highlighter' import SyntaxHighlighter from 'react-syntax-highlighter'
import { import {
@ -62,6 +62,17 @@ const getCorrectCapitalizationLanguageName = (language: string) => {
// visit https://reactjs.org/docs/error-decoder.html?invariant=185 for the full message // visit https://reactjs.org/docs/error-decoder.html?invariant=185 for the full message
// or use the non-minified dev environment for full errors and additional helpful warnings. // or use the non-minified dev environment for full errors and additional helpful warnings.
// Define ECharts event parameter types
interface EChartsEventParams {
type: string;
seriesIndex?: number;
dataIndex?: number;
name?: string;
value?: any;
currentIndex?: number; // Added for timeline events
[key: string]: any;
}
const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any) => { const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any) => {
const { theme } = useTheme() const { theme } = useTheme()
const [isSVG, setIsSVG] = useState(true) const [isSVG, setIsSVG] = useState(true)
@ -70,6 +81,11 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
const echartsRef = useRef<any>(null) const echartsRef = useRef<any>(null)
const contentRef = useRef<string>('') const contentRef = useRef<string>('')
const processedRef = useRef<boolean>(false) // Track if content was successfully processed const processedRef = useRef<boolean>(false) // Track if content was successfully processed
const instanceIdRef = useRef<string>(`chart-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`) // Unique ID for logging
const isInitialRenderRef = useRef<boolean>(true) // Track if this is initial render
const chartInstanceRef = useRef<any>(null) // Direct reference to ECharts instance
const resizeTimerRef = useRef<NodeJS.Timeout | null>(null) // For debounce handling
const finishedEventCountRef = useRef<number>(0) // Track finished event trigger count
const match = /language-(\w+)/.exec(className || '') const match = /language-(\w+)/.exec(className || '')
const language = match?.[1] const language = match?.[1]
const languageShowName = getCorrectCapitalizationLanguageName(language || '') const languageShowName = getCorrectCapitalizationLanguageName(language || '')
@ -85,36 +101,64 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
width: 'auto', width: 'auto',
}) as any, []) }) as any, [])
const echartsOnEvents = useMemo(() => ({ // Debounce resize operations
finished: () => { const debouncedResize = useCallback(() => {
const instance = echartsRef.current?.getEchartsInstance?.() if (resizeTimerRef.current)
if (instance) clearTimeout(resizeTimerRef.current)
instance.resize()
resizeTimerRef.current = setTimeout(() => {
if (chartInstanceRef.current)
chartInstanceRef.current.resize()
resizeTimerRef.current = null
}, 200)
}, [])
// Handle ECharts instance initialization
const handleChartReady = useCallback((instance: any) => {
chartInstanceRef.current = instance
// Force resize to ensure timeline displays correctly
setTimeout(() => {
if (chartInstanceRef.current)
chartInstanceRef.current.resize()
}, 200)
}, [])
// Store event handlers in useMemo to avoid recreating them
const echartsEvents = useMemo(() => ({
finished: (params: EChartsEventParams) => {
// Limit finished event frequency to avoid infinite loops
finishedEventCountRef.current++
if (finishedEventCountRef.current > 3) {
// Stop processing after 3 times to avoid infinite loops
return
}
if (chartInstanceRef.current) {
// Use debounced resize
debouncedResize()
}
}, },
}), [echartsRef]) // echartsRef is stable, so this effectively runs once. }), [debouncedResize])
// Handle container resize for echarts // Handle container resize for echarts
useEffect(() => { useEffect(() => {
if (language !== 'echarts' || !echartsRef.current) return if (language !== 'echarts' || !chartInstanceRef.current) return
const handleResize = () => { const handleResize = () => {
// This gets the echarts instance from the component if (chartInstanceRef.current)
const instance = echartsRef.current?.getEchartsInstance?.() // Use debounced resize
if (instance) debouncedResize()
instance.resize()
} }
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
// Also manually trigger resize after a short delay to ensure proper sizing
const resizeTimer = setTimeout(handleResize, 200)
return () => { return () => {
window.removeEventListener('resize', handleResize) window.removeEventListener('resize', handleResize)
clearTimeout(resizeTimer) if (resizeTimerRef.current)
clearTimeout(resizeTimerRef.current)
} }
}, [language, echartsRef.current]) }, [language, debouncedResize])
// Process chart data when content changes // Process chart data when content changes
useEffect(() => { useEffect(() => {
// Only process echarts content // Only process echarts content
@ -222,6 +266,7 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
} }
}, [language, children]) }, [language, children])
// Cache rendered content to avoid unnecessary re-renders
const renderCodeContent = useMemo(() => { const renderCodeContent = useMemo(() => {
const content = String(children).replace(/\n$/, '') const content = String(children).replace(/\n$/, '')
switch (language) { switch (language) {
@ -274,6 +319,9 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
// Success state: show the chart // Success state: show the chart
if (chartState === 'success' && finalChartOption) { if (chartState === 'success' && finalChartOption) {
// Reset finished event counter
finishedEventCountRef.current = 0
return ( return (
<div style={{ <div style={{
minWidth: '300px', minWidth: '300px',
@ -286,13 +334,20 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
}}> }}>
<ErrorBoundary> <ErrorBoundary>
<ReactEcharts <ReactEcharts
ref={echartsRef} ref={(e) => {
if (e && isInitialRenderRef.current) {
echartsRef.current = e
isInitialRenderRef.current = false
}
}}
option={finalChartOption} option={finalChartOption}
style={echartsStyle} style={echartsStyle}
theme={isDarkMode ? 'dark' : undefined} theme={isDarkMode ? 'dark' : undefined}
opts={echartsOpts} opts={echartsOpts}
notMerge={true} notMerge={false}
onEvents={echartsOnEvents} lazyUpdate={false}
onEvents={echartsEvents}
onChartReady={handleChartReady}
/> />
</ErrorBoundary> </ErrorBoundary>
</div> </div>
@ -363,7 +418,7 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
</SyntaxHighlighter> </SyntaxHighlighter>
) )
} }
}, [children, language, isSVG, finalChartOption, props, theme, match, chartState, isDarkMode, echartsStyle, echartsOpts, echartsOnEvents]) }, [children, language, isSVG, finalChartOption, props, theme, match, chartState, isDarkMode, echartsStyle, echartsOpts, handleChartReady, echartsEvents])
if (inline || !match) if (inline || !match)
return <code {...props} className={className}>{children}</code> return <code {...props} className={className}>{children}</code>

@ -152,7 +152,6 @@ Chat applications support session persistence, allowing previous chat history to
- `data` (object) detail - `data` (object) detail
- `id` (string) Unique ID of workflow execution - `id` (string) Unique ID of workflow execution
- `workflow_id` (string) ID of related workflow - `workflow_id` (string) ID of related workflow
- `sequence_number` (int) Self-increasing serial number, self-increasing in the App, starting from 1
- `created_at` (timestamp) Creation timestamp, e.g., 1705395332 - `created_at` (timestamp) Creation timestamp, e.g., 1705395332
- `event: node_started` node execution started - `event: node_started` node execution started
- `task_id` (string) Task ID, used for request tracking and the below Stop Generate API - `task_id` (string) Task ID, used for request tracking and the below Stop Generate API
@ -287,7 +286,7 @@ Chat applications support session persistence, allowing previous chat history to
### Streaming Mode ### Streaming Mode
<CodeGroup title="Response"> <CodeGroup title="Response">
```streaming {{ title: 'Response' }} ```streaming {{ title: 'Response' }}
data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "sequence_number": 1, "created_at": 1679586595}} data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "created_at": 1679586595}}
data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}} data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}}
data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}} data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}}
data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}} data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}}

@ -152,7 +152,6 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `data` (object) 詳細 - `data` (object) 詳細
- `id` (string) ワークフロー実行の一意ID - `id` (string) ワークフロー実行の一意ID
- `workflow_id` (string) 関連ワークフローのID - `workflow_id` (string) 関連ワークフローのID
- `sequence_number` (int) 自己増加シリアル番号、アプリ内で自己増加し、1から始まります
- `created_at` (timestamp) 作成タイムスタンプ、例1705395332 - `created_at` (timestamp) 作成タイムスタンプ、例1705395332
- `event: node_started` ノード実行が開始 - `event: node_started` ノード実行が開始
- `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用
@ -287,7 +286,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
### ストリーミングモード ### ストリーミングモード
<CodeGroup title="応答"> <CodeGroup title="応答">
```streaming {{ title: '応答' }} ```streaming {{ title: '応答' }}
data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "sequence_number": 1, "created_at": 1679586595}} data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "created_at": 1679586595}}
data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}} data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}}
data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}} data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}}
data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}} data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}}

@ -153,7 +153,6 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
- `data` (object) 详细内容 - `data` (object) 详细内容
- `id` (string) workflow 执行 ID - `id` (string) workflow 执行 ID
- `workflow_id` (string) 关联 Workflow ID - `workflow_id` (string) 关联 Workflow ID
- `sequence_number` (int) 自增序号App 内自增,从 1 开始
- `created_at` (timestamp) 开始时间 - `created_at` (timestamp) 开始时间
- `event: node_started` node 开始执行 - `event: node_started` node 开始执行
- `task_id` (string) 任务 ID用于请求跟踪和下方的停止响应接口 - `task_id` (string) 任务 ID用于请求跟踪和下方的停止响应接口
@ -297,7 +296,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
### 流式模式 ### 流式模式
<CodeGroup title="Response"> <CodeGroup title="Response">
```streaming {{ title: 'Response' }} ```streaming {{ title: 'Response' }}
data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "sequence_number": 1, "created_at": 1679586595}} data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "created_at": 1679586595}}
data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}} data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}}
data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}} data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}}
data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}} data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}}

@ -103,7 +103,6 @@ Workflow applications offers non-session support and is ideal for translation, a
- `data` (object) detail - `data` (object) detail
- `id` (string) Unique ID of workflow execution - `id` (string) Unique ID of workflow execution
- `workflow_id` (string) ID of related workflow - `workflow_id` (string) ID of related workflow
- `sequence_number` (int) Self-increasing serial number, self-increasing in the App, starting from 1
- `created_at` (timestamp) Creation timestamp, e.g., 1705395332 - `created_at` (timestamp) Creation timestamp, e.g., 1705395332
- `event: node_started` node execution started - `event: node_started` node execution started
- `task_id` (string) Task ID, used for request tracking and the below Stop Generate API - `task_id` (string) Task ID, used for request tracking and the below Stop Generate API
@ -241,7 +240,7 @@ Workflow applications offers non-session support and is ideal for translation, a
### Streaming Mode ### Streaming Mode
<CodeGroup title="Response"> <CodeGroup title="Response">
```streaming {{ title: 'Response' }} ```streaming {{ title: 'Response' }}
data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "sequence_number": 1, "created_at": 1679586595}} data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "created_at": 1679586595}}
data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}} data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}}
data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}} data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}}
data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}} data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}}
@ -533,6 +532,12 @@ Workflow applications offers non-session support and is ideal for translation, a
<Property name='limit' type='int' key='limit'> <Property name='limit' type='int' key='limit'>
How many chat history messages to return in one request, default is 20. How many chat history messages to return in one request, default is 20.
</Property> </Property>
<Property name='created_by_end_user_session_id' type='str' key='created_by_end_user_session_id'>
Created by which endUser, for example, `abc-123`.
</Property>
<Property name='created_by_account' type='str' key='created_by_account'>
Created by which email account, for example, lizb@test.com.
</Property>
</Properties> </Properties>
### Response ### Response

@ -104,7 +104,6 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `data` (object) 詳細 - `data` (object) 詳細
- `id` (string) ワークフロー実行の一意の ID - `id` (string) ワークフロー実行の一意の ID
- `workflow_id` (string) 関連するワークフローの ID - `workflow_id` (string) 関連するワークフローの ID
- `sequence_number` (int) 自己増加シリアル番号、アプリ内で自己増加し、1 から始まります
- `created_at` (timestamp) 作成タイムスタンプ、例1705395332 - `created_at` (timestamp) 作成タイムスタンプ、例1705395332
- `event: node_started` ノード実行開始 - `event: node_started` ノード実行開始
- `task_id` (string) タスク ID、リクエスト追跡と以下の Stop Generate API に使用 - `task_id` (string) タスク ID、リクエスト追跡と以下の Stop Generate API に使用
@ -242,7 +241,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
### ストリーミングモード ### ストリーミングモード
<CodeGroup title="応答"> <CodeGroup title="応答">
```streaming {{ title: '応答' }} ```streaming {{ title: '応答' }}
data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "sequence_number": 1, "created_at": 1679586595}} data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "created_at": 1679586595}}
data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}} data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}}
data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}} data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}}
data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}} data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}}
@ -534,6 +533,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<Property name='limit' type='int' key='limit'> <Property name='limit' type='int' key='limit'>
1回のリクエストで返すチャット履歴メッセージの数、デフォルトは20。 1回のリクエストで返すチャット履歴メッセージの数、デフォルトは20。
</Property> </Property>
<Property name='created_by_end_user_session_id' type='str' key='created_by_end_user_session_id'>
どのendUserによって作成されたか、例えば、`abc-123`。
</Property>
<Property name='created_by_account' type='str' key='created_by_account'>
どのメールアカウントによって作成されたか、例えば、lizb@test.com。
</Property>
</Properties> </Properties>
### 応答 ### 応答

@ -98,7 +98,6 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
- `data` (object) 详细内容 - `data` (object) 详细内容
- `id` (string) workflow 执行 ID - `id` (string) workflow 执行 ID
- `workflow_id` (string) 关联 Workflow ID - `workflow_id` (string) 关联 Workflow ID
- `sequence_number` (int) 自增序号App 内自增,从 1 开始
- `created_at` (timestamp) 开始时间 - `created_at` (timestamp) 开始时间
- `event: node_started` node 开始执行 - `event: node_started` node 开始执行
- `task_id` (string) 任务 ID用于请求跟踪和下方的停止响应接口 - `task_id` (string) 任务 ID用于请求跟踪和下方的停止响应接口
@ -232,7 +231,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
### Streaming Mode ### Streaming Mode
<CodeGroup title="Response"> <CodeGroup title="Response">
```streaming {{ title: 'Response' }} ```streaming {{ title: 'Response' }}
data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "sequence_number": 1, "created_at": 1679586595}} data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "created_at": 1679586595}}
data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}} data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}}
data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}} data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}}
data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}} data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}}
@ -522,6 +521,12 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
<Property name='limit' type='int' key='limit'> <Property name='limit' type='int' key='limit'>
每页条数, 默认20. 每页条数, 默认20.
</Property> </Property>
<Property name='created_by_end_user_session_id' type='str' key='created_by_end_user_session_id'>
由哪个endUser创建例如`abc-123`.
</Property>
<Property name='created_by_account' type='str' key='created_by_account'>
由哪个邮箱账户创建例如lizb@test.com.
</Property>
</Properties> </Properties>
### Response ### Response

@ -17,9 +17,9 @@ import Loading from '@/app/components/base/loading'
import ProviderCard from '@/app/components/plugins/provider-card' import ProviderCard from '@/app/components/plugins/provider-card'
import List from '@/app/components/plugins/marketplace/list' import List from '@/app/components/plugins/marketplace/list'
import type { Plugin } from '@/app/components/plugins/types' import type { Plugin } from '@/app/components/plugins/types'
import { MARKETPLACE_URL_PREFIX } from '@/config'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { getLocaleOnClient } from '@/i18n' import { getLocaleOnClient } from '@/i18n'
import { getMarketplaceUrl } from '@/utils/var'
type InstallFromMarketplaceProps = { type InstallFromMarketplaceProps = {
providers: ModelProvider[] providers: ModelProvider[]
@ -55,7 +55,7 @@ const InstallFromMarketplace = ({
</div> </div>
<div className='mb-2 flex items-center pt-2'> <div className='mb-2 flex items-center pt-2'>
<span className='system-sm-regular pr-1 text-text-tertiary'>{t('common.modelProvider.discoverMore')}</span> <span className='system-sm-regular pr-1 text-text-tertiary'>{t('common.modelProvider.discoverMore')}</span>
<Link target="_blank" href={`${MARKETPLACE_URL_PREFIX}${theme ? `?theme=${theme}` : ''}`} className='system-sm-medium inline-flex items-center text-text-accent'> <Link target="_blank" href={getMarketplaceUrl('', { theme })} className='system-sm-medium inline-flex items-center text-text-accent'>
{t('plugin.marketplace.difyMarketplace')} {t('plugin.marketplace.difyMarketplace')}
<RiArrowRightUpLine className='h-4 w-4' /> <RiArrowRightUpLine className='h-4 w-4' />
</Link> </Link>

@ -15,6 +15,7 @@ import { renderI18nObject } from '@/i18n'
import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks' import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks'
import Partner from '../base/badges/partner' import Partner from '../base/badges/partner'
import Verified from '../base/badges/verified' import Verified from '../base/badges/verified'
import { RiAlertFill } from '@remixicon/react'
export type Props = { export type Props = {
className?: string className?: string
@ -28,6 +29,7 @@ export type Props = {
isLoading?: boolean isLoading?: boolean
loadingFileName?: string loadingFileName?: string
locale?: string locale?: string
limitedInstall?: boolean
} }
const Card = ({ const Card = ({
@ -42,6 +44,7 @@ const Card = ({
isLoading = false, isLoading = false,
loadingFileName, loadingFileName,
locale: localeFromProps, locale: localeFromProps,
limitedInstall = false,
}: Props) => { }: Props) => {
const defaultLocale = useGetLanguage() const defaultLocale = useGetLanguage()
const locale = localeFromProps ? getLanguage(localeFromProps) : defaultLocale const locale = localeFromProps ? getLanguage(localeFromProps) : defaultLocale
@ -54,7 +57,7 @@ const Card = ({
obj ? renderI18nObject(obj, locale) : '' obj ? renderI18nObject(obj, locale) : ''
const isPartner = badges.includes('partner') const isPartner = badges.includes('partner')
const wrapClassName = cn('hover-bg-components-panel-on-panel-item-bg relative rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-4 pb-3 shadow-xs', className) const wrapClassName = cn('hover-bg-components-panel-on-panel-item-bg relative overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs', className)
if (isLoading) { if (isLoading) {
return ( return (
<Placeholder <Placeholder
@ -66,30 +69,39 @@ const Card = ({
return ( return (
<div className={wrapClassName}> <div className={wrapClassName}>
{!hideCornerMark && <CornerMark text={cornerMark} />} <div className={cn('p-4 pb-3', limitedInstall && 'pb-1')}>
{/* Header */} {!hideCornerMark && <CornerMark text={cornerMark} />}
<div className="flex"> {/* Header */}
<Icon src={icon} installed={installed} installFailed={installFailed} /> <div className="flex">
<div className="ml-3 w-0 grow"> <Icon src={icon} installed={installed} installFailed={installFailed} />
<div className="flex h-5 items-center"> <div className="ml-3 w-0 grow">
<Title title={getLocalizedText(label)} /> <div className="flex h-5 items-center">
{isPartner && <Partner className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.partnerTip')} />} <Title title={getLocalizedText(label)} />
{verified && <Verified className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.verifiedTip')} />} {isPartner && <Partner className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.partnerTip')} />}
{titleLeft} {/* This can be version badge */} {verified && <Verified className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.verifiedTip')} />}
{titleLeft} {/* This can be version badge */}
</div>
<OrgInfo
className="mt-0.5"
orgName={org}
packageName={name}
/>
</div> </div>
<OrgInfo
className="mt-0.5"
orgName={org}
packageName={name}
/>
</div> </div>
<Description
className="mt-3"
text={getLocalizedText(brief)}
descriptionLineRows={descriptionLineRows}
/>
{footer && <div>{footer}</div>}
</div> </div>
<Description {limitedInstall
className="mt-3" && <div className='relative flex h-8 items-center gap-x-2 px-3 after:absolute after:bottom-0 after:left-0 after:right-0 after:top-0 after:bg-toast-warning-bg after:opacity-40'>
text={getLocalizedText(brief)} <RiAlertFill className='h-3 w-3 shrink-0 text-text-warning-secondary' />
descriptionLineRows={descriptionLineRows} <p className='system-xs-regular z-10 grow text-text-secondary'>
/> {t('plugin.installModal.installWarning')}
{footer && <div>{footer}</div>} </p>
</div>}
</div> </div>
) )
} }

@ -0,0 +1,46 @@
import { useGlobalPublicStore } from '@/context/global-public-context'
import type { SystemFeatures } from '@/types/feature'
import { InstallationScope } from '@/types/feature'
import type { Plugin, PluginManifestInMarket } from '../../types'
type PluginProps = (Plugin | PluginManifestInMarket) & { from: 'github' | 'marketplace' | 'package' }
export function pluginInstallLimit(plugin: PluginProps, systemFeatures: SystemFeatures) {
if (systemFeatures.plugin_installation_permission.restrict_to_marketplace_only) {
if (plugin.from === 'github' || plugin.from === 'package')
return { canInstall: false }
}
if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.ALL) {
return {
canInstall: true,
}
}
if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.NONE) {
return {
canInstall: false,
}
}
const verification = plugin.verification || {}
if (!plugin.verification || !plugin.verification.authorized_category)
verification.authorized_category = 'langgenius'
if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.OFFICIAL_ONLY) {
return {
canInstall: verification.authorized_category === 'langgenius',
}
}
if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.OFFICIAL_AND_PARTNER) {
return {
canInstall: verification.authorized_category === 'langgenius' || verification.authorized_category === 'partner',
}
}
return {
canInstall: true,
}
}
export default function usePluginInstallLimit(plugin: PluginProps) {
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
return pluginInstallLimit(plugin, systemFeatures)
}

@ -39,7 +39,7 @@ const Item: FC<Props> = ({
plugin_id: data.unique_identifier, plugin_id: data.unique_identifier,
} }
onFetchedPayload(payload) onFetchedPayload(payload)
setPayload(payload) setPayload({ ...payload, from: dependency.type })
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]) }, [data])

@ -8,6 +8,7 @@ import useGetIcon from '../../base/use-get-icon'
import { MARKETPLACE_API_PREFIX } from '@/config' import { MARKETPLACE_API_PREFIX } from '@/config'
import Version from '../../base/version' import Version from '../../base/version'
import type { VersionProps } from '../../../types' import type { VersionProps } from '../../../types'
import usePluginInstallLimit from '../../hooks/use-install-plugin-limit'
type Props = { type Props = {
checked: boolean checked: boolean
@ -29,9 +30,11 @@ const LoadedItem: FC<Props> = ({
...particleVersionInfo, ...particleVersionInfo,
toInstallVersion: payload.version, toInstallVersion: payload.version,
} }
const { canInstall } = usePluginInstallLimit(payload)
return ( return (
<div className='flex items-center space-x-2'> <div className='flex items-center space-x-2'>
<Checkbox <Checkbox
disabled={!canInstall}
className='shrink-0' className='shrink-0'
checked={checked} checked={checked}
onCheck={() => onCheckedChange(payload)} onCheck={() => onCheckedChange(payload)}
@ -43,6 +46,7 @@ const LoadedItem: FC<Props> = ({
icon: isFromMarketPlace ? `${MARKETPLACE_API_PREFIX}/plugins/${payload.org}/${payload.name}/icon` : getIconUrl(payload.icon), icon: isFromMarketPlace ? `${MARKETPLACE_API_PREFIX}/plugins/${payload.org}/${payload.name}/icon` : getIconUrl(payload.icon),
}} }}
titleLeft={payload.version ? <Version {...versionInfo} /> : null} titleLeft={payload.version ? <Version {...versionInfo} /> : null}
limitedInstall={!canInstall}
/> />
</div> </div>
) )

@ -29,7 +29,7 @@ const PackageItem: FC<Props> = ({
const plugin = pluginManifestToCardPluginProps(payload.value.manifest) const plugin = pluginManifestToCardPluginProps(payload.value.manifest)
return ( return (
<LoadedItem <LoadedItem
payload={plugin} payload={{ ...plugin, from: payload.type }}
checked={checked} checked={checked}
onCheckedChange={onCheckedChange} onCheckedChange={onCheckedChange}
isFromMarketPlace={isFromMarketPlace} isFromMarketPlace={isFromMarketPlace}

@ -1,5 +1,6 @@
'use client' 'use client'
import type { FC } from 'react' import type { ForwardRefRenderFunction } from 'react'
import { useImperativeHandle } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react'
import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '../../../types' import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '../../../types'
import MarketplaceItem from '../item/marketplace-item' import MarketplaceItem from '../item/marketplace-item'
@ -9,22 +10,34 @@ import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use
import produce from 'immer' import produce from 'immer'
import PackageItem from '../item/package-item' import PackageItem from '../item/package-item'
import LoadingError from '../../base/loading-error' import LoadingError from '../../base/loading-error'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { pluginInstallLimit } from '../../hooks/use-install-plugin-limit'
type Props = { type Props = {
allPlugins: Dependency[] allPlugins: Dependency[]
selectedPlugins: Plugin[] selectedPlugins: Plugin[]
onSelect: (plugin: Plugin, selectedIndex: number) => void onSelect: (plugin: Plugin, selectedIndex: number, allCanInstallPluginsLength: number) => void
onSelectAll: (plugins: Plugin[], selectedIndexes: number[]) => void
onDeSelectAll: () => void
onLoadedAllPlugin: (installedInfo: Record<string, VersionInfo>) => void onLoadedAllPlugin: (installedInfo: Record<string, VersionInfo>) => void
isFromMarketPlace?: boolean isFromMarketPlace?: boolean
} }
const InstallByDSLList: FC<Props> = ({ export type ExposeRefs = {
selectAllPlugins: () => void
deSelectAllPlugins: () => void
}
const InstallByDSLList: ForwardRefRenderFunction<ExposeRefs, Props> = ({
allPlugins, allPlugins,
selectedPlugins, selectedPlugins,
onSelect, onSelect,
onSelectAll,
onDeSelectAll,
onLoadedAllPlugin, onLoadedAllPlugin,
isFromMarketPlace, isFromMarketPlace,
}) => { }, ref) => {
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
// DSL has id, to get plugin info to show more info // DSL has id, to get plugin info to show more info
const { isLoading: isFetchingMarketplaceDataById, data: infoGetById, error: infoByIdError } = useFetchPluginsInMarketPlaceByInfo(allPlugins.filter(d => d.type === 'marketplace').map((d) => { const { isLoading: isFetchingMarketplaceDataById, data: infoGetById, error: infoByIdError } = useFetchPluginsInMarketPlaceByInfo(allPlugins.filter(d => d.type === 'marketplace').map((d) => {
const dependecy = (d as GitHubItemAndMarketPlaceDependency).value const dependecy = (d as GitHubItemAndMarketPlaceDependency).value
@ -97,7 +110,8 @@ const InstallByDSLList: FC<Props> = ({
const sortedList = allPlugins.filter(d => d.type === 'marketplace').map((d) => { const sortedList = allPlugins.filter(d => d.type === 'marketplace').map((d) => {
const p = d as GitHubItemAndMarketPlaceDependency const p = d as GitHubItemAndMarketPlaceDependency
const id = p.value.marketplace_plugin_unique_identifier?.split(':')[0] const id = p.value.marketplace_plugin_unique_identifier?.split(':')[0]
return infoGetById.data.list.find(item => item.plugin.plugin_id === id)?.plugin const retPluginInfo = infoGetById.data.list.find(item => item.plugin.plugin_id === id)?.plugin
return { ...retPluginInfo, from: d.type } as Plugin
}) })
const payloads = sortedList const payloads = sortedList
const failedIndex: number[] = [] const failedIndex: number[] = []
@ -106,7 +120,7 @@ const InstallByDSLList: FC<Props> = ({
if (payloads[i]) { if (payloads[i]) {
draft[index] = { draft[index] = {
...payloads[i], ...payloads[i],
version: payloads[i].version || payloads[i].latest_version, version: payloads[i]!.version || payloads[i]!.latest_version,
} }
} }
else { failedIndex.push(index) } else { failedIndex.push(index) }
@ -181,9 +195,35 @@ const InstallByDSLList: FC<Props> = ({
const handleSelect = useCallback((index: number) => { const handleSelect = useCallback((index: number) => {
return () => { return () => {
onSelect(plugins[index]!, index) const canSelectPlugins = plugins.filter((p) => {
const { canInstall } = pluginInstallLimit(p!, systemFeatures)
return canInstall
})
onSelect(plugins[index]!, index, canSelectPlugins.length)
} }
}, [onSelect, plugins]) }, [onSelect, plugins, systemFeatures])
useImperativeHandle(ref, () => ({
selectAllPlugins: () => {
const selectedIndexes: number[] = []
const selectedPlugins: Plugin[] = []
allPlugins.forEach((d, index) => {
const p = plugins[index]
if (!p)
return
const { canInstall } = pluginInstallLimit(p, systemFeatures)
if (canInstall) {
selectedIndexes.push(index)
selectedPlugins.push(p)
}
})
onSelectAll(selectedPlugins, selectedIndexes)
},
deSelectAllPlugins: () => {
onDeSelectAll()
},
}))
return ( return (
<> <>
{allPlugins.map((d, index) => { {allPlugins.map((d, index) => {
@ -211,7 +251,7 @@ const InstallByDSLList: FC<Props> = ({
key={index} key={index}
checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)} checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)}
onCheckedChange={handleSelect(index)} onCheckedChange={handleSelect(index)}
payload={plugin} payload={{ ...plugin, from: d.type } as Plugin}
version={(d as GitHubItemAndMarketPlaceDependency).value.version! || plugin?.version || ''} version={(d as GitHubItemAndMarketPlaceDependency).value.version! || plugin?.version || ''}
versionInfo={getVersionInfo(`${plugin?.org || plugin?.author}/${plugin?.name}`)} versionInfo={getVersionInfo(`${plugin?.org || plugin?.author}/${plugin?.name}`)}
/> />
@ -234,4 +274,4 @@ const InstallByDSLList: FC<Props> = ({
</> </>
) )
} }
export default React.memo(InstallByDSLList) export default React.forwardRef(InstallByDSLList)

@ -1,15 +1,18 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import { useRef } from 'react'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import type { Dependency, InstallStatusResponse, Plugin, VersionInfo } from '../../../types' import type { Dependency, InstallStatusResponse, Plugin, VersionInfo } from '../../../types'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { RiLoader2Line } from '@remixicon/react' import { RiLoader2Line } from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { ExposeRefs } from './install-multi'
import InstallMulti from './install-multi' import InstallMulti from './install-multi'
import { useInstallOrUpdate } from '@/service/use-plugins' import { useInstallOrUpdate } from '@/service/use-plugins'
import useRefreshPluginList from '../../hooks/use-refresh-plugin-list' import useRefreshPluginList from '../../hooks/use-refresh-plugin-list'
import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-permission' import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-permission'
import { useMittContextSelector } from '@/context/mitt-context' import { useMittContextSelector } from '@/context/mitt-context'
import Checkbox from '@/app/components/base/checkbox'
const i18nPrefix = 'plugin.installModal' const i18nPrefix = 'plugin.installModal'
type Props = { type Props = {
@ -34,18 +37,8 @@ const Install: FC<Props> = ({
const [selectedPlugins, setSelectedPlugins] = React.useState<Plugin[]>([]) const [selectedPlugins, setSelectedPlugins] = React.useState<Plugin[]>([])
const [selectedIndexes, setSelectedIndexes] = React.useState<number[]>([]) const [selectedIndexes, setSelectedIndexes] = React.useState<number[]>([])
const selectedPluginsNum = selectedPlugins.length const selectedPluginsNum = selectedPlugins.length
const installMultiRef = useRef<ExposeRefs>(null)
const { refreshPluginList } = useRefreshPluginList() const { refreshPluginList } = useRefreshPluginList()
const handleSelect = (plugin: Plugin, selectedIndex: number) => {
const isSelected = !!selectedPlugins.find(p => p.plugin_id === plugin.plugin_id)
let nextSelectedPlugins
if (isSelected)
nextSelectedPlugins = selectedPlugins.filter(p => p.plugin_id !== plugin.plugin_id)
else
nextSelectedPlugins = [...selectedPlugins, plugin]
setSelectedPlugins(nextSelectedPlugins)
const nextSelectedIndexes = isSelected ? selectedIndexes.filter(i => i !== selectedIndex) : [...selectedIndexes, selectedIndex]
setSelectedIndexes(nextSelectedIndexes)
}
const [canInstall, setCanInstall] = React.useState(false) const [canInstall, setCanInstall] = React.useState(false)
const [installedInfo, setInstalledInfo] = useState<Record<string, VersionInfo> | undefined>(undefined) const [installedInfo, setInstalledInfo] = useState<Record<string, VersionInfo> | undefined>(undefined)
@ -81,6 +74,51 @@ const Install: FC<Props> = ({
installedInfo: installedInfo!, installedInfo: installedInfo!,
}) })
} }
const [isSelectAll, setIsSelectAll] = useState(false)
const [isIndeterminate, setIsIndeterminate] = useState(false)
const handleClickSelectAll = useCallback(() => {
if (isSelectAll)
installMultiRef.current?.deSelectAllPlugins()
else
installMultiRef.current?.selectAllPlugins()
}, [isSelectAll])
const handleSelectAll = useCallback((plugins: Plugin[], selectedIndexes: number[]) => {
setSelectedPlugins(plugins)
setSelectedIndexes(selectedIndexes)
setIsSelectAll(true)
setIsIndeterminate(false)
}, [])
const handleDeSelectAll = useCallback(() => {
setSelectedPlugins([])
setSelectedIndexes([])
setIsSelectAll(false)
setIsIndeterminate(false)
}, [])
const handleSelect = useCallback((plugin: Plugin, selectedIndex: number, allPluginsLength: number) => {
const isSelected = !!selectedPlugins.find(p => p.plugin_id === plugin.plugin_id)
let nextSelectedPlugins
if (isSelected)
nextSelectedPlugins = selectedPlugins.filter(p => p.plugin_id !== plugin.plugin_id)
else
nextSelectedPlugins = [...selectedPlugins, plugin]
setSelectedPlugins(nextSelectedPlugins)
const nextSelectedIndexes = isSelected ? selectedIndexes.filter(i => i !== selectedIndex) : [...selectedIndexes, selectedIndex]
setSelectedIndexes(nextSelectedIndexes)
if (nextSelectedPlugins.length === 0) {
setIsSelectAll(false)
setIsIndeterminate(false)
}
else if (nextSelectedPlugins.length === allPluginsLength) {
setIsSelectAll(true)
setIsIndeterminate(false)
}
else {
setIsIndeterminate(true)
setIsSelectAll(false)
}
}, [selectedPlugins, selectedIndexes])
const { canInstallPluginFromMarketplace } = useCanInstallPluginFromMarketplace() const { canInstallPluginFromMarketplace } = useCanInstallPluginFromMarketplace()
return ( return (
<> <>
@ -90,9 +128,12 @@ const Install: FC<Props> = ({
</div> </div>
<div className='w-full space-y-1 rounded-2xl bg-background-section-burn p-2'> <div className='w-full space-y-1 rounded-2xl bg-background-section-burn p-2'>
<InstallMulti <InstallMulti
ref={installMultiRef}
allPlugins={allPlugins} allPlugins={allPlugins}
selectedPlugins={selectedPlugins} selectedPlugins={selectedPlugins}
onSelect={handleSelect} onSelect={handleSelect}
onSelectAll={handleSelectAll}
onDeSelectAll={handleDeSelectAll}
onLoadedAllPlugin={handleLoadedAllPlugin} onLoadedAllPlugin={handleLoadedAllPlugin}
isFromMarketPlace={isFromMarketPlace} isFromMarketPlace={isFromMarketPlace}
/> />
@ -100,21 +141,29 @@ const Install: FC<Props> = ({
</div> </div>
{/* Action Buttons */} {/* Action Buttons */}
{!isHideButton && ( {!isHideButton && (
<div className='flex items-center justify-end gap-2 self-stretch p-6 pt-5'> <div className='flex items-center justify-between gap-2 self-stretch p-6 pt-5'>
{!canInstall && ( <div className='px-2'>
<Button variant='secondary' className='min-w-[72px]' onClick={onCancel}> {canInstall && <div className='flex items-center gap-x-2' onClick={handleClickSelectAll}>
{t('common.operation.cancel')} <Checkbox checked={isSelectAll} indeterminate={isIndeterminate} />
<p className='system-sm-medium cursor-pointer text-text-secondary'>{isSelectAll ? t('common.operation.deSelectAll') : t('common.operation.selectAll')}</p>
</div>}
</div>
<div className='flex items-center justify-end gap-2 self-stretch'>
{!canInstall && (
<Button variant='secondary' className='min-w-[72px]' onClick={onCancel}>
{t('common.operation.cancel')}
</Button>
)}
<Button
variant='primary'
className='flex min-w-[72px] space-x-0.5'
disabled={!canInstall || isInstalling || selectedPlugins.length === 0 || !canInstallPluginFromMarketplace}
onClick={handleInstall}
>
{isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />}
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span>
</Button> </Button>
)} </div>
<Button
variant='primary'
className='flex min-w-[72px] space-x-0.5'
disabled={!canInstall || isInstalling || selectedPlugins.length === 0 || !canInstallPluginFromMarketplace}
onClick={handleInstall}
>
{isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />}
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span>
</Button>
</div> </div>
)} )}

@ -124,7 +124,7 @@ const Installed: FC<Props> = ({
/> />
</p> </p>
{!isDifyVersionCompatible && ( {!isDifyVersionCompatible && (
<p className='system-md-regular flex items-center gap-1 text-text-secondary text-text-warning'> <p className='system-md-regular flex items-center gap-1 text-text-warning'>
{t('plugin.difyVersionNotCompatible', { minimalDifyVersion: payload.meta.minimum_dify_version })} {t('plugin.difyVersionNotCompatible', { minimalDifyVersion: payload.meta.minimum_dify_version })}
</p> </p>
)} )}

@ -15,6 +15,7 @@ import Version from '../../base/version'
import { usePluginTaskList } from '@/service/use-plugins' import { usePluginTaskList } from '@/service/use-plugins'
import { gte } from 'semver' import { gte } from 'semver'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import useInstallPluginLimit from '../../hooks/use-install-plugin-limit'
const i18nPrefix = 'plugin.installModal' const i18nPrefix = 'plugin.installModal'
@ -124,15 +125,16 @@ const Installed: FC<Props> = ({
const isDifyVersionCompatible = useMemo(() => { const isDifyVersionCompatible = useMemo(() => {
if (!pluginDeclaration || !langeniusVersionInfo.current_version) return true if (!pluginDeclaration || !langeniusVersionInfo.current_version) return true
return gte(langeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version ?? '0.0.0') return gte(langeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version ?? '0.0.0')
}, [langeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version]) }, [langeniusVersionInfo.current_version, pluginDeclaration])
const { canInstall } = useInstallPluginLimit({ ...payload, from: 'marketplace' })
return ( return (
<> <>
<div className='flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3'> <div className='flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3'>
<div className='system-md-regular text-text-secondary'> <div className='system-md-regular text-text-secondary'>
<p>{t(`${i18nPrefix}.readyToInstall`)}</p> <p>{t(`${i18nPrefix}.readyToInstall`)}</p>
{!isDifyVersionCompatible && ( {!isDifyVersionCompatible && (
<p className='system-md-regular text-text-secondary text-text-warning'> <p className='system-md-regular text-text-warning'>
{t('plugin.difyVersionNotCompatible', { minimalDifyVersion: pluginDeclaration?.manifest.meta.minimum_dify_version })} {t('plugin.difyVersionNotCompatible', { minimalDifyVersion: pluginDeclaration?.manifest.meta.minimum_dify_version })}
</p> </p>
)} )}
@ -146,6 +148,7 @@ const Installed: FC<Props> = ({
installedVersion={installedVersion} installedVersion={installedVersion}
toInstallVersion={toInstallVersion} toInstallVersion={toInstallVersion}
/>} />}
limitedInstall={!canInstall}
/> />
</div> </div>
</div> </div>
@ -159,7 +162,7 @@ const Installed: FC<Props> = ({
<Button <Button
variant='primary' variant='primary'
className='flex min-w-[72px] space-x-0.5' className='flex min-w-[72px] space-x-0.5'
disabled={isInstalling || isLoading} disabled={isInstalling || isLoading || !canInstall}
onClick={handleInstall} onClick={handleInstall}
> >
{isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />} {isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />}

@ -1,5 +1,6 @@
import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../types' import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../types'
import type { GitHubUrlInfo } from '@/app/components/plugins/types' import type { GitHubUrlInfo } from '@/app/components/plugins/types'
import { isEmpty } from 'lodash-es'
export const pluginManifestToCardPluginProps = (pluginManifest: PluginDeclaration): Plugin => { export const pluginManifestToCardPluginProps = (pluginManifest: PluginDeclaration): Plugin => {
return { return {
@ -47,6 +48,7 @@ export const pluginManifestInMarketToPluginProps = (pluginManifest: PluginManife
}, },
tags: [], tags: [],
badges: pluginManifest.badges, badges: pluginManifest.badges,
verification: isEmpty(pluginManifest.verification) ? { authorized_category: 'langgenius' } : pluginManifest.verification,
} }
} }

@ -56,7 +56,7 @@ const CardWrapper = ({
> >
{t('plugin.detailPanel.operation.install')} {t('plugin.detailPanel.operation.install')}
</Button> </Button>
<a href={`${getPluginLinkInMarketplace(plugin)}?language=${localeFromLocale}${theme ? `&theme=${theme}` : ''}`} target='_blank' className='block w-[calc(50%-4px)] flex-1 shrink-0'> <a href={getPluginLinkInMarketplace(plugin, { language: localeFromLocale, theme })} target='_blank' className='block w-[calc(50%-4px)] flex-1 shrink-0'>
<Button <Button
className='w-full gap-0.5' className='w-full gap-0.5'
> >

@ -8,8 +8,8 @@ import type {
} from '@/app/components/plugins/marketplace/types' } from '@/app/components/plugins/marketplace/types'
import { import {
MARKETPLACE_API_PREFIX, MARKETPLACE_API_PREFIX,
MARKETPLACE_URL_PREFIX,
} from '@/config' } from '@/config'
import { getMarketplaceUrl } from '@/utils/var'
export const getPluginIconInMarketplace = (plugin: Plugin) => { export const getPluginIconInMarketplace = (plugin: Plugin) => {
if (plugin.type === 'bundle') if (plugin.type === 'bundle')
@ -32,10 +32,10 @@ export const getFormattedPlugin = (bundle: any) => {
} }
} }
export const getPluginLinkInMarketplace = (plugin: Plugin) => { export const getPluginLinkInMarketplace = (plugin: Plugin, params?: Record<string, string | undefined>) => {
if (plugin.type === 'bundle') if (plugin.type === 'bundle')
return `${MARKETPLACE_URL_PREFIX}/bundles/${plugin.org}/${plugin.name}` return getMarketplaceUrl(`/bundles/${plugin.org}/${plugin.name}`, params)
return `${MARKETPLACE_URL_PREFIX}/plugins/${plugin.org}/${plugin.name}` return getMarketplaceUrl(`/plugins/${plugin.org}/${plugin.name}`, params)
} }
export const getMarketplacePluginsByCollectionId = async (collectionId: string, query?: CollectionsAndPluginsSearchParams) => { export const getMarketplacePluginsByCollectionId = async (collectionId: string, query?: CollectionsAndPluginsSearchParams) => {

@ -33,8 +33,9 @@ import { useGetLanguage } from '@/context/i18n'
import { useModalContext } from '@/context/modal-context' import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { useInvalidateAllToolProviders } from '@/service/use-tools' import { useInvalidateAllToolProviders } from '@/service/use-tools'
import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config' import { API_PREFIX } from '@/config'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { getMarketplaceUrl } from '@/utils/var'
const i18nPrefix = 'plugin.action' const i18nPrefix = 'plugin.action'
@ -87,7 +88,7 @@ const DetailHeader = ({
if (isFromGitHub) if (isFromGitHub)
return `https://github.com/${meta!.repo}` return `https://github.com/${meta!.repo}`
if (isFromMarketplace) if (isFromMarketplace)
return `${MARKETPLACE_URL_PREFIX}/plugins/${author}/${name}${theme ? `?theme=${theme}` : ''}` return getMarketplaceUrl(`/plugins/${author}/${name}`, { theme })
return '' return ''
}, [author, isFromGitHub, isFromMarketplace, meta, name, theme]) }, [author, isFromGitHub, isFromMarketplace, meta, name, theme])

@ -21,13 +21,14 @@ import OrgInfo from '../card/base/org-info'
import Title from '../card/base/title' import Title from '../card/base/title'
import Action from './action' import Action from './action'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config' import { API_PREFIX } from '@/config'
import { useSingleCategories } from '../hooks' import { useSingleCategories } from '../hooks'
import { useRenderI18nObject } from '@/hooks/use-i18n' import { useRenderI18nObject } from '@/hooks/use-i18n'
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list' import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { gte } from 'semver' import { gte } from 'semver'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import { getMarketplaceUrl } from '@/utils/var'
type Props = { type Props = {
className?: string className?: string
@ -166,7 +167,7 @@ const PluginItem: FC<Props> = ({
} }
{source === PluginSource.marketplace {source === PluginSource.marketplace
&& <> && <>
<a href={`${MARKETPLACE_URL_PREFIX}/plugins/${author}/${name}${theme ? `?theme=${theme}` : ''}`} target='_blank' className='flex items-center gap-0.5'> <a href={getMarketplaceUrl(`/plugins/${author}/${name}`, { theme })} target='_blank' className='flex items-center gap-0.5'>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{t('plugin.from')} <span className='text-text-secondary'>marketplace</span></div> <div className='system-2xs-medium-uppercase text-text-tertiary'>{t('plugin.from')} <span className='text-text-secondary'>marketplace</span></div>
<RiArrowRightUpLine className='h-3 w-3 text-text-tertiary' /> <RiArrowRightUpLine className='h-3 w-3 text-text-tertiary' />
</a> </a>

@ -1,4 +1,5 @@
import React, { useMemo, useRef, useState } from 'react' 'use client'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices' import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
import { FileZip } from '@/app/components/base/icons/src/vender/solid/files' import { FileZip } from '@/app/components/base/icons/src/vender/solid/files'
import { Github } from '@/app/components/base/icons/src/vender/solid/general' import { Github } from '@/app/components/base/icons/src/vender/solid/general'
@ -14,12 +15,18 @@ import { noop } from 'lodash-es'
import { useGlobalPublicStore } from '@/context/global-public-context' import { useGlobalPublicStore } from '@/context/global-public-context'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
type InstallMethod = {
icon: React.FC<{ className?: string }>
text: string
action: string
}
const Empty = () => { const Empty = () => {
const { t } = useTranslation() const { t } = useTranslation()
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const [selectedAction, setSelectedAction] = useState<string | null>(null) const [selectedAction, setSelectedAction] = useState<string | null>(null)
const [selectedFile, setSelectedFile] = useState<File | null>(null) const [selectedFile, setSelectedFile] = useState<File | null>(null)
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) const { enable_marketplace, plugin_installation_permission } = useGlobalPublicStore(s => s.systemFeatures)
const setActiveTab = usePluginPageContext(v => v.setActiveTab) const setActiveTab = usePluginPageContext(v => v.setActiveTab)
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@ -39,6 +46,22 @@ const Empty = () => {
return t('plugin.list.notFound') return t('plugin.list.notFound')
}, [pluginList?.plugins.length, t, filters.categories.length, filters.tags.length, filters.searchQuery]) }, [pluginList?.plugins.length, t, filters.categories.length, filters.tags.length, filters.searchQuery])
const [installMethods, setInstallMethods] = useState<InstallMethod[]>([])
useEffect(() => {
const methods = []
if (enable_marketplace)
methods.push({ icon: MagicBox, text: t('plugin.source.marketplace'), action: 'marketplace' })
if (plugin_installation_permission.restrict_to_marketplace_only) {
setInstallMethods(methods)
}
else {
methods.push({ icon: Github, text: t('plugin.source.github'), action: 'github' })
methods.push({ icon: FileZip, text: t('plugin.source.local'), action: 'local' })
setInstallMethods(methods)
}
}, [plugin_installation_permission, enable_marketplace, t])
return ( return (
<div className='relative z-0 w-full grow'> <div className='relative z-0 w-full grow'>
{/* skeleton */} {/* skeleton */}
@ -71,15 +94,7 @@ const Empty = () => {
accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS} accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS}
/> />
<div className='flex w-full flex-col gap-y-1'> <div className='flex w-full flex-col gap-y-1'>
{[ {installMethods.map(({ icon: Icon, text, action }) => (
...(
(enable_marketplace)
? [{ icon: MagicBox, text: t('plugin.list.source.marketplace'), action: 'marketplace' }]
: []
),
{ icon: Github, text: t('plugin.list.source.github'), action: 'github' },
{ icon: FileZip, text: t('plugin.list.source.local'), action: 'local' },
].map(({ icon: Icon, text, action }) => (
<Button <Button
key={action} key={action}
className='justify-start gap-x-0.5 px-3' className='justify-start gap-x-0.5 px-3'

@ -136,7 +136,7 @@ const PluginPage = ({
const options = usePluginPageContext(v => v.options) const options = usePluginPageContext(v => v.options)
const activeTab = usePluginPageContext(v => v.activeTab) const activeTab = usePluginPageContext(v => v.activeTab)
const setActiveTab = usePluginPageContext(v => v.setActiveTab) const setActiveTab = usePluginPageContext(v => v.setActiveTab)
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) const { enable_marketplace, branding } = useGlobalPublicStore(s => s.systemFeatures)
const isPluginsTab = useMemo(() => activeTab === PLUGIN_PAGE_TABS_MAP.plugins, [activeTab]) const isPluginsTab = useMemo(() => activeTab === PLUGIN_PAGE_TABS_MAP.plugins, [activeTab])
const isExploringMarketplace = useMemo(() => { const isExploringMarketplace = useMemo(() => {
@ -225,7 +225,7 @@ const PluginPage = ({
) )
} }
{ {
canSetPermissions && ( canSetPermissions && !branding.enabled && (
<Tooltip <Tooltip
popupContent={t('plugin.privilege.title')} popupContent={t('plugin.privilege.title')}
> >

@ -1,6 +1,6 @@
'use client' 'use client'
import { useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { RiAddLine, RiArrowDownSLine } from '@remixicon/react' import { RiAddLine, RiArrowDownSLine } from '@remixicon/react'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices' import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
@ -22,6 +22,13 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
type Props = { type Props = {
onSwitchToMarketplaceTab: () => void onSwitchToMarketplaceTab: () => void
} }
type InstallMethod = {
icon: React.FC<{ className?: string }>
text: string
action: string
}
const InstallPluginDropdown = ({ const InstallPluginDropdown = ({
onSwitchToMarketplaceTab, onSwitchToMarketplaceTab,
}: Props) => { }: Props) => {
@ -30,7 +37,7 @@ const InstallPluginDropdown = ({
const [isMenuOpen, setIsMenuOpen] = useState(false) const [isMenuOpen, setIsMenuOpen] = useState(false)
const [selectedAction, setSelectedAction] = useState<string | null>(null) const [selectedAction, setSelectedAction] = useState<string | null>(null)
const [selectedFile, setSelectedFile] = useState<File | null>(null) const [selectedFile, setSelectedFile] = useState<File | null>(null)
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) const { enable_marketplace, plugin_installation_permission } = useGlobalPublicStore(s => s.systemFeatures)
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0] const file = event.target.files?.[0]
@ -54,6 +61,22 @@ const InstallPluginDropdown = ({
// console.log(res) // console.log(res)
// } // }
const [installMethods, setInstallMethods] = useState<InstallMethod[]>([])
useEffect(() => {
const methods = []
if (enable_marketplace)
methods.push({ icon: MagicBox, text: t('plugin.source.marketplace'), action: 'marketplace' })
if (plugin_installation_permission.restrict_to_marketplace_only) {
setInstallMethods(methods)
}
else {
methods.push({ icon: Github, text: t('plugin.source.github'), action: 'github' })
methods.push({ icon: FileZip, text: t('plugin.source.local'), action: 'local' })
setInstallMethods(methods)
}
}, [plugin_installation_permission, enable_marketplace, t])
return ( return (
<PortalToFollowElem <PortalToFollowElem
open={isMenuOpen} open={isMenuOpen}
@ -84,15 +107,7 @@ const InstallPluginDropdown = ({
accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS} accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS}
/> />
<div className='w-full'> <div className='w-full'>
{[ {installMethods.map(({ icon: Icon, text, action }) => (
...(
(enable_marketplace)
? [{ icon: MagicBox, text: t('plugin.source.marketplace'), action: 'marketplace' }]
: []
),
{ icon: Github, text: t('plugin.source.github'), action: 'github' },
{ icon: FileZip, text: t('plugin.source.local'), action: 'local' },
].map(({ icon: Icon, text, action }) => (
<div <div
key={action} key={action}
className='flex w-full !cursor-pointer items-center gap-1 rounded-lg px-2 py-1.5 hover:bg-state-base-hover' className='flex w-full !cursor-pointer items-center gap-1 rounded-lg px-2 py-1.5 hover:bg-state-base-hover'

@ -94,7 +94,11 @@ export type PluginManifestInMarket = {
introduction: string introduction: string
verified: boolean verified: boolean
install_count: number install_count: number
badges: string[] badges: string[],
verification: {
authorized_category: 'langgenius' | 'partner' | 'community'
},
from: Dependency['type']
} }
export type PluginDetail = { export type PluginDetail = {
@ -145,7 +149,11 @@ export type Plugin = {
settings: CredentialFormSchemaBase[] settings: CredentialFormSchemaBase[]
} }
tags: { name: string }[] tags: { name: string }[]
badges: string[] badges: string[],
verification: {
authorized_category: 'langgenius' | 'partner' | 'community'
},
from: Dependency['type']
} }
export enum PermissionType { export enum PermissionType {

@ -12,7 +12,7 @@ import { useMarketplace } from './hooks'
import List from '@/app/components/plugins/marketplace/list' import List from '@/app/components/plugins/marketplace/list'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import { getLocaleOnClient } from '@/i18n' import { getLocaleOnClient } from '@/i18n'
import { MARKETPLACE_URL_PREFIX } from '@/config' import { getMarketplaceUrl } from '@/utils/var'
type MarketplaceProps = { type MarketplaceProps = {
searchPluginText: string searchPluginText: string
@ -84,7 +84,7 @@ const Marketplace = ({
</span> </span>
{t('common.operation.in')} {t('common.operation.in')}
<a <a
href={`${MARKETPLACE_URL_PREFIX}?language=${locale}&q=${searchPluginText}&tags=${filterPluginTags.join(',')}${theme ? `&theme=${theme}` : ''}`} href={getMarketplaceUrl('', { language: locale, q: searchPluginText, tags: filterPluginTags.join(','), theme })}
className='system-sm-medium ml-1 flex items-center text-text-accent' className='system-sm-medium ml-1 flex items-center text-text-accent'
target='_blank' target='_blank'
> >

@ -12,6 +12,7 @@ import {
useWorkflowRun, useWorkflowRun,
useWorkflowStartRun, useWorkflowStartRun,
} from '../hooks' } from '../hooks'
import { useWorkflowStore } from '@/app/components/workflow/store'
type WorkflowMainProps = Pick<WorkflowProps, 'nodes' | 'edges' | 'viewport'> type WorkflowMainProps = Pick<WorkflowProps, 'nodes' | 'edges' | 'viewport'>
const WorkflowMain = ({ const WorkflowMain = ({
@ -20,14 +21,28 @@ const WorkflowMain = ({
viewport, viewport,
}: WorkflowMainProps) => { }: WorkflowMainProps) => {
const featuresStore = useFeaturesStore() const featuresStore = useFeaturesStore()
const workflowStore = useWorkflowStore()
const handleWorkflowDataUpdate = useCallback((payload: any) => { const handleWorkflowDataUpdate = useCallback((payload: any) => {
if (payload.features && featuresStore) { const {
features,
conversation_variables,
environment_variables,
} = payload
if (features && featuresStore) {
const { setFeatures } = featuresStore.getState() const { setFeatures } = featuresStore.getState()
setFeatures(payload.features) setFeatures(features)
} }
}, [featuresStore]) if (conversation_variables) {
const { setConversationVariables } = workflowStore.getState()
setConversationVariables(conversation_variables)
}
if (environment_variables) {
const { setEnvironmentVariables } = workflowStore.getState()
setEnvironmentVariables(environment_variables)
}
}, [featuresStore, workflowStore])
const { const {
doSyncWorkflowDraft, doSyncWorkflowDraft,

@ -12,9 +12,9 @@ import {
PortalToFollowElemTrigger, PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem' } from '@/app/components/base/portal-to-follow-elem'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { MARKETPLACE_URL_PREFIX } from '@/config'
import { useDownloadPlugin } from '@/service/use-plugins' import { useDownloadPlugin } from '@/service/use-plugins'
import { downloadFile } from '@/utils/format' import { downloadFile } from '@/utils/format'
import { getMarketplaceUrl } from '@/utils/var'
type Props = { type Props = {
open: boolean open: boolean
@ -80,7 +80,7 @@ const OperationDropdown: FC<Props> = ({
<PortalToFollowElemContent className='z-[9999]'> <PortalToFollowElemContent className='z-[9999]'>
<div className='w-[112px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'> <div className='w-[112px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'>
<div onClick={handleDownload} className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>{t('common.operation.download')}</div> <div onClick={handleDownload} className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>{t('common.operation.download')}</div>
<a href={`${MARKETPLACE_URL_PREFIX}/plugins/${author}/${name}${theme ? `?theme=${theme}` : ''}`} target='_blank' className='system-md-regular block cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>{t('common.operation.viewDetails')}</a> <a href={getMarketplaceUrl(`/plugins/${author}/${name}`, { theme })} target='_blank' className='system-md-regular block cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>{t('common.operation.viewDetails')}</a>
</div> </div>
</PortalToFollowElemContent> </PortalToFollowElemContent>
</PortalToFollowElem> </PortalToFollowElem>

@ -2,6 +2,7 @@ import { memo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useIsChatMode } from '../hooks' import { useIsChatMode } from '../hooks'
import { useStore } from '../store' import { useStore } from '../store'
import { formatWorkflowRunIdentifier } from '../utils'
import { ClockPlay } from '@/app/components/base/icons/src/vender/line/time' import { ClockPlay } from '@/app/components/base/icons/src/vender/line/time'
const RunningTitle = () => { const RunningTitle = () => {
@ -12,7 +13,7 @@ const RunningTitle = () => {
return ( return (
<div className='flex h-[18px] items-center text-xs text-gray-500'> <div className='flex h-[18px] items-center text-xs text-gray-500'>
<ClockPlay className='mr-1 h-3 w-3 text-gray-500' /> <ClockPlay className='mr-1 h-3 w-3 text-gray-500' />
<span>{isChatMode ? `Test Chat#${historyWorkflowData?.sequence_number}` : `Test Run#${historyWorkflowData?.sequence_number}`}</span> <span>{isChatMode ? `Test Chat${formatWorkflowRunIdentifier(historyWorkflowData?.finished_at)}` : `Test Run${formatWorkflowRunIdentifier(historyWorkflowData?.finished_at)}`}</span>
<span className='mx-1'>·</span> <span className='mx-1'>·</span>
<span className='ml-1 flex h-[18px] items-center rounded-[5px] border border-indigo-300 bg-white/[0.48] px-1 text-[10px] font-semibold uppercase text-indigo-600'> <span className='ml-1 flex h-[18px] items-center rounded-[5px] border border-indigo-300 bg-white/[0.48] px-1 text-[10px] font-semibold uppercase text-indigo-600'>
{t('workflow.common.viewOnly')} {t('workflow.common.viewOnly')}

@ -18,6 +18,7 @@ import {
useWorkflowRun, useWorkflowRun,
} from '../hooks' } from '../hooks'
import { ControlMode, WorkflowRunningStatus } from '../types' import { ControlMode, WorkflowRunningStatus } from '../types'
import { formatWorkflowRunIdentifier } from '../utils'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { import {
PortalToFollowElem, PortalToFollowElem,
@ -199,7 +200,7 @@ const ViewHistory = ({
item.id === historyWorkflowData?.id && 'text-text-accent', item.id === historyWorkflowData?.id && 'text-text-accent',
)} )}
> >
{`Test ${isChatMode ? 'Chat' : 'Run'} #${item.sequence_number}`} {`Test ${isChatMode ? 'Chat' : 'Run'}${formatWorkflowRunIdentifier(item.finished_at)}`}
</div> </div>
<div className='flex items-center text-xs leading-[18px] text-text-tertiary'> <div className='flex items-center text-xs leading-[18px] text-text-tertiary'>
{item.created_by_account?.name} · {formatTimeFromNow((item.finished_at || item.created_at) * 1000)} {item.created_by_account?.name} · {formatTimeFromNow((item.finished_at || item.created_at) * 1000)}

@ -61,7 +61,7 @@ export const useShortcuts = (): void => {
return !showFeaturesPanel && !isEventTargetInputArea(e.target as HTMLElement) return !showFeaturesPanel && !isEventTargetInputArea(e.target as HTMLElement)
}, [workflowStore]) }, [workflowStore])
useKeyPress(['delete'], (e) => { useKeyPress(['delete', 'backspace'], (e) => {
if (shouldHandleShortcut(e)) { if (shouldHandleShortcut(e)) {
e.preventDefault() e.preventDefault()
handleNodesDelete() handleNodesDelete()

@ -10,6 +10,7 @@ import {
useWorkflowStore, useWorkflowStore,
} from '../../store' } from '../../store'
import { useWorkflowRun } from '../../hooks' import { useWorkflowRun } from '../../hooks'
import { formatWorkflowRunIdentifier } from '../../utils'
import UserInput from './user-input' import UserInput from './user-input'
import Chat from '@/app/components/base/chat/chat' import Chat from '@/app/components/base/chat/chat'
import type { ChatItem, ChatItemInTree } from '@/app/components/base/chat/types' import type { ChatItem, ChatItemInTree } from '@/app/components/base/chat/types'
@ -99,7 +100,7 @@ const ChatRecord = () => {
{fetched && ( {fetched && (
<> <>
<div className='flex shrink-0 items-center justify-between p-4 pb-1 text-base font-semibold text-text-primary'> <div className='flex shrink-0 items-center justify-between p-4 pb-1 text-base font-semibold text-text-primary'>
{`TEST CHAT#${historyWorkflowData?.sequence_number}`} {`TEST CHAT${formatWorkflowRunIdentifier(historyWorkflowData?.finished_at)}`}
<div <div
className='flex h-6 w-6 cursor-pointer items-center justify-center' className='flex h-6 w-6 cursor-pointer items-center justify-center'
onClick={() => { onClick={() => {

@ -37,6 +37,7 @@ const typeList = [
ChatVarType.ArrayString, ChatVarType.ArrayString,
ChatVarType.ArrayNumber, ChatVarType.ArrayNumber,
ChatVarType.ArrayObject, ChatVarType.ArrayObject,
ChatVarType.ArrayFile,
] ]
const objectPlaceholder = `# example const objectPlaceholder = `# example
@ -127,6 +128,7 @@ const ChatVariableModal = ({
case ChatVarType.ArrayString: case ChatVarType.ArrayString:
case ChatVarType.ArrayNumber: case ChatVarType.ArrayNumber:
case ChatVarType.ArrayObject: case ChatVarType.ArrayObject:
case ChatVarType.ArrayFile:
return value?.filter(Boolean) || [] return value?.filter(Boolean) || []
} }
} }
@ -294,84 +296,86 @@ const ChatVariableModal = ({
</div> </div>
</div> </div>
{/* default value */} {/* default value */}
<div className='mb-4'> {type !== ChatVarType.ArrayFile && (
<div className='system-sm-semibold mb-1 flex h-6 items-center justify-between text-text-secondary'> <div className='mb-4'>
<div>{t('workflow.chatVariable.modal.value')}</div> <div className='system-sm-semibold mb-1 flex h-6 items-center justify-between text-text-secondary'>
{(type === ChatVarType.ArrayString || type === ChatVarType.ArrayNumber) && ( <div>{t('workflow.chatVariable.modal.value')}</div>
<Button {(type === ChatVarType.ArrayString || type === ChatVarType.ArrayNumber) && (
variant='ghost' <Button
size='small' variant='ghost'
className='text-text-tertiary' size='small'
onClick={() => handleEditorChange(!editInJSON)} className='text-text-tertiary'
> onClick={() => handleEditorChange(!editInJSON)}
{editInJSON ? <RiInputField className='mr-1 h-3.5 w-3.5' /> : <RiDraftLine className='mr-1 h-3.5 w-3.5' />} >
{editInJSON ? t('workflow.chatVariable.modal.oneByOne') : t('workflow.chatVariable.modal.editInJSON')} {editInJSON ? <RiInputField className='mr-1 h-3.5 w-3.5' /> : <RiDraftLine className='mr-1 h-3.5 w-3.5' />}
</Button> {editInJSON ? t('workflow.chatVariable.modal.oneByOne') : t('workflow.chatVariable.modal.editInJSON')}
)} </Button>
{type === ChatVarType.Object && ( )}
<Button {type === ChatVarType.Object && (
variant='ghost' <Button
size='small' variant='ghost'
className='text-text-tertiary' size='small'
onClick={() => handleEditorChange(!editInJSON)} className='text-text-tertiary'
> onClick={() => handleEditorChange(!editInJSON)}
{editInJSON ? <RiInputField className='mr-1 h-3.5 w-3.5' /> : <RiDraftLine className='mr-1 h-3.5 w-3.5' />} >
{editInJSON ? t('workflow.chatVariable.modal.editInForm') : t('workflow.chatVariable.modal.editInJSON')} {editInJSON ? <RiInputField className='mr-1 h-3.5 w-3.5' /> : <RiDraftLine className='mr-1 h-3.5 w-3.5' />}
</Button> {editInJSON ? t('workflow.chatVariable.modal.editInForm') : t('workflow.chatVariable.modal.editInJSON')}
)} </Button>
</div> )}
<div className='flex'> </div>
{type === ChatVarType.String && ( <div className='flex'>
// Input will remove \n\r, so use Textarea just like description area {type === ChatVarType.String && (
<textarea // Input will remove \n\r, so use Textarea just like description area
className='system-sm-regular placeholder:system-sm-regular block h-20 w-full resize-none appearance-none rounded-lg border border-transparent bg-components-input-bg-normal p-2 caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs' <textarea
value={value} className='system-sm-regular placeholder:system-sm-regular block h-20 w-full resize-none appearance-none rounded-lg border border-transparent bg-components-input-bg-normal p-2 caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs'
placeholder={t('workflow.chatVariable.modal.valuePlaceholder') || ''} value={value}
onChange={e => setValue(e.target.value)} placeholder={t('workflow.chatVariable.modal.valuePlaceholder') || ''}
/> onChange={e => setValue(e.target.value)}
)} />
{type === ChatVarType.Number && ( )}
<Input {type === ChatVarType.Number && (
placeholder={t('workflow.chatVariable.modal.valuePlaceholder') || ''} <Input
value={value} placeholder={t('workflow.chatVariable.modal.valuePlaceholder') || ''}
onChange={e => setValue(Number(e.target.value))} value={value}
type='number' onChange={e => setValue(Number(e.target.value))}
/> type='number'
)}
{type === ChatVarType.Object && !editInJSON && (
<ObjectValueList
list={objectValue}
onChange={setObjectValue}
/>
)}
{type === ChatVarType.ArrayString && !editInJSON && (
<ArrayValueList
isString
list={value || [undefined]}
onChange={setValue}
/>
)}
{type === ChatVarType.ArrayNumber && !editInJSON && (
<ArrayValueList
isString={false}
list={value || [undefined]}
onChange={setValue}
/>
)}
{editInJSON && (
<div className='w-full rounded-[10px] bg-components-input-bg-normal py-2 pl-3 pr-1' style={{ height: editorMinHeight }}>
<CodeEditor
isExpand
noWrapper
language={CodeLanguage.json}
value={editorContent}
placeholder={<div className='whitespace-pre'>{placeholder}</div>}
onChange={handleEditorValueChange}
/> />
</div> )}
)} {type === ChatVarType.Object && !editInJSON && (
<ObjectValueList
list={objectValue}
onChange={setObjectValue}
/>
)}
{type === ChatVarType.ArrayString && !editInJSON && (
<ArrayValueList
isString
list={value || [undefined]}
onChange={setValue}
/>
)}
{type === ChatVarType.ArrayNumber && !editInJSON && (
<ArrayValueList
isString={false}
list={value || [undefined]}
onChange={setValue}
/>
)}
{editInJSON && (
<div className='w-full rounded-[10px] bg-components-input-bg-normal py-2 pl-3 pr-1' style={{ height: editorMinHeight }}>
<CodeEditor
isExpand
noWrapper
language={CodeLanguage.json}
value={editorContent}
placeholder={<div className='whitespace-pre'>{placeholder}</div>}
onChange={handleEditorValueChange}
/>
</div>
)}
</div>
</div> </div>
</div> )}
{/* description */} {/* description */}
<div className=''> <div className=''>
<div className='system-sm-semibold mb-1 flex h-6 items-center text-text-secondary'>{t('workflow.chatVariable.modal.description')}</div> <div className='system-sm-semibold mb-1 flex h-6 items-center text-text-secondary'>{t('workflow.chatVariable.modal.description')}</div>

@ -5,4 +5,5 @@ export enum ChatVarType {
ArrayString = 'array[string]', ArrayString = 'array[string]',
ArrayNumber = 'array[number]', ArrayNumber = 'array[number]',
ArrayObject = 'array[object]', ArrayObject = 'array[object]',
ArrayFile = 'array[file]',
} }

@ -3,6 +3,7 @@ import type { WorkflowDataUpdater } from '../types'
import Run from '../run' import Run from '../run'
import { useStore } from '../store' import { useStore } from '../store'
import { useWorkflowUpdate } from '../hooks' import { useWorkflowUpdate } from '../hooks'
import { formatWorkflowRunIdentifier } from '../utils'
const Record = () => { const Record = () => {
const historyWorkflowData = useStore(s => s.historyWorkflowData) const historyWorkflowData = useStore(s => s.historyWorkflowData)
@ -20,7 +21,7 @@ const Record = () => {
return ( return (
<div className='flex h-full w-[400px] flex-col rounded-l-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl'> <div className='flex h-full w-[400px] flex-col rounded-l-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl'>
<div className='system-xl-semibold flex items-center justify-between p-4 pb-0 text-text-primary'> <div className='system-xl-semibold flex items-center justify-between p-4 pb-0 text-text-primary'>
{`Test Run#${historyWorkflowData?.sequence_number}`} {`Test Run${formatWorkflowRunIdentifier(historyWorkflowData?.finished_at)}`}
</div> </div>
<Run <Run
runID={historyWorkflowData?.id || ''} runID={historyWorkflowData?.id || ''}

@ -20,6 +20,7 @@ import { useStore } from '../store'
import { import {
WorkflowRunningStatus, WorkflowRunningStatus,
} from '../types' } from '../types'
import { formatWorkflowRunIdentifier } from '../utils'
import Toast from '../../base/toast' import Toast from '../../base/toast'
import InputsPanel from './inputs-panel' import InputsPanel from './inputs-panel'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
@ -88,7 +89,7 @@ const WorkflowPreview = () => {
onMouseDown={startResizing} onMouseDown={startResizing}
/> />
<div className='flex items-center justify-between p-4 pb-1 text-base font-semibold text-text-primary'> <div className='flex items-center justify-between p-4 pb-1 text-base font-semibold text-text-primary'>
{`Test Run${!workflowRunningData?.result.sequence_number ? '' : `#${workflowRunningData?.result.sequence_number}`}`} {`Test Run${formatWorkflowRunIdentifier(workflowRunningData?.result.finished_at)}`}
<div className='cursor-pointer p-1' onClick={() => handleCancelDebugAndPreviewPanel()}> <div className='cursor-pointer p-1' onClick={() => handleCancelDebugAndPreviewPanel()}>
<RiCloseLine className='h-4 w-4 text-text-tertiary' /> <RiCloseLine className='h-4 w-4 text-text-tertiary' />
</div> </div>

@ -360,7 +360,6 @@ export type WorkflowRunningData = {
message_id?: string message_id?: string
conversation_id?: string conversation_id?: string
result: { result: {
sequence_number?: number
workflow_id?: string workflow_id?: string
inputs?: string inputs?: string
process_data?: string process_data?: string
@ -383,9 +382,9 @@ export type WorkflowRunningData = {
export type HistoryWorkflowData = { export type HistoryWorkflowData = {
id: string id: string
sequence_number: number
status: string status: string
conversation_id?: string conversation_id?: string
finished_at?: number
} }
export enum ChangeType { export enum ChangeType {

@ -86,6 +86,8 @@ const UpdateDSLModal = ({
graph, graph,
features, features,
hash, hash,
conversation_variables,
environment_variables,
} = await fetchWorkflowDraft(`/apps/${app_id}/workflows/draft`) } = await fetchWorkflowDraft(`/apps/${app_id}/workflows/draft`)
const { nodes, edges, viewport } = graph const { nodes, edges, viewport } = graph
@ -122,6 +124,8 @@ const UpdateDSLModal = ({
viewport, viewport,
features: newFeatures, features: newFeatures,
hash, hash,
conversation_variables: conversation_variables || [],
environment_variables: environment_variables || [],
}, },
} as any) } as any)
}, [eventEmitter]) }, [eventEmitter])

@ -33,3 +33,22 @@ export const isEventTargetInputArea = (target: HTMLElement) => {
if (target.contentEditable === 'true') if (target.contentEditable === 'true')
return true return true
} }
/**
* Format workflow run identifier using finished_at timestamp
* @param finishedAt - Unix timestamp in seconds
* @param fallbackText - Text to show when finishedAt is not available (default: 'Running')
* @returns Formatted string like " (14:30:25)" or " (Running)"
*/
export const formatWorkflowRunIdentifier = (finishedAt?: number, fallbackText = 'Running'): string => {
if (!finishedAt)
return ` (${fallbackText})`
const date = new Date(finishedAt * 1000)
const timeStr = date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
return ` (${timeStr})`
}

@ -19,19 +19,87 @@ import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants' import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants'
export const getLayoutByDagre = (originNodes: Node[], originEdges: Edge[]) => { export const getLayoutByDagre = (originNodes: Node[], originEdges: Edge[]) => {
const dagreGraph = new dagre.graphlib.Graph() const dagreGraph = new dagre.graphlib.Graph({ compound: true })
dagreGraph.setDefaultEdgeLabel(() => ({})) dagreGraph.setDefaultEdgeLabel(() => ({}))
const nodes = cloneDeep(originNodes).filter(node => !node.parentId && node.type === CUSTOM_NODE) const nodes = cloneDeep(originNodes).filter(node => !node.parentId && node.type === CUSTOM_NODE)
const edges = cloneDeep(originEdges).filter(edge => (!edge.data?.isInIteration && !edge.data?.isInLoop)) const edges = cloneDeep(originEdges).filter(edge => (!edge.data?.isInIteration && !edge.data?.isInLoop))
// The default dagre layout algorithm often fails to correctly order the branches
// of an If/Else node, leading to crossed edges.
//
// To solve this, we employ a "virtual container" strategy:
// 1. A virtual, compound parent node (the "container") is created for each If/Else node's branches.
// 2. Each direct child of the If/Else node is preceded by a virtual dummy node. These dummies are placed inside the container.
// 3. A rigid, sequential chain of invisible edges is created between these dummy nodes (e.g., dummy_IF -> dummy_ELIF -> dummy_ELSE).
//
// This forces dagre to treat the ordered branches as an unbreakable, atomic group,
// ensuring their layout respects the intended logical sequence.
const ifElseNodes = nodes.filter(node => node.data.type === BlockEnum.IfElse)
let virtualLogicApplied = false
ifElseNodes.forEach((ifElseNode) => {
const childEdges = edges.filter(e => e.source === ifElseNode.id)
if (childEdges.length <= 1)
return
virtualLogicApplied = true
const sortedChildEdges = childEdges.sort((edgeA, edgeB) => {
const handleA = edgeA.sourceHandle
const handleB = edgeB.sourceHandle
if (handleA && handleB) {
const cases = (ifElseNode.data as any).cases || []
const isAElse = handleA === 'false'
const isBElse = handleB === 'false'
if (isAElse) return 1
if (isBElse) return -1
const indexA = cases.findIndex((c: any) => c.case_id === handleA)
const indexB = cases.findIndex((c: any) => c.case_id === handleB)
if (indexA !== -1 && indexB !== -1)
return indexA - indexB
}
return 0
})
const parentDummyId = `dummy-parent-${ifElseNode.id}`
dagreGraph.setNode(parentDummyId, { width: 1, height: 1 })
const dummyNodes: string[] = []
sortedChildEdges.forEach((edge) => {
const dummyNodeId = `dummy-${edge.source}-${edge.target}`
dummyNodes.push(dummyNodeId)
dagreGraph.setNode(dummyNodeId, { width: 1, height: 1 })
dagreGraph.setParent(dummyNodeId, parentDummyId)
const edgeIndex = edges.findIndex(e => e.id === edge.id)
if (edgeIndex > -1)
edges.splice(edgeIndex, 1)
edges.push({ id: `e-${edge.source}-${dummyNodeId}`, source: edge.source, target: dummyNodeId, sourceHandle: edge.sourceHandle } as Edge)
edges.push({ id: `e-${dummyNodeId}-${edge.target}`, source: dummyNodeId, target: edge.target, targetHandle: edge.targetHandle } as Edge)
})
for (let i = 0; i < dummyNodes.length - 1; i++) {
const sourceDummy = dummyNodes[i]
const targetDummy = dummyNodes[i + 1]
edges.push({ id: `e-dummy-${sourceDummy}-${targetDummy}`, source: sourceDummy, target: targetDummy } as Edge)
}
})
dagreGraph.setGraph({ dagreGraph.setGraph({
rankdir: 'LR', rankdir: 'LR',
align: 'UL', align: 'UL',
nodesep: 40, nodesep: 40,
ranksep: 60, ranksep: virtualLogicApplied ? 30 : 60,
ranker: 'tight-tree', ranker: 'tight-tree',
marginx: 30, marginx: 30,
marginy: 200, marginy: 200,
}) })
nodes.forEach((node) => { nodes.forEach((node) => {
dagreGraph.setNode(node.id, { dagreGraph.setNode(node.id, {
width: node.width!, width: node.width!,

@ -1,7 +1,7 @@
export * from './node' export * from './node'
export * from './edge' export * from './edge'
export * from './workflow-init' export * from './workflow-init'
export * from './layout' export * from './dagre-layout'
export * from './common' export * from './common'
export * from './tool' export * from './tool'
export * from './workflow' export * from './workflow'

@ -1,30 +0,0 @@
'use client'
import type { FC } from 'react'
import classNames from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useTheme } from 'next-themes'
type LoginLogoProps = {
className?: string
}
const LoginLogo: FC<LoginLogoProps> = ({
className,
}) => {
const { systemFeatures } = useGlobalPublicStore()
const { theme } = useTheme()
let src = theme === 'light' ? '/logo/logo-site.png' : `/logo/logo-site-${theme}.png`
if (systemFeatures.branding.enabled)
src = systemFeatures.branding.login_page_logo
return (
<img
src={src}
className={classNames('block w-auto h-10', className)}
alt='logo'
/>
)
}
export default LoginLogo

@ -34,7 +34,7 @@ const SSOAuth: FC<SSOAuthProps> = ({
} }
else if (protocol === SSOProtocol.OIDC) { else if (protocol === SSOProtocol.OIDC) {
getUserOIDCSSOUrl(invite_token).then((res) => { getUserOIDCSSOUrl(invite_token).then((res) => {
document.cookie = `user-oidc-state=${res.state}` document.cookie = `user-oidc-state=${res.state};Path=/`
router.push(res.url) router.push(res.url)
}).finally(() => { }).finally(() => {
setIsLoading(false) setIsLoading(false)
@ -42,7 +42,7 @@ const SSOAuth: FC<SSOAuthProps> = ({
} }
else if (protocol === SSOProtocol.OAuth2) { else if (protocol === SSOProtocol.OAuth2) {
getUserOAuth2SSOUrl(invite_token).then((res) => { getUserOAuth2SSOUrl(invite_token).then((res) => {
document.cookie = `user-oauth2-state=${res.state}` document.cookie = `user-oauth2-state=${res.state};Path=/`
router.push(res.url) router.push(res.url)
}).finally(() => { }).finally(() => {
setIsLoading(false) setIsLoading(false)

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

Loading…
Cancel
Save