Merge branch 'main' into feat/rag-pipeline

pull/21398/head
twwu 11 months ago
commit 387826674c

@ -84,10 +84,8 @@ jobs:
elasticsearch elasticsearch
oceanbase oceanbase
- name: Check VDB Ready (TiDB, Oceanbase) - name: Check VDB Ready (TiDB)
run: | run: uv run --project api python api/tests/integration_tests/vdb/tidb_vector/check_tiflash_ready.py
uv run --project api python api/tests/integration_tests/vdb/tidb_vector/check_tiflash_ready.py
uv run --project api python api/tests/integration_tests/vdb/oceanbase/check_oceanbase_ready.py
- name: Test Vector Stores - name: Test Vector Stores
run: uv run --project api bash dev/pytest/pytest_vdb.sh run: uv run --project api bash dev/pytest/pytest_vdb.sh

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

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

@ -5,7 +5,11 @@ from werkzeug.exceptions import Forbidden, NotFound
import services.dataset_service import services.dataset_service
from controllers.service_api import api from controllers.service_api import api
from controllers.service_api.dataset.error import DatasetInUseError, DatasetNameDuplicateError from controllers.service_api.dataset.error import DatasetInUseError, DatasetNameDuplicateError
from controllers.service_api.wraps import DatasetApiResource, validate_dataset_token from controllers.service_api.wraps import (
DatasetApiResource,
cloud_edition_billing_rate_limit_check,
validate_dataset_token,
)
from core.model_runtime.entities.model_entities import ModelType from core.model_runtime.entities.model_entities import ModelType
from core.plugin.entities.plugin import ModelProviderID from core.plugin.entities.plugin import ModelProviderID
from core.provider_manager import ProviderManager from core.provider_manager import ProviderManager
@ -70,6 +74,7 @@ class DatasetListApi(DatasetApiResource):
response = {"data": data, "has_more": len(datasets) == limit, "limit": limit, "total": total, "page": page} response = {"data": data, "has_more": len(datasets) == limit, "limit": limit, "total": total, "page": page}
return response, 200 return response, 200
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
def post(self, tenant_id): def post(self, tenant_id):
"""Resource for creating datasets.""" """Resource for creating datasets."""
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
@ -193,6 +198,7 @@ class DatasetApi(DatasetApiResource):
return data, 200 return data, 200
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
def patch(self, _, dataset_id): def patch(self, _, dataset_id):
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str) dataset = DatasetService.get_dataset(dataset_id_str)
@ -293,6 +299,7 @@ class DatasetApi(DatasetApiResource):
return result_data, 200 return result_data, 200
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
def delete(self, _, dataset_id): def delete(self, _, dataset_id):
""" """
Deletes a dataset given its ID. Deletes a dataset given its ID.

@ -19,7 +19,11 @@ from controllers.service_api.dataset.error import (
ArchivedDocumentImmutableError, ArchivedDocumentImmutableError,
DocumentIndexingError, DocumentIndexingError,
) )
from controllers.service_api.wraps import DatasetApiResource, cloud_edition_billing_resource_check from controllers.service_api.wraps import (
DatasetApiResource,
cloud_edition_billing_rate_limit_check,
cloud_edition_billing_resource_check,
)
from core.errors.error import ProviderTokenNotInitError from core.errors.error import ProviderTokenNotInitError
from extensions.ext_database import db from extensions.ext_database import db
from fields.document_fields import document_fields, document_status_fields from fields.document_fields import document_fields, document_status_fields
@ -35,6 +39,7 @@ class DocumentAddByTextApi(DatasetApiResource):
@cloud_edition_billing_resource_check("vector_space", "dataset") @cloud_edition_billing_resource_check("vector_space", "dataset")
@cloud_edition_billing_resource_check("documents", "dataset") @cloud_edition_billing_resource_check("documents", "dataset")
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
def post(self, tenant_id, dataset_id): def post(self, tenant_id, dataset_id):
"""Create document by text.""" """Create document by text."""
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
@ -99,6 +104,7 @@ class DocumentUpdateByTextApi(DatasetApiResource):
"""Resource for update documents.""" """Resource for update documents."""
@cloud_edition_billing_resource_check("vector_space", "dataset") @cloud_edition_billing_resource_check("vector_space", "dataset")
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
def post(self, tenant_id, dataset_id, document_id): def post(self, tenant_id, dataset_id, document_id):
"""Update document by text.""" """Update document by text."""
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
@ -158,6 +164,7 @@ class DocumentAddByFileApi(DatasetApiResource):
@cloud_edition_billing_resource_check("vector_space", "dataset") @cloud_edition_billing_resource_check("vector_space", "dataset")
@cloud_edition_billing_resource_check("documents", "dataset") @cloud_edition_billing_resource_check("documents", "dataset")
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
def post(self, tenant_id, dataset_id): def post(self, tenant_id, dataset_id):
"""Create document by upload file.""" """Create document by upload file."""
args = {} args = {}
@ -232,6 +239,7 @@ class DocumentUpdateByFileApi(DatasetApiResource):
"""Resource for update documents.""" """Resource for update documents."""
@cloud_edition_billing_resource_check("vector_space", "dataset") @cloud_edition_billing_resource_check("vector_space", "dataset")
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
def post(self, tenant_id, dataset_id, document_id): def post(self, tenant_id, dataset_id, document_id):
"""Update document by upload file.""" """Update document by upload file."""
args = {} args = {}
@ -302,6 +310,7 @@ class DocumentUpdateByFileApi(DatasetApiResource):
class DocumentDeleteApi(DatasetApiResource): class DocumentDeleteApi(DatasetApiResource):
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
def delete(self, tenant_id, dataset_id, document_id): def delete(self, tenant_id, dataset_id, document_id):
"""Delete document.""" """Delete document."""
document_id = str(document_id) document_id = str(document_id)

@ -1,9 +1,10 @@
from controllers.console.datasets.hit_testing_base import DatasetsHitTestingBase from controllers.console.datasets.hit_testing_base import DatasetsHitTestingBase
from controllers.service_api import api from controllers.service_api import api
from controllers.service_api.wraps import DatasetApiResource from controllers.service_api.wraps import DatasetApiResource, cloud_edition_billing_rate_limit_check
class HitTestingApi(DatasetApiResource, DatasetsHitTestingBase): class HitTestingApi(DatasetApiResource, DatasetsHitTestingBase):
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
def post(self, tenant_id, dataset_id): def post(self, tenant_id, dataset_id):
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)

@ -3,7 +3,7 @@ from flask_restful import marshal, reqparse
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from controllers.service_api import api from controllers.service_api import api
from controllers.service_api.wraps import DatasetApiResource from controllers.service_api.wraps import DatasetApiResource, cloud_edition_billing_rate_limit_check
from fields.dataset_fields import dataset_metadata_fields from fields.dataset_fields import dataset_metadata_fields
from services.dataset_service import DatasetService from services.dataset_service import DatasetService
from services.entities.knowledge_entities.knowledge_entities import ( from services.entities.knowledge_entities.knowledge_entities import (
@ -14,6 +14,7 @@ from services.metadata_service import MetadataService
class DatasetMetadataCreateServiceApi(DatasetApiResource): class DatasetMetadataCreateServiceApi(DatasetApiResource):
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
def post(self, tenant_id, dataset_id): def post(self, tenant_id, dataset_id):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("type", type=str, required=True, nullable=True, location="json") parser.add_argument("type", type=str, required=True, nullable=True, location="json")
@ -39,6 +40,7 @@ class DatasetMetadataCreateServiceApi(DatasetApiResource):
class DatasetMetadataServiceApi(DatasetApiResource): class DatasetMetadataServiceApi(DatasetApiResource):
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
def patch(self, tenant_id, dataset_id, metadata_id): def patch(self, tenant_id, dataset_id, metadata_id):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("name", type=str, required=True, nullable=True, location="json") parser.add_argument("name", type=str, required=True, nullable=True, location="json")
@ -54,6 +56,7 @@ class DatasetMetadataServiceApi(DatasetApiResource):
metadata = MetadataService.update_metadata_name(dataset_id_str, metadata_id_str, args.get("name")) metadata = MetadataService.update_metadata_name(dataset_id_str, metadata_id_str, args.get("name"))
return marshal(metadata, dataset_metadata_fields), 200 return marshal(metadata, dataset_metadata_fields), 200
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
def delete(self, tenant_id, dataset_id, metadata_id): def delete(self, tenant_id, dataset_id, metadata_id):
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)
metadata_id_str = str(metadata_id) metadata_id_str = str(metadata_id)
@ -73,6 +76,7 @@ class DatasetMetadataBuiltInFieldServiceApi(DatasetApiResource):
class DatasetMetadataBuiltInFieldActionServiceApi(DatasetApiResource): class DatasetMetadataBuiltInFieldActionServiceApi(DatasetApiResource):
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
def post(self, tenant_id, dataset_id, action): def post(self, tenant_id, dataset_id, action):
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str) dataset = DatasetService.get_dataset(dataset_id_str)
@ -88,6 +92,7 @@ class DatasetMetadataBuiltInFieldActionServiceApi(DatasetApiResource):
class DocumentMetadataEditServiceApi(DatasetApiResource): class DocumentMetadataEditServiceApi(DatasetApiResource):
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
def post(self, tenant_id, dataset_id): def post(self, tenant_id, dataset_id):
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str) dataset = DatasetService.get_dataset(dataset_id_str)

@ -8,6 +8,7 @@ from controllers.service_api.app.error import ProviderNotInitializeError
from controllers.service_api.wraps import ( from controllers.service_api.wraps import (
DatasetApiResource, DatasetApiResource,
cloud_edition_billing_knowledge_limit_check, cloud_edition_billing_knowledge_limit_check,
cloud_edition_billing_rate_limit_check,
cloud_edition_billing_resource_check, cloud_edition_billing_resource_check,
) )
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
@ -35,6 +36,7 @@ class SegmentApi(DatasetApiResource):
@cloud_edition_billing_resource_check("vector_space", "dataset") @cloud_edition_billing_resource_check("vector_space", "dataset")
@cloud_edition_billing_knowledge_limit_check("add_segment", "dataset") @cloud_edition_billing_knowledge_limit_check("add_segment", "dataset")
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
def post(self, tenant_id, dataset_id, document_id): def post(self, tenant_id, dataset_id, document_id):
"""Create single segment.""" """Create single segment."""
# check dataset # check dataset
@ -139,6 +141,7 @@ class SegmentApi(DatasetApiResource):
class DatasetSegmentApi(DatasetApiResource): class DatasetSegmentApi(DatasetApiResource):
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
def delete(self, tenant_id, dataset_id, document_id, segment_id): def delete(self, tenant_id, dataset_id, document_id, segment_id):
# check dataset # check dataset
dataset_id = str(dataset_id) dataset_id = str(dataset_id)
@ -162,6 +165,7 @@ class DatasetSegmentApi(DatasetApiResource):
return 204 return 204
@cloud_edition_billing_resource_check("vector_space", "dataset") @cloud_edition_billing_resource_check("vector_space", "dataset")
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
def post(self, tenant_id, dataset_id, document_id, segment_id): def post(self, tenant_id, dataset_id, document_id, segment_id):
# check dataset # check dataset
dataset_id = str(dataset_id) dataset_id = str(dataset_id)
@ -236,6 +240,7 @@ class ChildChunkApi(DatasetApiResource):
@cloud_edition_billing_resource_check("vector_space", "dataset") @cloud_edition_billing_resource_check("vector_space", "dataset")
@cloud_edition_billing_knowledge_limit_check("add_segment", "dataset") @cloud_edition_billing_knowledge_limit_check("add_segment", "dataset")
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
def post(self, tenant_id, dataset_id, document_id, segment_id): def post(self, tenant_id, dataset_id, document_id, segment_id):
"""Create child chunk.""" """Create child chunk."""
# check dataset # check dataset
@ -332,6 +337,7 @@ class DatasetChildChunkApi(DatasetApiResource):
"""Resource for updating child chunks.""" """Resource for updating child chunks."""
@cloud_edition_billing_knowledge_limit_check("add_segment", "dataset") @cloud_edition_billing_knowledge_limit_check("add_segment", "dataset")
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
def delete(self, tenant_id, dataset_id, document_id, segment_id, child_chunk_id): def delete(self, tenant_id, dataset_id, document_id, segment_id, child_chunk_id):
"""Delete child chunk.""" """Delete child chunk."""
# check dataset # check dataset
@ -370,6 +376,7 @@ class DatasetChildChunkApi(DatasetApiResource):
@cloud_edition_billing_resource_check("vector_space", "dataset") @cloud_edition_billing_resource_check("vector_space", "dataset")
@cloud_edition_billing_knowledge_limit_check("add_segment", "dataset") @cloud_edition_billing_knowledge_limit_check("add_segment", "dataset")
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
def patch(self, tenant_id, dataset_id, document_id, segment_id, child_chunk_id): def patch(self, tenant_id, dataset_id, document_id, segment_id, child_chunk_id):
"""Update child chunk.""" """Update child chunk."""
# check dataset # check dataset

@ -138,14 +138,11 @@ class DatasetConfigManager:
if not config.get("dataset_configs"): if not config.get("dataset_configs"):
config["dataset_configs"] = {"retrieval_model": "single"} config["dataset_configs"] = {"retrieval_model": "single"}
if not config["dataset_configs"].get("datasets"):
config["dataset_configs"]["datasets"] = {"strategy": "router", "datasets": []}
if not isinstance(config["dataset_configs"], dict): if not isinstance(config["dataset_configs"], dict):
raise ValueError("dataset_configs must be of object type") raise ValueError("dataset_configs must be of object type")
if not isinstance(config["dataset_configs"], dict): if not config["dataset_configs"].get("datasets"):
raise ValueError("dataset_configs must be of object type") config["dataset_configs"]["datasets"] = {"strategy": "router", "datasets": []}
need_manual_query_datasets = config.get("dataset_configs") and config["dataset_configs"].get( need_manual_query_datasets = config.get("dataset_configs") and config["dataset_configs"].get(
"datasets", {} "datasets", {}

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

@ -542,8 +542,6 @@ class LBModelManager:
return config return config
return None
def cooldown(self, config: ModelLoadBalancingConfiguration, expire: int = 60) -> None: def cooldown(self, config: ModelLoadBalancingConfiguration, expire: int = 60) -> None:
""" """
Cooldown model load balancing config Cooldown model load balancing config

@ -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]:

@ -22,6 +22,7 @@ class FirecrawlApp:
"formats": ["markdown"], "formats": ["markdown"],
"onlyMainContent": True, "onlyMainContent": True,
"timeout": 30000, "timeout": 30000,
"integration": "dify",
} }
if params: if params:
json_data.update(params) json_data.update(params)
@ -39,7 +40,7 @@ class FirecrawlApp:
def crawl_url(self, url, params=None) -> str: def crawl_url(self, url, params=None) -> str:
# Documentation: https://docs.firecrawl.dev/api-reference/endpoint/crawl-post # Documentation: https://docs.firecrawl.dev/api-reference/endpoint/crawl-post
headers = self._prepare_headers() headers = self._prepare_headers()
json_data = {"url": url} json_data = {"url": url, "integration": "dify"}
if params: if params:
json_data.update(params) json_data.update(params)
response = self._post_request(f"{self.base_url}/v1/crawl", json_data, headers) response = self._post_request(f"{self.base_url}/v1/crawl", json_data, headers)
@ -49,7 +50,6 @@ class FirecrawlApp:
return cast(str, job_id) return cast(str, job_id)
else: else:
self._handle_error(response, "start crawl job") self._handle_error(response, "start crawl job")
# FIXME: unreachable code for mypy
return "" # unreachable return "" # unreachable
def check_crawl_status(self, job_id) -> dict[str, Any]: def check_crawl_status(self, job_id) -> dict[str, Any]:
@ -82,7 +82,6 @@ class FirecrawlApp:
) )
else: else:
self._handle_error(response, "check crawl status") self._handle_error(response, "check crawl status")
# FIXME: unreachable code for mypy
return {} # unreachable return {} # unreachable
def _format_crawl_status_response( def _format_crawl_status_response(
@ -126,4 +125,31 @@ class FirecrawlApp:
def _handle_error(self, response, action) -> None: def _handle_error(self, response, action) -> None:
error_message = response.json().get("error", "Unknown error occurred") error_message = response.json().get("error", "Unknown error occurred")
raise Exception(f"Failed to {action}. Status code: {response.status_code}. Error: {error_message}") raise Exception(f"Failed to {action}. Status code: {response.status_code}. Error: {error_message}") # type: ignore[return]
def search(self, query: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
# Documentation: https://docs.firecrawl.dev/api-reference/endpoint/search
headers = self._prepare_headers()
json_data = {
"query": query,
"limit": 5,
"lang": "en",
"country": "us",
"timeout": 60000,
"ignoreInvalidURLs": False,
"scrapeOptions": {},
"integration": "dify",
}
if params:
json_data.update(params)
response = self._post_request(f"{self.base_url}/v1/search", json_data, headers)
if response.status_code == 200:
response_data = response.json()
if not response_data.get("success"):
raise Exception(f"Search failed. Error: {response_data.get('warning', 'Unknown error')}")
return cast(dict[str, Any], response_data)
elif response.status_code in {402, 409, 500, 429, 408}:
self._handle_error(response, "perform search")
return {} # Avoid additional exception after handling error
else:
raise Exception(f"Failed to perform search. Status code: {response.status_code}")

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

@ -496,6 +496,8 @@ class DatasetRetrieval:
all_documents = self.calculate_keyword_score(query, all_documents, top_k) all_documents = self.calculate_keyword_score(query, all_documents, top_k)
elif index_type == "high_quality": elif index_type == "high_quality":
all_documents = self.calculate_vector_score(all_documents, top_k, score_threshold) all_documents = self.calculate_vector_score(all_documents, top_k, score_threshold)
else:
all_documents = all_documents[:top_k] if top_k else all_documents
self._on_query(query, dataset_ids, app_id, user_from, user_id) self._on_query(query, dataset_ids, app_id, user_from, user_id)

@ -151,12 +151,17 @@ class SQLAlchemyWorkflowExecutionRepository(WorkflowExecutionRepository):
existing = session.scalar(select(WorkflowRun).where(WorkflowRun.id == domain_model.id_)) existing = session.scalar(select(WorkflowRun).where(WorkflowRun.id == domain_model.id_))
if not existing: if not existing:
# For new records, get the next sequence number # For new records, get the next sequence number
stmt = select(func.max(WorkflowRun.sequence_number)).where( # in case multiple executions are created concurrently, use for update
stmt = (
select(func.coalesce(func.max(WorkflowRun.sequence_number), 0) + 1)
.where(
WorkflowRun.app_id == self._app_id, WorkflowRun.app_id == self._app_id,
WorkflowRun.tenant_id == self._tenant_id, WorkflowRun.tenant_id == self._tenant_id,
) )
max_sequence = session.scalar(stmt) .with_for_update()
db_model.sequence_number = (max_sequence or 0) + 1 )
next_seq = session.scalar(stmt)
db_model.sequence_number = int(next_seq) if next_seq is not None else 1
else: else:
# For updates, keep the existing sequence number # For updates, keep the existing sequence number
db_model.sequence_number = existing.sequence_number db_model.sequence_number = existing.sequence_number

@ -639,26 +639,19 @@ class GraphEngine:
retry_start_at = datetime.now(UTC).replace(tzinfo=None) retry_start_at = datetime.now(UTC).replace(tzinfo=None)
# yield control to other threads # yield control to other threads
time.sleep(0.001) time.sleep(0.001)
generator = node_instance.run() event_stream = node_instance.run()
for item in generator: for event in event_stream:
if isinstance(item, GraphEngineEvent): if isinstance(event, GraphEngineEvent):
if isinstance(item, BaseIterationEvent):
# add parallel info to iteration event # add parallel info to iteration event
item.parallel_id = parallel_id if isinstance(event, BaseIterationEvent | BaseLoopEvent):
item.parallel_start_node_id = parallel_start_node_id event.parallel_id = parallel_id
item.parent_parallel_id = parent_parallel_id event.parallel_start_node_id = parallel_start_node_id
item.parent_parallel_start_node_id = parent_parallel_start_node_id event.parent_parallel_id = parent_parallel_id
elif isinstance(item, BaseLoopEvent): event.parent_parallel_start_node_id = parent_parallel_start_node_id
# add parallel info to loop event yield event
item.parallel_id = parallel_id
item.parallel_start_node_id = parallel_start_node_id
item.parent_parallel_id = parent_parallel_id
item.parent_parallel_start_node_id = parent_parallel_start_node_id
yield item
else: else:
if isinstance(item, RunCompletedEvent): if isinstance(event, RunCompletedEvent):
run_result = item.run_result run_result = event.run_result
if run_result.status == WorkflowNodeExecutionStatus.FAILED: if run_result.status == WorkflowNodeExecutionStatus.FAILED:
if ( if (
retries == max_retries retries == max_retries
@ -694,7 +687,7 @@ class GraphEngine:
# if run failed, handle error # if run failed, handle error
run_result = self._handle_continue_on_error( run_result = self._handle_continue_on_error(
node_instance, node_instance,
item.run_result, event.run_result,
self.graph_runtime_state.variable_pool, self.graph_runtime_state.variable_pool,
handle_exceptions=handle_exceptions, handle_exceptions=handle_exceptions,
) )
@ -797,28 +790,28 @@ class GraphEngine:
should_continue_retry = False should_continue_retry = False
break break
elif isinstance(item, RunStreamChunkEvent): elif isinstance(event, RunStreamChunkEvent):
yield NodeRunStreamChunkEvent( yield NodeRunStreamChunkEvent(
id=node_instance.id, id=node_instance.id,
node_id=node_instance.node_id, node_id=node_instance.node_id,
node_type=node_instance.node_type, node_type=node_instance.node_type,
node_data=node_instance.node_data, node_data=node_instance.node_data,
chunk_content=item.chunk_content, chunk_content=event.chunk_content,
from_variable_selector=item.from_variable_selector, from_variable_selector=event.from_variable_selector,
route_node_state=route_node_state, route_node_state=route_node_state,
parallel_id=parallel_id, parallel_id=parallel_id,
parallel_start_node_id=parallel_start_node_id, parallel_start_node_id=parallel_start_node_id,
parent_parallel_id=parent_parallel_id, parent_parallel_id=parent_parallel_id,
parent_parallel_start_node_id=parent_parallel_start_node_id, parent_parallel_start_node_id=parent_parallel_start_node_id,
) )
elif isinstance(item, RunRetrieverResourceEvent): elif isinstance(event, RunRetrieverResourceEvent):
yield NodeRunRetrieverResourceEvent( yield NodeRunRetrieverResourceEvent(
id=node_instance.id, id=node_instance.id,
node_id=node_instance.node_id, node_id=node_instance.node_id,
node_type=node_instance.node_type, node_type=node_instance.node_type,
node_data=node_instance.node_data, node_data=node_instance.node_data,
retriever_resources=item.retriever_resources, retriever_resources=event.retriever_resources,
context=item.context, context=event.context,
route_node_state=route_node_state, route_node_state=route_node_state,
parallel_id=parallel_id, parallel_id=parallel_id,
parallel_start_node_id=parallel_start_node_id, parallel_start_node_id=parallel_start_node_id,

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

@ -421,7 +421,7 @@ class AppDslService:
# Set icon type # Set icon type
icon_type_value = icon_type or app_data.get("icon_type") icon_type_value = icon_type or app_data.get("icon_type")
if icon_type_value in ["emoji", "link"]: if icon_type_value in ["emoji", "link", "image"]:
icon_type = icon_type_value icon_type = icon_type_value
else: else:
icon_type = "emoji" icon_type = "emoji"

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

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

@ -1,49 +0,0 @@
import time
import pymysql
def check_oceanbase_ready() -> bool:
try:
connection = pymysql.connect(
host="localhost",
port=2881,
user="root",
password="difyai123456",
)
affected_rows = connection.query("SELECT 1")
return affected_rows == 1
except Exception as e:
print(f"Oceanbase is not ready. Exception: {e}")
return False
finally:
if connection:
connection.close()
def main():
max_attempts = 50
retry_interval_seconds = 2
is_oceanbase_ready = False
for attempt in range(max_attempts):
try:
is_oceanbase_ready = check_oceanbase_ready()
except Exception as e:
print(f"Oceanbase is not ready. Exception: {e}")
is_oceanbase_ready = False
if is_oceanbase_ready:
break
else:
print(f"Attempt {attempt + 1} failed, retry in {retry_interval_seconds} seconds...")
time.sleep(retry_interval_seconds)
if is_oceanbase_ready:
print("Oceanbase is ready.")
else:
print(f"Oceanbase is not ready after {max_attempts} attempting checks.")
exit(1)
if __name__ == "__main__":
main()

@ -815,7 +815,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}
@ -451,6 +451,14 @@ services:
OB_CLUSTER_NAME: ${OCEANBASE_CLUSTER_NAME:-difyai} OB_CLUSTER_NAME: ${OCEANBASE_CLUSTER_NAME:-difyai}
OB_SERVER_IP: 127.0.0.1 OB_SERVER_IP: 127.0.0.1
MODE: mini MODE: mini
ports:
- "${OCEANBASE_VECTOR_PORT:-2881}:2881"
healthcheck:
test: [ 'CMD-SHELL', 'obclient -h127.0.0.1 -P2881 -uroot@test -p$${OB_TENANT_PASSWORD} -e "SELECT 1;"' ]
interval: 10s
retries: 30
start_period: 30s
timeout: 10s
# Oracle vector database # Oracle vector database
oracle: oracle:

@ -356,7 +356,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 +591,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}
@ -958,6 +958,14 @@ services:
OB_CLUSTER_NAME: ${OCEANBASE_CLUSTER_NAME:-difyai} OB_CLUSTER_NAME: ${OCEANBASE_CLUSTER_NAME:-difyai}
OB_SERVER_IP: 127.0.0.1 OB_SERVER_IP: 127.0.0.1
MODE: mini MODE: mini
ports:
- "${OCEANBASE_VECTOR_PORT:-2881}:2881"
healthcheck:
test: [ 'CMD-SHELL', 'obclient -h127.0.0.1 -P2881 -uroot@test -p$${OB_TENANT_PASSWORD} -e "SELECT 1;"' ]
interval: 10s
retries: 30
start_period: 30s
timeout: 10s
# Oracle vector database # Oracle vector database
oracle: oracle:

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

@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next'
import { useDebounceFn } from 'ahooks' import { useDebounceFn } from 'ahooks'
import { import {
RiApps2Line, RiApps2Line,
RiDragDropLine,
RiExchange2Line, RiExchange2Line,
RiFile4Line, RiFile4Line,
RiMessage3Line, RiMessage3Line,
@ -16,7 +17,8 @@ import {
} from '@remixicon/react' } from '@remixicon/react'
import AppCard from './AppCard' import AppCard from './AppCard'
import NewAppCard from './NewAppCard' import NewAppCard from './NewAppCard'
import useAppsQueryState from './hooks/useAppsQueryState' import useAppsQueryState from './hooks/use-apps-query-state'
import { useDSLDragDrop } from './hooks/use-dsl-drag-drop'
import type { AppListResponse } from '@/models/app' import type { AppListResponse } from '@/models/app'
import { fetchAppList } from '@/service/apps' import { fetchAppList } from '@/service/apps'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
@ -29,6 +31,7 @@ import { useStore as useTagStore } from '@/app/components/base/tag-management/st
import TagManagementModal from '@/app/components/base/tag-management' import TagManagementModal from '@/app/components/base/tag-management'
import TagFilter from '@/app/components/base/tag-management/filter' import TagFilter from '@/app/components/base/tag-management/filter'
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label' import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
import CreateFromDSLModal from '@/app/components/app/create-from-dsl-modal'
const getKey = ( const getKey = (
pageIndex: number, pageIndex: number,
@ -67,6 +70,9 @@ const Apps = () => {
const [tagFilterValue, setTagFilterValue] = useState<string[]>(tagIDs) const [tagFilterValue, setTagFilterValue] = useState<string[]>(tagIDs)
const [searchKeywords, setSearchKeywords] = useState(keywords) const [searchKeywords, setSearchKeywords] = useState(keywords)
const newAppCardRef = useRef<HTMLDivElement>(null) const newAppCardRef = useRef<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(false)
const [droppedDSLFile, setDroppedDSLFile] = useState<File | undefined>()
const setKeywords = useCallback((keywords: string) => { const setKeywords = useCallback((keywords: string) => {
setQuery(prev => ({ ...prev, keywords })) setQuery(prev => ({ ...prev, keywords }))
}, [setQuery]) }, [setQuery])
@ -74,6 +80,17 @@ const Apps = () => {
setQuery(prev => ({ ...prev, tagIDs })) setQuery(prev => ({ ...prev, tagIDs }))
}, [setQuery]) }, [setQuery])
const handleDSLFileDropped = useCallback((file: File) => {
setDroppedDSLFile(file)
setShowCreateFromDSLModal(true)
}, [])
const { dragging } = useDSLDragDrop({
onDSLFileDropped: handleDSLFileDropped,
containerRef,
enabled: isCurrentWorkspaceEditor,
})
const { data, isLoading, error, setSize, mutate } = useSWRInfinite( const { data, isLoading, error, setSize, mutate } = useSWRInfinite(
(pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, isCreatedByMe, tagIDs, searchKeywords), (pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, isCreatedByMe, tagIDs, searchKeywords),
fetchAppList, fetchAppList,
@ -151,6 +168,12 @@ const Apps = () => {
return ( return (
<> <>
<div ref={containerRef} className='relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body'>
{dragging && (
<div className="absolute inset-0 z-50 m-0.5 rounded-2xl border-2 border-dashed border-components-dropzone-border-accent bg-[rgba(21,90,239,0.14)] p-2">
</div>
)}
<div className='sticky top-0 z-10 flex flex-wrap items-center justify-between gap-y-2 bg-background-body px-12 pb-2 pt-4 leading-[56px]'> <div className='sticky top-0 z-10 flex flex-wrap items-center justify-between gap-y-2 bg-background-body px-12 pb-2 pt-4 leading-[56px]'>
<TabSliderNew <TabSliderNew
value={activeTab} value={activeTab}
@ -188,11 +211,39 @@ const Apps = () => {
&& <NewAppCard ref={newAppCardRef} className='z-10' onSuccess={mutate} />} && <NewAppCard ref={newAppCardRef} className='z-10' onSuccess={mutate} />}
<NoAppsFound /> <NoAppsFound />
</div>} </div>}
{isCurrentWorkspaceEditor && (
<div
className={`flex items-center justify-center gap-2 py-4 ${dragging ? 'text-text-accent' : 'text-text-quaternary'}`}
role="region"
aria-label={t('app.newApp.dropDSLToCreateApp')}
>
<RiDragDropLine className="h-4 w-4" />
<span className="system-xs-regular">{t('app.newApp.dropDSLToCreateApp')}</span>
</div>
)}
<CheckModal /> <CheckModal />
<div ref={anchorRef} className='h-0'> </div> <div ref={anchorRef} className='h-0'> </div>
{showTagManagementModal && ( {showTagManagementModal && (
<TagManagementModal type='app' show={showTagManagementModal} /> <TagManagementModal type='app' show={showTagManagementModal} />
)} )}
</div>
{showCreateFromDSLModal && (
<CreateFromDSLModal
show={showCreateFromDSLModal}
onClose={() => {
setShowCreateFromDSLModal(false)
setDroppedDSLFile(undefined)
}}
onSuccess={() => {
setShowCreateFromDSLModal(false)
setDroppedDSLFile(undefined)
mutate()
}}
droppedFile={droppedDSLFile}
/>
)}
</> </>
) )
} }

@ -0,0 +1,72 @@
import { useEffect, useState } from 'react'
type DSLDragDropHookProps = {
onDSLFileDropped: (file: File) => void
containerRef: React.RefObject<HTMLDivElement>
enabled?: boolean
}
export const useDSLDragDrop = ({ onDSLFileDropped, containerRef, enabled = true }: DSLDragDropHookProps) => {
const [dragging, setDragging] = useState(false)
const handleDragEnter = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
if (e.dataTransfer?.types.includes('Files'))
setDragging(true)
}
const handleDragOver = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
}
const handleDragLeave = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
if (e.relatedTarget === null || !containerRef.current?.contains(e.relatedTarget as Node))
setDragging(false)
}
const handleDrop = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
setDragging(false)
if (!e.dataTransfer)
return
const files = [...e.dataTransfer.files]
if (files.length === 0)
return
const file = files[0]
if (file.name.toLowerCase().endsWith('.yaml') || file.name.toLowerCase().endsWith('.yml'))
onDSLFileDropped(file)
}
useEffect(() => {
if (!enabled)
return
const current = containerRef.current
if (current) {
current.addEventListener('dragenter', handleDragEnter)
current.addEventListener('dragover', handleDragOver)
current.addEventListener('dragleave', handleDragLeave)
current.addEventListener('drop', handleDrop)
}
return () => {
if (current) {
current.removeEventListener('dragenter', handleDragEnter)
current.removeEventListener('dragover', handleDragOver)
current.removeEventListener('dragleave', handleDragLeave)
current.removeEventListener('drop', handleDrop)
}
}
}, [containerRef, enabled])
return {
dragging: enabled ? dragging : false,
}
}

@ -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,7 +1,7 @@
'use client' 'use client'
import type { MouseEventHandler } from 'react' import type { MouseEventHandler } from 'react'
import { useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -35,6 +35,7 @@ type CreateFromDSLModalProps = {
onClose: () => void onClose: () => void
activeTab?: string activeTab?: string
dslUrl?: string dslUrl?: string
droppedFile?: File
} }
export enum CreateFromDSLModalTab { export enum CreateFromDSLModalTab {
@ -42,11 +43,11 @@ export enum CreateFromDSLModalTab {
FROM_URL = 'from-url', FROM_URL = 'from-url',
} }
const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDSLModalTab.FROM_FILE, dslUrl = '' }: CreateFromDSLModalProps) => { const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDSLModalTab.FROM_FILE, dslUrl = '', droppedFile }: CreateFromDSLModalProps) => {
const { push } = useRouter() const { push } = useRouter()
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useContext(ToastContext) const { notify } = useContext(ToastContext)
const [currentFile, setDSLFile] = useState<File>() const [currentFile, setDSLFile] = useState<File | undefined>(droppedFile)
const [fileContent, setFileContent] = useState<string>() const [fileContent, setFileContent] = useState<string>()
const [currentTab, setCurrentTab] = useState(activeTab) const [currentTab, setCurrentTab] = useState(activeTab)
const [dslUrlValue, setDslUrlValue] = useState(dslUrl) const [dslUrlValue, setDslUrlValue] = useState(dslUrl)
@ -78,6 +79,11 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
const isCreatingRef = useRef(false) const isCreatingRef = useRef(false)
useEffect(() => {
if (droppedFile)
handleFile(droppedFile)
}, [droppedFile])
const onCreate: MouseEventHandler = async () => { const onCreate: MouseEventHandler = async () => {
if (currentTab === CreateFromDSLModalTab.FROM_FILE && !currentFile) if (currentTab === CreateFromDSLModalTab.FROM_FILE && !currentFile)
return return

@ -50,6 +50,10 @@ const OPTION_MAP = {
// user_id: 'YOU CAN DEFINE USER ID HERE', // user_id: 'YOU CAN DEFINE USER ID HERE',
// conversation_id: 'YOU CAN DEFINE CONVERSATION ID HERE, IT MUST BE A VALID UUID', // conversation_id: 'YOU CAN DEFINE CONVERSATION ID HERE, IT MUST BE A VALID UUID',
}, },
userVariables: {
// avatar_url: 'YOU CAN DEFINE USER AVATAR URL HERE',
// name: 'YOU CAN DEFINE USER NAME HERE',
},
} }
</script> </script>
<script <script

@ -25,6 +25,7 @@ import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested
import { Markdown } from '@/app/components/base/markdown' import { Markdown } from '@/app/components/base/markdown'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { FileEntity } from '../../file-uploader/types' import type { FileEntity } from '../../file-uploader/types'
import Avatar from '../../avatar'
const ChatWrapper = () => { const ChatWrapper = () => {
const { const {
@ -49,6 +50,7 @@ const ChatWrapper = () => {
setClearChatList, setClearChatList,
setIsResponding, setIsResponding,
allInputsHidden, allInputsHidden,
initUserVariables,
} = useEmbeddedChatbotContext() } = useEmbeddedChatbotContext()
const appConfig = useMemo(() => { const appConfig = useMemo(() => {
const config = appParams || {} const config = appParams || {}
@ -261,6 +263,14 @@ const ChatWrapper = () => {
switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)} switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
inputDisabled={inputDisabled} inputDisabled={inputDisabled}
isMobile={isMobile} isMobile={isMobile}
questionIcon={
initUserVariables?.avatar_url
? <Avatar
avatar={initUserVariables.avatar_url}
name={initUserVariables.name || 'user'}
size={40}
/> : undefined
}
/> />
) )
} }

@ -52,6 +52,10 @@ export type EmbeddedChatbotContextValue = {
currentConversationInputs: Record<string, any> | null, currentConversationInputs: Record<string, any> | null,
setCurrentConversationInputs: (v: Record<string, any>) => void, setCurrentConversationInputs: (v: Record<string, any>) => void,
allInputsHidden: boolean allInputsHidden: boolean
initUserVariables?: {
name?: string
avatar_url?: string
}
} }
export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({
@ -81,5 +85,6 @@ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>
currentConversationInputs: {}, currentConversationInputs: {},
setCurrentConversationInputs: noop, setCurrentConversationInputs: noop,
allInputsHidden: false, allInputsHidden: false,
initUserVariables: {},
}) })
export const useEmbeddedChatbotContext = () => useContext(EmbeddedChatbotContext) export const useEmbeddedChatbotContext = () => useContext(EmbeddedChatbotContext)

@ -15,7 +15,7 @@ import type {
Feedback, Feedback,
} from '../types' } from '../types'
import { CONVERSATION_ID_INFO } from '../constants' import { CONVERSATION_ID_INFO } from '../constants'
import { buildChatItemTree, getProcessedInputsFromUrlParams, getProcessedSystemVariablesFromUrlParams } from '../utils' import { buildChatItemTree, getProcessedInputsFromUrlParams, getProcessedSystemVariablesFromUrlParams, getProcessedUserVariablesFromUrlParams } from '../utils'
import { getProcessedFilesFromResponse } from '../../file-uploader/utils' import { getProcessedFilesFromResponse } from '../../file-uploader/utils'
import { import {
fetchAppInfo, fetchAppInfo,
@ -169,6 +169,7 @@ export const useEmbeddedChatbot = () => {
const newConversationInputsRef = useRef<Record<string, any>>({}) const newConversationInputsRef = useRef<Record<string, any>>({})
const [newConversationInputs, setNewConversationInputs] = useState<Record<string, any>>({}) const [newConversationInputs, setNewConversationInputs] = useState<Record<string, any>>({})
const [initInputs, setInitInputs] = useState<Record<string, any>>({}) const [initInputs, setInitInputs] = useState<Record<string, any>>({})
const [initUserVariables, setInitUserVariables] = useState<Record<string, any>>({})
const handleNewConversationInputsChange = useCallback((newInputs: Record<string, any>) => { const handleNewConversationInputsChange = useCallback((newInputs: Record<string, any>) => {
newConversationInputsRef.current = newInputs newConversationInputsRef.current = newInputs
setNewConversationInputs(newInputs) setNewConversationInputs(newInputs)
@ -237,7 +238,9 @@ export const useEmbeddedChatbot = () => {
// init inputs from url params // init inputs from url params
(async () => { (async () => {
const inputs = await getProcessedInputsFromUrlParams() const inputs = await getProcessedInputsFromUrlParams()
const userVariables = await getProcessedUserVariablesFromUrlParams()
setInitInputs(inputs) setInitInputs(inputs)
setInitUserVariables(userVariables)
})() })()
}, []) }, [])
useEffect(() => { useEffect(() => {
@ -418,5 +421,6 @@ export const useEmbeddedChatbot = () => {
currentConversationInputs, currentConversationInputs,
setCurrentConversationInputs, setCurrentConversationInputs,
allInputsHidden, allInputsHidden,
initUserVariables,
} }
} }

@ -195,6 +195,7 @@ const EmbeddedChatbotWrapper = () => {
currentConversationInputs, currentConversationInputs,
setCurrentConversationInputs, setCurrentConversationInputs,
allInputsHidden, allInputsHidden,
initUserVariables,
} = useEmbeddedChatbot() } = useEmbeddedChatbot()
return <EmbeddedChatbotContext.Provider value={{ return <EmbeddedChatbotContext.Provider value={{
@ -233,6 +234,7 @@ const EmbeddedChatbotWrapper = () => {
currentConversationInputs, currentConversationInputs,
setCurrentConversationInputs, setCurrentConversationInputs,
allInputsHidden, allInputsHidden,
initUserVariables,
}}> }}>
<Chatbot /> <Chatbot />
</EmbeddedChatbotContext.Provider> </EmbeddedChatbotContext.Provider>

@ -32,7 +32,8 @@ async function getProcessedInputsFromUrlParams(): Promise<Record<string, any>> {
const entriesArray = Array.from(urlParams.entries()) const entriesArray = Array.from(urlParams.entries())
await Promise.all( await Promise.all(
entriesArray.map(async ([key, value]) => { entriesArray.map(async ([key, value]) => {
if (!key.startsWith('sys.')) const prefixArray = ['sys.', 'user.']
if (!prefixArray.some(prefix => key.startsWith(prefix)))
inputs[key] = await decodeBase64AndDecompress(decodeURIComponent(value)) inputs[key] = await decodeBase64AndDecompress(decodeURIComponent(value))
}), }),
) )
@ -52,6 +53,19 @@ async function getProcessedSystemVariablesFromUrlParams(): Promise<Record<string
return systemVariables return systemVariables
} }
async function getProcessedUserVariablesFromUrlParams(): Promise<Record<string, any>> {
const urlParams = new URLSearchParams(window.location.search)
const userVariables: Record<string, any> = {}
const entriesArray = Array.from(urlParams.entries())
await Promise.all(
entriesArray.map(async ([key, value]) => {
if (key.startsWith('user.'))
userVariables[key.slice(5)] = await decodeBase64AndDecompress(decodeURIComponent(value))
}),
)
return userVariables
}
function isValidGeneratedAnswer(item?: ChatItem | ChatItemInTree): boolean { function isValidGeneratedAnswer(item?: ChatItem | ChatItemInTree): boolean {
return !!item && item.isAnswer && !item.id.startsWith('answer-placeholder-') && !item.isOpeningStatement return !!item && item.isAnswer && !item.id.startsWith('answer-placeholder-') && !item.isOpeningStatement
} }
@ -198,6 +212,7 @@ export {
getRawInputsFromUrlParams, getRawInputsFromUrlParams,
getProcessedInputsFromUrlParams, getProcessedInputsFromUrlParams,
getProcessedSystemVariablesFromUrlParams, getProcessedSystemVariablesFromUrlParams,
getProcessedUserVariablesFromUrlParams,
isValidGeneratedAnswer, isValidGeneratedAnswer,
getLastAnswer, getLastAnswer,
buildChatItemTree, buildChatItemTree,

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

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

@ -31,7 +31,7 @@ const PluginsNav = ({
)}> )}>
<div <div
className={classNames( className={classNames(
'relative flex flex-row h-8 p-1.5 gap-0.5 border border-transparent items-center justify-center rounded-xl system-sm-medium-uppercase', 'relative flex flex-row h-8 p-1.5 gap-0.5 border border-transparent items-center justify-center rounded-xl system-sm-medium',
activated && 'border-components-main-nav-nav-button-border bg-components-main-nav-nav-button-bg-active shadow-md text-components-main-nav-nav-button-text', activated && 'border-components-main-nav-nav-button-border bg-components-main-nav-nav-button-bg-active shadow-md text-components-main-nav-nav-button-text',
!activated && 'text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary', !activated && 'text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
(isInstallingWithError || isFailed) && !activated && 'border-components-panel-border-subtle', (isInstallingWithError || isFailed) && !activated && 'border-components-panel-border-subtle',

@ -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,6 +69,7 @@ const Card = ({
return ( return (
<div className={wrapClassName}> <div className={wrapClassName}>
<div className={cn('p-4 pb-3', limitedInstall && 'pb-1')}>
{!hideCornerMark && <CornerMark text={cornerMark} />} {!hideCornerMark && <CornerMark text={cornerMark} />}
{/* Header */} {/* Header */}
<div className="flex"> <div className="flex">
@ -91,6 +95,14 @@ const Card = ({
/> />
{footer && <div>{footer}</div>} {footer && <div>{footer}</div>}
</div> </div>
{limitedInstall
&& <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'>
<RiAlertFill className='h-3 w-3 shrink-0 text-text-warning-secondary' />
<p className='system-xs-regular z-10 grow text-text-secondary'>
{t('plugin.installModal.installWarning')}
</p>
</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,7 +141,14 @@ 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'>
<div className='px-2'>
{canInstall && <div className='flex items-center gap-x-2' onClick={handleClickSelectAll}>
<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 && ( {!canInstall && (
<Button variant='secondary' className='min-w-[72px]' onClick={onCancel}> <Button variant='secondary' className='min-w-[72px]' onClick={onCancel}>
{t('common.operation.cancel')} {t('common.operation.cancel')}
@ -116,6 +164,7 @@ const Install: FC<Props> = ({
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span> <span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span>
</Button> </Button>
</div> </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'

@ -96,7 +96,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 = {
@ -147,7 +151,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
@ -88,7 +88,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,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>

@ -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()

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

@ -88,6 +88,7 @@ const translation = {
appCreateDSLErrorPart3: 'Current application DSL version: ', appCreateDSLErrorPart3: 'Current application DSL version: ',
appCreateDSLErrorPart4: 'System-supported DSL version: ', appCreateDSLErrorPart4: 'System-supported DSL version: ',
appCreateFailed: 'Failed to create app', appCreateFailed: 'Failed to create app',
dropDSLToCreateApp: 'Drop DSL file here to create app',
}, },
newAppFromTemplate: { newAppFromTemplate: {
byCategories: 'BY CATEGORIES', byCategories: 'BY CATEGORIES',

@ -64,6 +64,8 @@ const translation = {
skip: 'Skip', skip: 'Skip',
format: 'Format', format: 'Format',
more: 'More', more: 'More',
selectAll: 'Select All',
deSelectAll: 'Deselect All',
}, },
errorMsg: { errorMsg: {
fieldRequired: '{{field}} is required', fieldRequired: '{{field}} is required',

@ -156,6 +156,7 @@ const translation = {
next: 'Next', next: 'Next',
pluginLoadError: 'Plugin load error', pluginLoadError: 'Plugin load error',
pluginLoadErrorDesc: 'This plugin will not be installed', pluginLoadErrorDesc: 'This plugin will not be installed',
installWarning: 'This plugin is not allowed to be installed.',
}, },
installFromGitHub: { installFromGitHub: {
installPlugin: 'Install plugin from GitHub', installPlugin: 'Install plugin from GitHub',

@ -64,6 +64,8 @@ const translation = {
in: '中', in: '中',
format: 'フォーマット', format: 'フォーマット',
more: 'もっと', more: 'もっと',
selectAll: 'すべて選択',
deSelectAll: 'すべて選択解除',
}, },
errorMsg: { errorMsg: {
fieldRequired: '{{field}}は必要です', fieldRequired: '{{field}}は必要です',

@ -137,6 +137,7 @@ const translation = {
installPlugin: 'プラグインをインストールする', installPlugin: 'プラグインをインストールする',
back: '戻る', back: '戻る',
uploadingPackage: '{{packageName}}をアップロード中...', uploadingPackage: '{{packageName}}をアップロード中...',
installWarning: 'このプラグインはインストールを許可されていません。',
}, },
installFromGitHub: { installFromGitHub: {
installedSuccessfully: 'インストールに成功しました', installedSuccessfully: 'インストールに成功しました',

@ -58,6 +58,7 @@ const translation = {
format: 'Format', format: 'Format',
downloadSuccess: 'Descărcarea a fost finalizată.', downloadSuccess: 'Descărcarea a fost finalizată.',
more: 'Mai mult', more: 'Mai mult',
deSelectAll: 'Deselectați tot',
}, },
placeholder: { placeholder: {
input: 'Vă rugăm să introduceți', input: 'Vă rugăm să introduceți',

@ -140,6 +140,7 @@ const translation = {
install: 'Namestite', install: 'Namestite',
pluginLoadError: 'Napaka pri nalaganju vtičnika', pluginLoadError: 'Napaka pri nalaganju vtičnika',
installPlugin: 'Namestite vtičnik', installPlugin: 'Namestite vtičnik',
installWarning: 'Ta vtičnik ni dovoljen za namestitev.',
}, },
installFromGitHub: { installFromGitHub: {
updatePlugin: 'Posodobite vtičnik iz GitHuba', updatePlugin: 'Posodobite vtičnik iz GitHuba',

@ -89,6 +89,7 @@ const translation = {
appCreateDSLErrorPart3: '当前应用 DSL 版本:', appCreateDSLErrorPart3: '当前应用 DSL 版本:',
appCreateDSLErrorPart4: '系统支持 DSL 版本:', appCreateDSLErrorPart4: '系统支持 DSL 版本:',
appCreateFailed: '应用创建失败', appCreateFailed: '应用创建失败',
dropDSLToCreateApp: '拖放 DSL 文件到此处创建应用',
}, },
newAppFromTemplate: { newAppFromTemplate: {
byCategories: '分类', byCategories: '分类',

@ -64,6 +64,8 @@ const translation = {
skip: '跳过', skip: '跳过',
format: '格式化', format: '格式化',
more: '更多', more: '更多',
selectAll: '全选',
deSelectAll: '取消全选',
}, },
errorMsg: { errorMsg: {
fieldRequired: '{{field}} 为必填项', fieldRequired: '{{field}} 为必填项',

@ -156,6 +156,7 @@ const translation = {
next: '下一步', next: '下一步',
pluginLoadError: '插件加载错误', pluginLoadError: '插件加载错误',
pluginLoadErrorDesc: '此插件将不会被安装', pluginLoadErrorDesc: '此插件将不会被安装',
installWarning: '此插件不允许安装。',
}, },
installFromGitHub: { installFromGitHub: {
installPlugin: '从 GitHub 安装插件', installPlugin: '从 GitHub 安装插件',

@ -112,9 +112,21 @@
return compressedSystemVariables; return compressedSystemVariables;
} }
async function getCompressedUserVariablesFromConfig() {
const userVariables = config?.userVariables || {};
const compressedUserVariables = {};
await Promise.all(
Object.entries(userVariables).map(async ([key, value]) => {
compressedUserVariables[`user.${key}`] = await compressAndEncodeBase64(value);
})
);
return compressedUserVariables;
}
const params = new URLSearchParams({ const params = new URLSearchParams({
...await getCompressedInputsFromConfig(), ...await getCompressedInputsFromConfig(),
...await getCompressedSystemVariablesFromConfig() ...await getCompressedSystemVariablesFromConfig(),
...await getCompressedUserVariablesFromConfig()
}); });
const baseUrl = const baseUrl =

@ -18,7 +18,7 @@
transition-property: width, height; transition-property: width, height;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms; transition-duration: 150ms;
`;async function e(){let u=!1;if(y&&y.token){var e=new URLSearchParams({...await(async()=>{var e=y?.inputs||{};let n={};return await Promise.all(Object.entries(e).map(async([e,t])=>{n[e]=await i(t)})),n})(),...await(async()=>{var e=y?.systemVariables||{};let n={};return await Promise.all(Object.entries(e).map(async([e,t])=>{n["sys."+e]=await i(t)})),n})()}),n=y.baseUrl||`https://${y.isDev?"dev.":""}udify.app`;let o=new URL(n).origin,t=`${n}/chatbot/${y.token}?`+e;n=s();async function i(e){e=(new TextEncoder).encode(e),e=new Response(new Blob([e]).stream().pipeThrough(new CompressionStream("gzip"))).arrayBuffer(),e=new Uint8Array(await e);return btoa(String.fromCharCode(...e))}function s(){var e=document.createElement("iframe");return e.allow="fullscreen;microphone",e.title="dify chatbot bubble window",e.id=m,e.src=t,e.style.cssText=l,e}function d(){var e,t,n;window.innerWidth<=640||(e=document.getElementById(m),t=document.getElementById(h),e&&t&&(t=t.getBoundingClientRect(),n=window.innerHeight/2,t.top+t.height/2<n?(e.style.top=`var(--${h}-bottom, 1rem)`,e.style.bottom="unset"):(e.style.bottom=`var(--${h}-bottom, 1rem)`,e.style.top="unset"),t.left+t.width/2<window.innerWidth/2?(e.style.left=`var(--${h}-right, 1rem)`,e.style.right="unset"):(e.style.right=`var(--${h}-right, 1rem)`,e.style.left="unset")))}function r(){let n=document.createElement("div");Object.entries(y.containerProps||{}).forEach(([e,t])=>{"className"===e?n.classList.add(...t.split(" ")):"style"===e?"object"==typeof t?Object.assign(n.style,t):n.style.cssText=t:"function"==typeof t?n.addEventListener(e.replace(/^on/,"").toLowerCase(),t):n[e]=t}),n.id=h;var e=document.createElement("style"),e=(document.head.appendChild(e),e.sheet.insertRule(` `;async function e(){let u=!1;if(y&&y.token){var e=new URLSearchParams({...await(async()=>{var e=y?.inputs||{};let n={};return await Promise.all(Object.entries(e).map(async([e,t])=>{n[e]=await i(t)})),n})(),...await(async()=>{var e=y?.systemVariables||{};let n={};return await Promise.all(Object.entries(e).map(async([e,t])=>{n["sys."+e]=await i(t)})),n})(),...await(async()=>{var e=y?.userVariables||{};let n={};return await Promise.all(Object.entries(e).map(async([e,t])=>{n["user."+e]=await i(t)})),n})()}),n=y.baseUrl||`https://${y.isDev?"dev.":""}udify.app`;let o=new URL(n).origin,t=`${n}/chatbot/${y.token}?`+e;n=s();async function i(e){e=(new TextEncoder).encode(e),e=new Response(new Blob([e]).stream().pipeThrough(new CompressionStream("gzip"))).arrayBuffer(),e=new Uint8Array(await e);return btoa(String.fromCharCode(...e))}function s(){var e=document.createElement("iframe");return e.allow="fullscreen;microphone",e.title="dify chatbot bubble window",e.id=m,e.src=t,e.style.cssText=l,e}function r(){var e,t,n;window.innerWidth<=640||(e=document.getElementById(m),t=document.getElementById(h),e&&t&&(t=t.getBoundingClientRect(),n=window.innerHeight/2,t.top+t.height/2<n?(e.style.top=`var(--${h}-bottom, 1rem)`,e.style.bottom="unset"):(e.style.bottom=`var(--${h}-bottom, 1rem)`,e.style.top="unset"),t.left+t.width/2<window.innerWidth/2?(e.style.left=`var(--${h}-right, 1rem)`,e.style.right="unset"):(e.style.right=`var(--${h}-right, 1rem)`,e.style.left="unset")))}function d(){let n=document.createElement("div");Object.entries(y.containerProps||{}).forEach(([e,t])=>{"className"===e?n.classList.add(...t.split(" ")):"style"===e?"object"==typeof t?Object.assign(n.style,t):n.style.cssText=t:"function"==typeof t?n.addEventListener(e.replace(/^on/,"").toLowerCase(),t):n[e]=t}),n.id=h;var e=document.createElement("style"),e=(document.head.appendChild(e),e.sheet.insertRule(`
#${n.id} { #${n.id} {
position: fixed; position: fixed;
bottom: var(--${n.id}-bottom, 1rem); bottom: var(--${n.id}-bottom, 1rem);
@ -33,10 +33,10 @@
cursor: pointer; cursor: pointer;
z-index: 2147483647; z-index: 2147483647;
} }
`),document.createElement("div"));function t(){var e;u||((e=document.getElementById(m))?(e.style.display="none"===e.style.display?"block":"none","none"===e.style.display?p("open"):p("close"),"none"===e.style.display?document.removeEventListener("keydown",b):document.addEventListener("keydown",b),d()):(n.appendChild(s()),d(),this.title="Exit (ESC)",p("close"),document.addEventListener("keydown",b)))}if(e.style.cssText="position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 2147483647;",e.innerHTML=`<svg id="openIcon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> `),document.createElement("div"));function t(){var e;u||((e=document.getElementById(m))?(e.style.display="none"===e.style.display?"block":"none","none"===e.style.display?p("open"):p("close"),"none"===e.style.display?document.removeEventListener("keydown",b):document.addEventListener("keydown",b),r()):(n.appendChild(s()),r(),this.title="Exit (ESC)",p("close"),document.addEventListener("keydown",b)))}if(e.style.cssText="position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 2147483647;",e.innerHTML=`<svg id="openIcon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.7586 2L16.2412 2C17.0462 1.99999 17.7105 1.99998 18.2517 2.04419C18.8138 2.09012 19.3305 2.18868 19.8159 2.43598C20.5685 2.81947 21.1804 3.43139 21.5639 4.18404C21.8112 4.66937 21.9098 5.18608 21.9557 5.74818C21.9999 6.28937 21.9999 6.95373 21.9999 7.7587L22 14.1376C22.0004 14.933 22.0007 15.5236 21.8636 16.0353C21.4937 17.4156 20.4155 18.4938 19.0352 18.8637C18.7277 18.9461 18.3917 18.9789 17.9999 18.9918L17.9999 20.371C18 20.6062 18 20.846 17.9822 21.0425C17.9651 21.2305 17.9199 21.5852 17.6722 21.8955C17.3872 22.2525 16.9551 22.4602 16.4983 22.4597C16.1013 22.4593 15.7961 22.273 15.6386 22.1689C15.474 22.06 15.2868 21.9102 15.1031 21.7632L12.69 19.8327C12.1714 19.4178 12.0174 19.3007 11.8575 19.219C11.697 19.137 11.5262 19.0771 11.3496 19.0408C11.1737 19.0047 10.9803 19 10.3162 19H7.75858C6.95362 19 6.28927 19 5.74808 18.9558C5.18598 18.9099 4.66928 18.8113 4.18394 18.564C3.43129 18.1805 2.81937 17.5686 2.43588 16.816C2.18859 16.3306 2.09002 15.8139 2.0441 15.2518C1.99988 14.7106 1.99989 14.0463 1.9999 13.2413V7.75868C1.99989 6.95372 1.99988 6.28936 2.0441 5.74818C2.09002 5.18608 2.18859 4.66937 2.43588 4.18404C2.81937 3.43139 3.43129 2.81947 4.18394 2.43598C4.66928 2.18868 5.18598 2.09012 5.74808 2.04419C6.28927 1.99998 6.95364 1.99999 7.7586 2ZM10.5073 7.5C10.5073 6.67157 9.83575 6 9.00732 6C8.1789 6 7.50732 6.67157 7.50732 7.5C7.50732 8.32843 8.1789 9 9.00732 9C9.83575 9 10.5073 8.32843 10.5073 7.5ZM16.6073 11.7001C16.1669 11.3697 15.5426 11.4577 15.2105 11.8959C15.1488 11.9746 15.081 12.0486 15.0119 12.1207C14.8646 12.2744 14.6432 12.4829 14.3566 12.6913C13.7796 13.111 12.9818 13.5001 12.0073 13.5001C11.0328 13.5001 10.235 13.111 9.65799 12.6913C9.37138 12.4829 9.15004 12.2744 9.00274 12.1207C8.93366 12.0486 8.86581 11.9745 8.80418 11.8959C8.472 11.4577 7.84775 11.3697 7.40732 11.7001C6.96549 12.0314 6.87595 12.6582 7.20732 13.1001C7.20479 13.0968 7.21072 13.1043 7.22094 13.1171C7.24532 13.1478 7.29407 13.2091 7.31068 13.2289C7.36932 13.2987 7.45232 13.3934 7.55877 13.5045C7.77084 13.7258 8.08075 14.0172 8.48165 14.3088C9.27958 14.8891 10.4818 15.5001 12.0073 15.5001C13.5328 15.5001 14.735 14.8891 15.533 14.3088C15.9339 14.0172 16.2438 13.7258 16.4559 13.5045C16.5623 13.3934 16.6453 13.2987 16.704 13.2289C16.7333 13.1939 16.7567 13.165 16.7739 13.1432C17.1193 12.6969 17.0729 12.0493 16.6073 11.7001ZM15.0073 6C15.8358 6 16.5073 6.67157 16.5073 7.5C16.5073 8.32843 15.8358 9 15.0073 9C14.1789 9 13.5073 8.32843 13.5073 7.5C13.5073 6.67157 14.1789 6 15.0073 6Z" fill="white"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M7.7586 2L16.2412 2C17.0462 1.99999 17.7105 1.99998 18.2517 2.04419C18.8138 2.09012 19.3305 2.18868 19.8159 2.43598C20.5685 2.81947 21.1804 3.43139 21.5639 4.18404C21.8112 4.66937 21.9098 5.18608 21.9557 5.74818C21.9999 6.28937 21.9999 6.95373 21.9999 7.7587L22 14.1376C22.0004 14.933 22.0007 15.5236 21.8636 16.0353C21.4937 17.4156 20.4155 18.4938 19.0352 18.8637C18.7277 18.9461 18.3917 18.9789 17.9999 18.9918L17.9999 20.371C18 20.6062 18 20.846 17.9822 21.0425C17.9651 21.2305 17.9199 21.5852 17.6722 21.8955C17.3872 22.2525 16.9551 22.4602 16.4983 22.4597C16.1013 22.4593 15.7961 22.273 15.6386 22.1689C15.474 22.06 15.2868 21.9102 15.1031 21.7632L12.69 19.8327C12.1714 19.4178 12.0174 19.3007 11.8575 19.219C11.697 19.137 11.5262 19.0771 11.3496 19.0408C11.1737 19.0047 10.9803 19 10.3162 19H7.75858C6.95362 19 6.28927 19 5.74808 18.9558C5.18598 18.9099 4.66928 18.8113 4.18394 18.564C3.43129 18.1805 2.81937 17.5686 2.43588 16.816C2.18859 16.3306 2.09002 15.8139 2.0441 15.2518C1.99988 14.7106 1.99989 14.0463 1.9999 13.2413V7.75868C1.99989 6.95372 1.99988 6.28936 2.0441 5.74818C2.09002 5.18608 2.18859 4.66937 2.43588 4.18404C2.81937 3.43139 3.43129 2.81947 4.18394 2.43598C4.66928 2.18868 5.18598 2.09012 5.74808 2.04419C6.28927 1.99998 6.95364 1.99999 7.7586 2ZM10.5073 7.5C10.5073 6.67157 9.83575 6 9.00732 6C8.1789 6 7.50732 6.67157 7.50732 7.5C7.50732 8.32843 8.1789 9 9.00732 9C9.83575 9 10.5073 8.32843 10.5073 7.5ZM16.6073 11.7001C16.1669 11.3697 15.5426 11.4577 15.2105 11.8959C15.1488 11.9746 15.081 12.0486 15.0119 12.1207C14.8646 12.2744 14.6432 12.4829 14.3566 12.6913C13.7796 13.111 12.9818 13.5001 12.0073 13.5001C11.0328 13.5001 10.235 13.111 9.65799 12.6913C9.37138 12.4829 9.15004 12.2744 9.00274 12.1207C8.93366 12.0486 8.86581 11.9745 8.80418 11.8959C8.472 11.4577 7.84775 11.3697 7.40732 11.7001C6.96549 12.0314 6.87595 12.6582 7.20732 13.1001C7.20479 13.0968 7.21072 13.1043 7.22094 13.1171C7.24532 13.1478 7.29407 13.2091 7.31068 13.2289C7.36932 13.2987 7.45232 13.3934 7.55877 13.5045C7.77084 13.7258 8.08075 14.0172 8.48165 14.3088C9.27958 14.8891 10.4818 15.5001 12.0073 15.5001C13.5328 15.5001 14.735 14.8891 15.533 14.3088C15.9339 14.0172 16.2438 13.7258 16.4559 13.5045C16.5623 13.3934 16.6453 13.2987 16.704 13.2289C16.7333 13.1939 16.7567 13.165 16.7739 13.1432C17.1193 12.6969 17.0729 12.0493 16.6073 11.7001ZM15.0073 6C15.8358 6 16.5073 6.67157 16.5073 7.5C16.5073 8.32843 15.8358 9 15.0073 9C14.1789 9 13.5073 8.32843 13.5073 7.5C13.5073 6.67157 14.1789 6 15.0073 6Z" fill="white"/>
</svg> </svg>
<svg id="closeIcon" style="display:none" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg id="closeIcon" style="display:none" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 18L6 6M6 18L18 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M18 18L6 6M6 18L18 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>
`,n.appendChild(e),document.body.appendChild(n),n.addEventListener("click",t),n.addEventListener("touchend",e=>{e.preventDefault(),t()},{passive:!1}),y.draggable){var a=n;var l=y.dragAxis||"both";let s,d,t,r;function o(e){u=!1,r=("touchstart"===e.type?(s=e.touches[0].clientX-a.offsetLeft,d=e.touches[0].clientY-a.offsetTop,t=e.touches[0].clientX,e.touches[0]):(s=e.clientX-a.offsetLeft,d=e.clientY-a.offsetTop,t=e.clientX,e)).clientY,document.addEventListener("mousemove",i),document.addEventListener("touchmove",i,{passive:!1}),document.addEventListener("mouseup",c),document.addEventListener("touchend",c),e.preventDefault()}function i(n){var o="touchmove"===n.type?n.touches[0]:n,i=o.clientX-t,o=o.clientY-r;if(u=8<Math.abs(i)||8<Math.abs(o)?!0:u){a.style.transition="none",a.style.cursor="grabbing";i=document.getElementById(m);i&&(i.style.display="none",p("open"));let e,t;t="touchmove"===n.type?(e=n.touches[0].clientX-s,window.innerHeight-n.touches[0].clientY-d):(e=n.clientX-s,window.innerHeight-n.clientY-d);o=a.getBoundingClientRect(),i=window.innerWidth-o.width,n=window.innerHeight-o.height;"x"!==l&&"both"!==l||a.style.setProperty(`--${h}-left`,Math.max(0,Math.min(e,i))+"px"),"y"!==l&&"both"!==l||a.style.setProperty(`--${h}-bottom`,Math.max(0,Math.min(t,n))+"px")}}function c(){setTimeout(()=>{u=!1},0),a.style.transition="",a.style.cursor="pointer",document.removeEventListener("mousemove",i),document.removeEventListener("touchmove",i),document.removeEventListener("mouseup",c),document.removeEventListener("touchend",c)}a.addEventListener("mousedown",o),a.addEventListener("touchstart",o)}}n.style.display="none",document.body.appendChild(n),2048<t.length&&console.error("The URL is too long, please reduce the number of inputs to prevent the bot from failing to load"),window.addEventListener("message",e=>{var t,n;e.origin===o&&(t=document.getElementById(m))&&e.source===t.contentWindow&&("dify-chatbot-iframe-ready"===e.data.type&&t.contentWindow?.postMessage({type:"dify-chatbot-config",payload:{isToggledByButton:!0,isDraggable:!!y.draggable}},o),"dify-chatbot-expand-change"===e.data.type)&&(a=!a,n=document.getElementById(m))&&(a?n.style.cssText="\n position: absolute;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n top: unset;\n right: var(--dify-chatbot-bubble-button-right, 1rem); /* Align with dify-chatbot-bubble-button. */\n bottom: var(--dify-chatbot-bubble-button-bottom, 1rem); /* Align with dify-chatbot-bubble-button. */\n left: unset;\n min-width: 24rem;\n width: 48%;\n max-width: 40rem; /* Match mobile breakpoint*/\n min-height: 43.75rem;\n height: 88%;\n max-height: calc(100vh - 6rem);\n border: none;\n z-index: 2147483640;\n overflow: hidden;\n user-select: none;\n transition-property: width, height;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 150ms;\n ":n.style.cssText=l,d())}),document.getElementById(h)||r()}else console.error(t+" is empty or token is not provided")}function p(e="open"){"open"===e?(document.getElementById("openIcon").style.display="block",document.getElementById("closeIcon").style.display="none"):(document.getElementById("openIcon").style.display="none",document.getElementById("closeIcon").style.display="block")}function b(e){"Escape"===e.key&&(e=document.getElementById(m))&&"none"!==e.style.display&&(e.style.display="none",p("open"))}h,h,document.addEventListener("keydown",b),y?.dynamicScript?e():document.body.onload=e})(); `,n.appendChild(e),document.body.appendChild(n),n.addEventListener("click",t),n.addEventListener("touchend",e=>{e.preventDefault(),t()},{passive:!1}),y.draggable){var a=n;var l=y.dragAxis||"both";let s,r,t,d;function o(e){u=!1,d=("touchstart"===e.type?(s=e.touches[0].clientX-a.offsetLeft,r=e.touches[0].clientY-a.offsetTop,t=e.touches[0].clientX,e.touches[0]):(s=e.clientX-a.offsetLeft,r=e.clientY-a.offsetTop,t=e.clientX,e)).clientY,document.addEventListener("mousemove",i),document.addEventListener("touchmove",i,{passive:!1}),document.addEventListener("mouseup",c),document.addEventListener("touchend",c),e.preventDefault()}function i(n){var o="touchmove"===n.type?n.touches[0]:n,i=o.clientX-t,o=o.clientY-d;if(u=8<Math.abs(i)||8<Math.abs(o)?!0:u){a.style.transition="none",a.style.cursor="grabbing";i=document.getElementById(m);i&&(i.style.display="none",p("open"));let e,t;t="touchmove"===n.type?(e=n.touches[0].clientX-s,window.innerHeight-n.touches[0].clientY-r):(e=n.clientX-s,window.innerHeight-n.clientY-r);o=a.getBoundingClientRect(),i=window.innerWidth-o.width,n=window.innerHeight-o.height;"x"!==l&&"both"!==l||a.style.setProperty(`--${h}-left`,Math.max(0,Math.min(e,i))+"px"),"y"!==l&&"both"!==l||a.style.setProperty(`--${h}-bottom`,Math.max(0,Math.min(t,n))+"px")}}function c(){setTimeout(()=>{u=!1},0),a.style.transition="",a.style.cursor="pointer",document.removeEventListener("mousemove",i),document.removeEventListener("touchmove",i),document.removeEventListener("mouseup",c),document.removeEventListener("touchend",c)}a.addEventListener("mousedown",o),a.addEventListener("touchstart",o)}}n.style.display="none",document.body.appendChild(n),2048<t.length&&console.error("The URL is too long, please reduce the number of inputs to prevent the bot from failing to load"),window.addEventListener("message",e=>{var t,n;e.origin===o&&(t=document.getElementById(m))&&e.source===t.contentWindow&&("dify-chatbot-iframe-ready"===e.data.type&&t.contentWindow?.postMessage({type:"dify-chatbot-config",payload:{isToggledByButton:!0,isDraggable:!!y.draggable}},o),"dify-chatbot-expand-change"===e.data.type)&&(a=!a,n=document.getElementById(m))&&(a?n.style.cssText="\n position: absolute;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n top: unset;\n right: var(--dify-chatbot-bubble-button-right, 1rem); /* Align with dify-chatbot-bubble-button. */\n bottom: var(--dify-chatbot-bubble-button-bottom, 1rem); /* Align with dify-chatbot-bubble-button. */\n left: unset;\n min-width: 24rem;\n width: 48%;\n max-width: 40rem; /* Match mobile breakpoint*/\n min-height: 43.75rem;\n height: 88%;\n max-height: calc(100vh - 6rem);\n border: none;\n z-index: 2147483640;\n overflow: hidden;\n user-select: none;\n transition-property: width, height;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 150ms;\n ":n.style.cssText=l,r())}),document.getElementById(h)||d()}else console.error(t+" is empty or token is not provided")}function p(e="open"){"open"===e?(document.getElementById("openIcon").style.display="block",document.getElementById("closeIcon").style.display="none"):(document.getElementById("openIcon").style.display="none",document.getElementById("closeIcon").style.display="block")}function b(e){"Escape"===e.key&&(e=document.getElementById(m))&&"none"!==e.style.display&&(e.style.display="none",p("open"))}h,h,document.addEventListener("keydown",b),y?.dynamicScript?e():document.body.onload=e})();

@ -13,12 +13,23 @@ export enum LicenseStatus {
LOST = 'lost', LOST = 'lost',
} }
export enum InstallationScope {
ALL = 'all',
NONE = 'none',
OFFICIAL_ONLY = 'official_only',
OFFICIAL_AND_PARTNER = 'official_and_specific_partners',
}
type License = { type License = {
status: LicenseStatus status: LicenseStatus
expired_at: string | null expired_at: string | null
} }
export type SystemFeatures = { export type SystemFeatures = {
plugin_installation_permission: {
plugin_installation_scope: InstallationScope,
restrict_to_marketplace_only: boolean
},
sso_enforced_for_signin: boolean sso_enforced_for_signin: boolean
sso_enforced_for_signin_protocol: SSOProtocol | '' sso_enforced_for_signin_protocol: SSOProtocol | ''
sso_enforced_for_web: boolean sso_enforced_for_web: boolean
@ -50,6 +61,10 @@ export type SystemFeatures = {
} }
export const defaultSystemFeatures: SystemFeatures = { export const defaultSystemFeatures: SystemFeatures = {
plugin_installation_permission: {
plugin_installation_scope: InstallationScope.ALL,
restrict_to_marketplace_only: false,
},
sso_enforced_for_signin: false, sso_enforced_for_signin: false,
sso_enforced_for_signin_protocol: '', sso_enforced_for_signin_protocol: '',
sso_enforced_for_web: false, sso_enforced_for_web: false,

@ -1,4 +1,4 @@
import { MAX_VAR_KEY_LENGTH, VAR_ITEM_TEMPLATE, VAR_ITEM_TEMPLATE_IN_WORKFLOW, getMaxVarNameLength } from '@/config' import { MARKETPLACE_URL_PREFIX, MAX_VAR_KEY_LENGTH, VAR_ITEM_TEMPLATE, VAR_ITEM_TEMPLATE_IN_WORKFLOW, getMaxVarNameLength } from '@/config'
import { import {
CONTEXT_PLACEHOLDER_TEXT, CONTEXT_PLACEHOLDER_TEXT,
HISTORY_PLACEHOLDER_TEXT, HISTORY_PLACEHOLDER_TEXT,
@ -112,3 +112,15 @@ export const getVars = (value: string) => {
// Set the value of basePath // Set the value of basePath
// example: /dify // example: /dify
export const basePath = '' export const basePath = ''
export function getMarketplaceUrl(path: string, params?: Record<string, string | undefined>) {
const searchParams = new URLSearchParams({ source: encodeURIComponent(window.location.origin) })
if (params) {
Object.keys(params).forEach((key) => {
const value = params[key]
if (value !== undefined && value !== null)
searchParams.append(key, value)
})
}
return `${MARKETPLACE_URL_PREFIX}${path}?${searchParams.toString()}`
}

Loading…
Cancel
Save