Merge branch 'fix/github-link-and-badge-size' into deploy/dev

This commit is contained in:
NFish 2025-03-11 21:06:15 +08:00
commit 639b3151de
62 changed files with 1245 additions and 597 deletions

13
.github/ISSUE_TEMPLATE/tracker.yml vendored Normal file
View File

@ -0,0 +1,13 @@
name: "👾 Tracker"
description: For inner usages, please donot use this template.
title: "[Tracker] "
labels:
- tracker
body:
- type: textarea
id: content
attributes:
label: Blockers
placeholder: "- [ ] ..."
validations:
required: true

View File

@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
CURRENT_VERSION: str = Field(
description="Dify version",
default="1.0.0",
default="1.0.1",
)
COMMIT_SHA: str = Field(

View File

@ -316,7 +316,7 @@ class AppTraceApi(Resource):
@account_initialization_required
def post(self, app_id):
# add app trace
if not current_user.is_admin_or_owner:
if not current_user.is_editing_role:
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("enabled", type=bool, required=True, location="json")

View File

@ -457,10 +457,8 @@ class PublishedWorkflowApi(Resource):
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("marked_name", type=str,
required=False, default="", location="json")
parser.add_argument("marked_comment", type=str,
required=False, default="", location="json")
parser.add_argument("marked_name", type=str, required=False, default="", location="json")
parser.add_argument("marked_comment", type=str, required=False, default="", location="json")
args = parser.parse_args()
# Validate name and comment length
@ -614,14 +612,10 @@ class PublishedAllWorkflowApi(Resource):
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("page", type=inputs.int_range(
1, 99999), required=False, default=1, location="args")
parser.add_argument("limit", type=inputs.int_range(
1, 100), required=False, default=20, location="args")
parser.add_argument("user_id", type=str,
required=False, location="args")
parser.add_argument("named_only", type=inputs.boolean,
required=False, default=False, location="args")
parser.add_argument("page", type=inputs.int_range(1, 99999), required=False, default=1, location="args")
parser.add_argument("limit", type=inputs.int_range(1, 100), required=False, default=20, location="args")
parser.add_argument("user_id", type=str, required=False, location="args")
parser.add_argument("named_only", type=inputs.boolean, required=False, default=False, location="args")
args = parser.parse_args()
page = int(args.get("page", 1))
limit = int(args.get("limit", 10))
@ -670,10 +664,8 @@ class WorkflowByIdApi(Resource):
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("marked_name", type=str,
required=False, location="json")
parser.add_argument("marked_comment", type=str,
required=False, location="json")
parser.add_argument("marked_name", type=str, required=False, location="json")
parser.add_argument("marked_comment", type=str, required=False, location="json")
args = parser.parse_args()
# Validate name and comment length
@ -784,8 +776,6 @@ api.add_resource(
AdvancedChatDraftRunLoopNodeApi,
"/apps/<uuid:app_id>/advanced-chat/workflows/draft/loop/nodes/<string:node_id>/run",
)
api.add_resource(DefaultBlockConfigsApi,
"/apps/<uuid:app_id>/workflows/default-workflow-block-configs")
api.add_resource(
WorkflowDraftRunLoopNodeApi,
"/apps/<uuid:app_id>/workflows/draft/loop/nodes/<string:node_id>/run",
@ -798,6 +788,10 @@ api.add_resource(
PublishedAllWorkflowApi,
"/apps/<uuid:app_id>/workflows",
)
api.add_resource(
DefaultBlockConfigsApi,
"/apps/<uuid:app_id>/workflows/default-workflow-block-configs",
)
api.add_resource(
DefaultBlockConfigApi,
"/apps/<uuid:app_id>/workflows/default-workflow-block-configs/<string:block_type>",

View File

@ -10,7 +10,12 @@ from controllers.console import api
from controllers.console.apikey import api_key_fields, api_key_list
from controllers.console.app.error import ProviderNotInitializeError
from controllers.console.datasets.error import DatasetInUseError, DatasetNameDuplicateError, IndexingEstimateError
from controllers.console.wraps import account_initialization_required, enterprise_license_required, setup_required
from controllers.console.wraps import (
account_initialization_required,
cloud_edition_billing_rate_limit_check,
enterprise_license_required,
setup_required,
)
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
from core.indexing_runner import IndexingRunner
from core.model_runtime.entities.model_entities import ModelType
@ -96,6 +101,7 @@ class DatasetListApi(Resource):
@setup_required
@login_required
@account_initialization_required
@cloud_edition_billing_rate_limit_check("knowledge")
def post(self):
parser = reqparse.RequestParser()
parser.add_argument(
@ -210,6 +216,7 @@ class DatasetApi(Resource):
@setup_required
@login_required
@account_initialization_required
@cloud_edition_billing_rate_limit_check("knowledge")
def patch(self, dataset_id):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str)
@ -276,7 +283,11 @@ class DatasetApi(Resource):
data = request.get_json()
# check embedding model setting
if data.get("indexing_technique") == "high_quality":
if (
data.get("indexing_technique") == "high_quality"
and data.get("embedding_model_provider") is not None
and data.get("embedding_model") is not None
):
DatasetService.check_embedding_model_setting(
dataset.tenant_id, data.get("embedding_model_provider"), data.get("embedding_model")
)
@ -313,6 +324,7 @@ class DatasetApi(Resource):
@setup_required
@login_required
@account_initialization_required
@cloud_edition_billing_rate_limit_check("knowledge")
def delete(self, dataset_id):
dataset_id_str = str(dataset_id)

View File

@ -26,6 +26,7 @@ from controllers.console.datasets.error import (
)
from controllers.console.wraps import (
account_initialization_required,
cloud_edition_billing_rate_limit_check,
cloud_edition_billing_resource_check,
setup_required,
)
@ -242,6 +243,7 @@ class DatasetDocumentListApi(Resource):
@account_initialization_required
@marshal_with(documents_and_batch_fields)
@cloud_edition_billing_resource_check("vector_space")
@cloud_edition_billing_rate_limit_check("knowledge")
def post(self, dataset_id):
dataset_id = str(dataset_id)
@ -297,6 +299,7 @@ class DatasetDocumentListApi(Resource):
@setup_required
@login_required
@account_initialization_required
@cloud_edition_billing_rate_limit_check("knowledge")
def delete(self, dataset_id):
dataset_id = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id)
@ -320,6 +323,7 @@ class DatasetInitApi(Resource):
@account_initialization_required
@marshal_with(dataset_and_document_fields)
@cloud_edition_billing_resource_check("vector_space")
@cloud_edition_billing_rate_limit_check("knowledge")
def post(self):
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
@ -694,6 +698,7 @@ class DocumentProcessingApi(DocumentResource):
@setup_required
@login_required
@account_initialization_required
@cloud_edition_billing_rate_limit_check("knowledge")
def patch(self, dataset_id, document_id, action):
dataset_id = str(dataset_id)
document_id = str(document_id)
@ -730,6 +735,7 @@ class DocumentDeleteApi(DocumentResource):
@setup_required
@login_required
@account_initialization_required
@cloud_edition_billing_rate_limit_check("knowledge")
def delete(self, dataset_id, document_id):
dataset_id = str(dataset_id)
document_id = str(document_id)
@ -798,6 +804,7 @@ class DocumentStatusApi(DocumentResource):
@login_required
@account_initialization_required
@cloud_edition_billing_resource_check("vector_space")
@cloud_edition_billing_rate_limit_check("knowledge")
def patch(self, dataset_id, action):
dataset_id = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id)
@ -893,6 +900,7 @@ class DocumentPauseApi(DocumentResource):
@setup_required
@login_required
@account_initialization_required
@cloud_edition_billing_rate_limit_check("knowledge")
def patch(self, dataset_id, document_id):
"""pause document."""
dataset_id = str(dataset_id)
@ -925,6 +933,7 @@ class DocumentRecoverApi(DocumentResource):
@setup_required
@login_required
@account_initialization_required
@cloud_edition_billing_rate_limit_check("knowledge")
def patch(self, dataset_id, document_id):
"""recover document."""
dataset_id = str(dataset_id)
@ -954,6 +963,7 @@ class DocumentRetryApi(DocumentResource):
@setup_required
@login_required
@account_initialization_required
@cloud_edition_billing_rate_limit_check("knowledge")
def post(self, dataset_id):
"""retry document."""

View File

@ -19,6 +19,7 @@ from controllers.console.datasets.error import (
from controllers.console.wraps import (
account_initialization_required,
cloud_edition_billing_knowledge_limit_check,
cloud_edition_billing_rate_limit_check,
cloud_edition_billing_resource_check,
setup_required,
)
@ -106,6 +107,7 @@ class DatasetDocumentSegmentListApi(Resource):
@setup_required
@login_required
@account_initialization_required
@cloud_edition_billing_rate_limit_check("knowledge")
def delete(self, dataset_id, document_id):
# check dataset
dataset_id = str(dataset_id)
@ -137,6 +139,7 @@ class DatasetDocumentSegmentApi(Resource):
@login_required
@account_initialization_required
@cloud_edition_billing_resource_check("vector_space")
@cloud_edition_billing_rate_limit_check("knowledge")
def patch(self, dataset_id, document_id, action):
dataset_id = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id)
@ -191,6 +194,7 @@ class DatasetDocumentSegmentAddApi(Resource):
@account_initialization_required
@cloud_edition_billing_resource_check("vector_space")
@cloud_edition_billing_knowledge_limit_check("add_segment")
@cloud_edition_billing_rate_limit_check("knowledge")
def post(self, dataset_id, document_id):
# check dataset
dataset_id = str(dataset_id)
@ -240,6 +244,7 @@ class DatasetDocumentSegmentUpdateApi(Resource):
@login_required
@account_initialization_required
@cloud_edition_billing_resource_check("vector_space")
@cloud_edition_billing_rate_limit_check("knowledge")
def patch(self, dataset_id, document_id, segment_id):
# check dataset
dataset_id = str(dataset_id)
@ -299,6 +304,7 @@ class DatasetDocumentSegmentUpdateApi(Resource):
@setup_required
@login_required
@account_initialization_required
@cloud_edition_billing_rate_limit_check("knowledge")
def delete(self, dataset_id, document_id, segment_id):
# check dataset
dataset_id = str(dataset_id)
@ -336,6 +342,7 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
@account_initialization_required
@cloud_edition_billing_resource_check("vector_space")
@cloud_edition_billing_knowledge_limit_check("add_segment")
@cloud_edition_billing_rate_limit_check("knowledge")
def post(self, dataset_id, document_id):
# check dataset
dataset_id = str(dataset_id)
@ -402,6 +409,7 @@ class ChildChunkAddApi(Resource):
@account_initialization_required
@cloud_edition_billing_resource_check("vector_space")
@cloud_edition_billing_knowledge_limit_check("add_segment")
@cloud_edition_billing_rate_limit_check("knowledge")
def post(self, dataset_id, document_id, segment_id):
# check dataset
dataset_id = str(dataset_id)
@ -499,6 +507,7 @@ class ChildChunkAddApi(Resource):
@login_required
@account_initialization_required
@cloud_edition_billing_resource_check("vector_space")
@cloud_edition_billing_rate_limit_check("knowledge")
def patch(self, dataset_id, document_id, segment_id):
# check dataset
dataset_id = str(dataset_id)
@ -542,6 +551,7 @@ class ChildChunkUpdateApi(Resource):
@setup_required
@login_required
@account_initialization_required
@cloud_edition_billing_rate_limit_check("knowledge")
def delete(self, dataset_id, document_id, segment_id, child_chunk_id):
# check dataset
dataset_id = str(dataset_id)
@ -586,6 +596,7 @@ class ChildChunkUpdateApi(Resource):
@login_required
@account_initialization_required
@cloud_edition_billing_resource_check("vector_space")
@cloud_edition_billing_rate_limit_check("knowledge")
def patch(self, dataset_id, document_id, segment_id, child_chunk_id):
# check dataset
dataset_id = str(dataset_id)

View File

@ -2,7 +2,11 @@ from flask_restful import Resource # type: ignore
from controllers.console import api
from controllers.console.datasets.hit_testing_base import DatasetsHitTestingBase
from controllers.console.wraps import account_initialization_required, setup_required
from controllers.console.wraps import (
account_initialization_required,
cloud_edition_billing_rate_limit_check,
setup_required,
)
from libs.login import login_required
@ -10,6 +14,7 @@ class HitTestingApi(Resource, DatasetsHitTestingBase):
@setup_required
@login_required
@account_initialization_required
@cloud_edition_billing_rate_limit_check("knowledge")
def post(self, dataset_id):
dataset_id_str = str(dataset_id)

View File

@ -1,5 +1,6 @@
import json
import os
import time
from functools import wraps
from flask import abort, request
@ -8,6 +9,8 @@ from flask_login import current_user # type: ignore
from configs import dify_config
from controllers.console.workspace.error import AccountNotInitializedError
from extensions.ext_database import db
from extensions.ext_redis import redis_client
from models.dataset import RateLimitLog
from models.model import DifySetup
from services.feature_service import FeatureService, LicenseStatus
from services.operation_service import OperationService
@ -78,7 +81,9 @@ def cloud_edition_billing_resource_check(resource: str):
elif resource == "apps" and 0 < apps.limit <= apps.size:
abort(403, "The number of apps has reached the limit of your subscription.")
elif resource == "vector_space" and 0 < vector_space.limit <= vector_space.size:
abort(403, "The capacity of the vector space has reached the limit of your subscription.")
abort(
403, "The capacity of the knowledge storage space has reached the limit of your subscription."
)
elif resource == "documents" and 0 < documents_upload_quota.limit <= documents_upload_quota.size:
# The api of file upload is used in the multiple places,
# so we need to check the source of the request from datasets
@ -123,6 +128,41 @@ def cloud_edition_billing_knowledge_limit_check(resource: str):
return interceptor
def cloud_edition_billing_rate_limit_check(resource: str):
def interceptor(view):
@wraps(view)
def decorated(*args, **kwargs):
if resource == "knowledge":
knowledge_rate_limit = FeatureService.get_knowledge_rate_limit(current_user.current_tenant_id)
if knowledge_rate_limit.enabled:
current_time = int(time.time() * 1000)
key = f"rate_limit_{current_user.current_tenant_id}"
redis_client.zadd(key, {current_time: current_time})
redis_client.zremrangebyscore(key, 0, current_time - 60000)
request_count = redis_client.zcard(key)
if request_count > knowledge_rate_limit.limit:
# add ratelimit record
rate_limit_log = RateLimitLog(
tenant_id=current_user.current_tenant_id,
subscription_plan=knowledge_rate_limit.subscription_plan,
operation="knowledge",
)
db.session.add(rate_limit_log)
db.session.commit()
abort(
403, "Sorry, you have reached the knowledge base request rate limit of your subscription."
)
return view(*args, **kwargs)
return decorated
return interceptor
def cloud_utm_record(view):
@wraps(view)
def decorated(*args, **kwargs):

View File

@ -1,3 +1,4 @@
import time
from collections.abc import Callable
from datetime import UTC, datetime, timedelta
from enum import Enum
@ -13,8 +14,10 @@ from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden, Unauthorized
from extensions.ext_database import db
from extensions.ext_redis import redis_client
from libs.login import _get_user
from models.account import Account, Tenant, TenantAccountJoin, TenantStatus
from models.dataset import RateLimitLog
from models.model import ApiToken, App, EndUser
from services.feature_service import FeatureService
@ -139,6 +142,43 @@ def cloud_edition_billing_knowledge_limit_check(resource: str, api_token_type: s
return interceptor
def cloud_edition_billing_rate_limit_check(resource: str, api_token_type: str):
def interceptor(view):
@wraps(view)
def decorated(*args, **kwargs):
api_token = validate_and_get_api_token(api_token_type)
if resource == "knowledge":
knowledge_rate_limit = FeatureService.get_knowledge_rate_limit(api_token.tenant_id)
if knowledge_rate_limit.enabled:
current_time = int(time.time() * 1000)
key = f"rate_limit_{api_token.tenant_id}"
redis_client.zadd(key, {current_time: current_time})
redis_client.zremrangebyscore(key, 0, current_time - 60000)
request_count = redis_client.zcard(key)
if request_count > knowledge_rate_limit.limit:
# add ratelimit record
rate_limit_log = RateLimitLog(
tenant_id=api_token.tenant_id,
subscription_plan=knowledge_rate_limit.subscription_plan,
operation="knowledge",
)
db.session.add(rate_limit_log)
db.session.commit()
raise Forbidden(
"Sorry, you have reached the knowledge base request rate limit of your subscription."
)
return view(*args, **kwargs)
return decorated
return interceptor
def validate_dataset_token(view=None):
def decorator(view):
@wraps(view)

View File

@ -7,7 +7,6 @@ from json import JSONDecodeError
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import or_
from constants import HIDDEN_VALUE
from core.entities.model_entities import ModelStatus, ModelWithProviderEntity, SimpleModelProviderEntity
@ -180,37 +179,35 @@ class ProviderConfiguration(BaseModel):
else [],
)
def _get_custom_provider_credentials(self) -> Provider | None:
"""
Get custom provider credentials.
"""
# get provider
model_provider_id = ModelProviderID(self.provider.provider)
provider_names = [self.provider.provider]
if model_provider_id.is_langgenius():
provider_names.append(model_provider_id.provider_name)
provider_record = (
db.session.query(Provider)
.filter(
Provider.tenant_id == self.tenant_id,
Provider.provider_type == ProviderType.CUSTOM.value,
Provider.provider_name.in_(provider_names),
)
.first()
)
return provider_record
def custom_credentials_validate(self, credentials: dict) -> tuple[Provider | None, dict]:
"""
Validate custom credentials.
:param credentials: provider credentials
:return:
"""
# get provider
model_provider_id = ModelProviderID(self.provider.provider)
if model_provider_id.is_langgenius():
provider_record = (
db.session.query(Provider)
.filter(
Provider.tenant_id == self.tenant_id,
Provider.provider_type == ProviderType.CUSTOM.value,
or_(
Provider.provider_name == model_provider_id.provider_name,
Provider.provider_name == self.provider.provider,
),
)
.first()
)
else:
provider_record = (
db.session.query(Provider)
.filter(
Provider.tenant_id == self.tenant_id,
Provider.provider_type == ProviderType.CUSTOM.value,
Provider.provider_name == self.provider.provider,
)
.first()
)
provider_record = self._get_custom_provider_credentials()
# Get provider credential secret variables
provider_credential_secret_variables = self.extract_secret_variables(
@ -291,18 +288,7 @@ class ProviderConfiguration(BaseModel):
:return:
"""
# get provider
provider_record = (
db.session.query(Provider)
.filter(
Provider.tenant_id == self.tenant_id,
or_(
Provider.provider_name == ModelProviderID(self.provider.provider).plugin_name,
Provider.provider_name == self.provider.provider,
),
Provider.provider_type == ProviderType.CUSTOM.value,
)
.first()
)
provider_record = self._get_custom_provider_credentials()
# delete provider
if provider_record:
@ -349,6 +335,33 @@ class ProviderConfiguration(BaseModel):
return None
def _get_custom_model_credentials(
self,
model_type: ModelType,
model: str,
) -> ProviderModel | None:
"""
Get custom model credentials.
"""
# get provider model
model_provider_id = ModelProviderID(self.provider.provider)
provider_names = [self.provider.provider]
if model_provider_id.is_langgenius():
provider_names.append(model_provider_id.provider_name)
provider_model_record = (
db.session.query(ProviderModel)
.filter(
ProviderModel.tenant_id == self.tenant_id,
ProviderModel.provider_name.in_(provider_names),
ProviderModel.model_name == model,
ProviderModel.model_type == model_type.to_origin_model_type(),
)
.first()
)
return provider_model_record
def custom_model_credentials_validate(
self, model_type: ModelType, model: str, credentials: dict
) -> tuple[ProviderModel | None, dict]:
@ -361,16 +374,7 @@ class ProviderConfiguration(BaseModel):
:return:
"""
# get provider model
provider_model_record = (
db.session.query(ProviderModel)
.filter(
ProviderModel.tenant_id == self.tenant_id,
ProviderModel.provider_name == self.provider.provider,
ProviderModel.model_name == model,
ProviderModel.model_type == model_type.to_origin_model_type(),
)
.first()
)
provider_model_record = self._get_custom_model_credentials(model_type, model)
# Get provider credential secret variables
provider_credential_secret_variables = self.extract_secret_variables(
@ -451,16 +455,7 @@ class ProviderConfiguration(BaseModel):
:return:
"""
# get provider model
provider_model_record = (
db.session.query(ProviderModel)
.filter(
ProviderModel.tenant_id == self.tenant_id,
ProviderModel.provider_name == self.provider.provider,
ProviderModel.model_name == model,
ProviderModel.model_type == model_type.to_origin_model_type(),
)
.first()
)
provider_model_record = self._get_custom_model_credentials(model_type, model)
# delete provider model
if provider_model_record:
@ -475,6 +470,26 @@ class ProviderConfiguration(BaseModel):
provider_model_credentials_cache.delete()
def _get_provider_model_setting(self, model_type: ModelType, model: str) -> ProviderModelSetting | None:
"""
Get provider model setting.
"""
model_provider_id = ModelProviderID(self.provider.provider)
provider_names = [self.provider.provider]
if model_provider_id.is_langgenius():
provider_names.append(model_provider_id.provider_name)
return (
db.session.query(ProviderModelSetting)
.filter(
ProviderModelSetting.tenant_id == self.tenant_id,
ProviderModelSetting.provider_name.in_(provider_names),
ProviderModelSetting.model_type == model_type.to_origin_model_type(),
ProviderModelSetting.model_name == model,
)
.first()
)
def enable_model(self, model_type: ModelType, model: str) -> ProviderModelSetting:
"""
Enable model.
@ -482,16 +497,7 @@ class ProviderConfiguration(BaseModel):
:param model: model name
:return:
"""
model_setting = (
db.session.query(ProviderModelSetting)
.filter(
ProviderModelSetting.tenant_id == self.tenant_id,
ProviderModelSetting.provider_name == self.provider.provider,
ProviderModelSetting.model_type == model_type.to_origin_model_type(),
ProviderModelSetting.model_name == model,
)
.first()
)
model_setting = self._get_provider_model_setting(model_type, model)
if model_setting:
model_setting.enabled = True
@ -516,16 +522,7 @@ class ProviderConfiguration(BaseModel):
:param model: model name
:return:
"""
model_setting = (
db.session.query(ProviderModelSetting)
.filter(
ProviderModelSetting.tenant_id == self.tenant_id,
ProviderModelSetting.provider_name == self.provider.provider,
ProviderModelSetting.model_type == model_type.to_origin_model_type(),
ProviderModelSetting.model_name == model,
)
.first()
)
model_setting = self._get_provider_model_setting(model_type, model)
if model_setting:
model_setting.enabled = False
@ -550,13 +547,24 @@ class ProviderConfiguration(BaseModel):
:param model: model name
:return:
"""
return self._get_provider_model_setting(model_type, model)
def _get_load_balancing_config(self, model_type: ModelType, model: str) -> Optional[LoadBalancingModelConfig]:
"""
Get load balancing config.
"""
model_provider_id = ModelProviderID(self.provider.provider)
provider_names = [self.provider.provider]
if model_provider_id.is_langgenius():
provider_names.append(model_provider_id.provider_name)
return (
db.session.query(ProviderModelSetting)
db.session.query(LoadBalancingModelConfig)
.filter(
ProviderModelSetting.tenant_id == self.tenant_id,
ProviderModelSetting.provider_name == self.provider.provider,
ProviderModelSetting.model_type == model_type.to_origin_model_type(),
ProviderModelSetting.model_name == model,
LoadBalancingModelConfig.tenant_id == self.tenant_id,
LoadBalancingModelConfig.provider_name.in_(provider_names),
LoadBalancingModelConfig.model_type == model_type.to_origin_model_type(),
LoadBalancingModelConfig.model_name == model,
)
.first()
)
@ -568,11 +576,16 @@ class ProviderConfiguration(BaseModel):
:param model: model name
:return:
"""
model_provider_id = ModelProviderID(self.provider.provider)
provider_names = [self.provider.provider]
if model_provider_id.is_langgenius():
provider_names.append(model_provider_id.provider_name)
load_balancing_config_count = (
db.session.query(LoadBalancingModelConfig)
.filter(
LoadBalancingModelConfig.tenant_id == self.tenant_id,
LoadBalancingModelConfig.provider_name == self.provider.provider,
LoadBalancingModelConfig.provider_name.in_(provider_names),
LoadBalancingModelConfig.model_type == model_type.to_origin_model_type(),
LoadBalancingModelConfig.model_name == model,
)
@ -582,16 +595,7 @@ class ProviderConfiguration(BaseModel):
if load_balancing_config_count <= 1:
raise ValueError("Model load balancing configuration must be more than 1.")
model_setting = (
db.session.query(ProviderModelSetting)
.filter(
ProviderModelSetting.tenant_id == self.tenant_id,
ProviderModelSetting.provider_name == self.provider.provider,
ProviderModelSetting.model_type == model_type.to_origin_model_type(),
ProviderModelSetting.model_name == model,
)
.first()
)
model_setting = self._get_provider_model_setting(model_type, model)
if model_setting:
model_setting.load_balancing_enabled = True
@ -616,11 +620,16 @@ class ProviderConfiguration(BaseModel):
:param model: model name
:return:
"""
model_provider_id = ModelProviderID(self.provider.provider)
provider_names = [self.provider.provider]
if model_provider_id.is_langgenius():
provider_names.append(model_provider_id.provider_name)
model_setting = (
db.session.query(ProviderModelSetting)
.filter(
ProviderModelSetting.tenant_id == self.tenant_id,
ProviderModelSetting.provider_name == self.provider.provider,
ProviderModelSetting.provider_name.in_(provider_names),
ProviderModelSetting.model_type == model_type.to_origin_model_type(),
ProviderModelSetting.model_name == model,
)
@ -677,11 +686,16 @@ class ProviderConfiguration(BaseModel):
return
# get preferred provider
model_provider_id = ModelProviderID(self.provider.provider)
provider_names = [self.provider.provider]
if model_provider_id.is_langgenius():
provider_names.append(model_provider_id.provider_name)
preferred_model_provider = (
db.session.query(TenantPreferredModelProvider)
.filter(
TenantPreferredModelProvider.tenant_id == self.tenant_id,
TenantPreferredModelProvider.provider_name == self.provider.provider,
TenantPreferredModelProvider.provider_name.in_(provider_names),
)
.first()
)

View File

@ -88,7 +88,10 @@ class FixedRecursiveCharacterTextSplitter(EnhanceRecursiveCharacterTextSplitter)
break
# Now that we have the separator, split the text
if separator:
splits = text.split(separator)
if separator == " ":
splits = text.split()
else:
splits = text.split(separator)
else:
splits = list(text)
# Now go merging things, recursively splitting longer texts.

View File

@ -179,6 +179,18 @@ class ApiTool(Tool):
for content_type in self.api_bundle.openapi["requestBody"]["content"]:
headers["Content-Type"] = content_type
body_schema = self.api_bundle.openapi["requestBody"]["content"][content_type]["schema"]
# handle ref schema
if "$ref" in body_schema:
ref_path = body_schema["$ref"].split("/")
ref_name = ref_path[-1]
if (
"components" in self.api_bundle.openapi
and "schemas" in self.api_bundle.openapi["components"]
):
if ref_name in self.api_bundle.openapi["components"]["schemas"]:
body_schema = self.api_bundle.openapi["components"]["schemas"][ref_name]
required = body_schema.get("required", [])
properties = body_schema.get("properties", {})
for name, property in properties.items():
@ -186,6 +198,8 @@ class ApiTool(Tool):
if property.get("format") == "binary":
f = parameters[name]
files.append((name, (f.filename, download(f), f.mime_type)))
elif "$ref" in property:
body[name] = parameters[name]
else:
# convert type
body[name] = self._convert_body_property_type(property, parameters[name])

View File

@ -765,17 +765,22 @@ class ToolManager:
@classmethod
def generate_builtin_tool_icon_url(cls, provider_id: str) -> str:
return (
dify_config.CONSOLE_API_URL
+ "/console/api/workspaces/current/tool-provider/builtin/"
+ provider_id
+ "/icon"
return str(
URL(dify_config.CONSOLE_API_URL or "/")
/ "console"
/ "api"
/ "workspaces"
/ "current"
/ "tool-provider"
/ "builtin"
/ provider_id
/ "icon"
)
@classmethod
def generate_plugin_tool_icon_url(cls, tenant_id: str, filename: str) -> str:
return str(
URL(dify_config.CONSOLE_API_URL)
URL(dify_config.CONSOLE_API_URL or "/")
/ "console"
/ "api"
/ "workspaces"

View File

@ -1,6 +1,6 @@
import json
import logging
from collections import defaultdict
import time
from collections.abc import Mapping, Sequence
from typing import Any, Optional, cast
@ -35,9 +35,10 @@ from core.workflow.nodes.llm.entities import LLMNodeChatModelMessage, LLMNodeCom
from core.workflow.nodes.llm.node import LLMNode
from core.workflow.nodes.question_classifier.template_prompts import QUESTION_CLASSIFIER_USER_PROMPT_2
from extensions.ext_database import db
from libs.json_in_md_parser import parse_and_check_json_markdown
from models.dataset import Dataset, DatasetMetadata, Document
from extensions.ext_redis import redis_client
from models.dataset import Dataset, Document, RateLimitLog
from models.workflow import WorkflowNodeExecutionStatus
from services.feature_service import FeatureService
from .entities import KnowledgeRetrievalNodeData, ModelConfig
from .exc import (
@ -80,6 +81,31 @@ class KnowledgeRetrievalNode(LLMNode):
return NodeRunResult(
status=WorkflowNodeExecutionStatus.FAILED, inputs=variables, error="Query is required."
)
# check rate limit
if self.tenant_id:
knowledge_rate_limit = FeatureService.get_knowledge_rate_limit(self.tenant_id)
if knowledge_rate_limit.enabled:
current_time = int(time.time() * 1000)
key = f"rate_limit_{self.tenant_id}"
redis_client.zadd(key, {current_time: current_time})
redis_client.zremrangebyscore(key, 0, current_time - 60000)
request_count = redis_client.zcard(key)
if request_count > knowledge_rate_limit.limit:
# add ratelimit record
rate_limit_log = RateLimitLog(
tenant_id=self.tenant_id,
subscription_plan=knowledge_rate_limit.subscription_plan,
operation="knowledge",
)
db.session.add(rate_limit_log)
db.session.commit()
return NodeRunResult(
status=WorkflowNodeExecutionStatus.FAILED,
inputs=variables,
error="Sorry, you have reached the knowledge base request rate limit of your subscription.",
error_type="RateLimitExceeded",
)
# retrieve knowledge
try:
results = self._fetch_dataset_retriever(node_data=node_data, query=query)

View File

@ -94,6 +94,9 @@ class LLMNode(BaseNode[LLMNodeData]):
def _run(self) -> Generator[NodeEvent | InNodeEvent, None, None]:
node_inputs: Optional[dict[str, Any]] = None
process_data = None
result_text = ""
usage = LLMUsage.empty_usage()
finish_reason = None
try:
# init messages template
@ -178,9 +181,6 @@ class LLMNode(BaseNode[LLMNodeData]):
stop=stop,
)
result_text = ""
usage = LLMUsage.empty_usage()
finish_reason = None
for event in generator:
if isinstance(event, RunStreamChunkEvent):
yield event

View File

@ -270,7 +270,9 @@ class ToolNode(BaseNode[ToolNodeData]):
if self.node_type == NodeType.AGENT:
msg_metadata = message.message.json_object.pop("execution_metadata", {})
agent_execution_metadata = {
key: value for key, value in msg_metadata.items() if key in NodeRunMetadataKey
key: value
for key, value in msg_metadata.items()
if key in NodeRunMetadataKey.__members__.values()
}
json.append(message.message.json_object)
elif message.type == ToolInvokeMessage.MessageType.LINK:

View File

@ -32,11 +32,7 @@ class AwsS3Storage(BaseStorage):
aws_access_key_id=dify_config.S3_ACCESS_KEY,
endpoint_url=dify_config.S3_ENDPOINT,
region_name=dify_config.S3_REGION,
config=Config(
s3={"addressing_style": dify_config.S3_ADDRESS_STYLE},
request_checksum_calculation="when_required",
response_checksum_validation="when_required",
),
config=Config(s3={"addressing_style": dify_config.S3_ADDRESS_STYLE}),
)
# create bucket
try:

View File

@ -0,0 +1,43 @@
"""add_rate_limit_logs
Revision ID: f051706725cc
Revises: 923752d42eb6
Create Date: 2025-01-14 06:17:35.536388
"""
from alembic import op
import models as models
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'f051706725cc'
down_revision = 'ee79d9b1c156'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('rate_limit_logs',
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('tenant_id', models.types.StringUUID(), nullable=False),
sa.Column('subscription_plan', sa.String(length=255), nullable=False),
sa.Column('operation', sa.String(length=255), nullable=False),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False),
sa.PrimaryKeyConstraint('id', name='rate_limit_log_pkey')
)
with op.batch_alter_table('rate_limit_logs', schema=None) as batch_op:
batch_op.create_index('rate_limit_log_operation_idx', ['operation'], unique=False)
batch_op.create_index('rate_limit_log_tenant_idx', ['tenant_id'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('rate_limit_logs', schema=None) as batch_op:
batch_op.drop_index('rate_limit_log_tenant_idx')
batch_op.drop_index('rate_limit_log_operation_idx')
op.drop_table('rate_limit_logs')
# ### end Alembic commands ###

View File

@ -12,7 +12,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ee79d9b1c156'
down_revision = 'd20049ed0af6'
down_revision = '5511c782ee4c'
branch_labels = None
depends_on = None

View File

@ -1068,39 +1068,16 @@ class DatasetAutoDisableLog(db.Model): # type: ignore[name-defined]
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
class DatasetMetadata(db.Model): # type: ignore[name-defined]
__tablename__ = "dataset_metadatas"
class RateLimitLog(db.Model): # type: ignore[name-defined]
__tablename__ = "rate_limit_logs"
__table_args__ = (
db.PrimaryKeyConstraint("id", name="dataset_metadata_pkey"),
db.Index("dataset_metadata_tenant_idx", "tenant_id"),
db.Index("dataset_metadata_dataset_idx", "dataset_id"),
db.PrimaryKeyConstraint("id", name="rate_limit_log_pkey"),
db.Index("rate_limit_log_tenant_idx", "tenant_id"),
db.Index("rate_limit_log_operation_idx", "operation"),
)
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
tenant_id = db.Column(StringUUID, nullable=False)
dataset_id = db.Column(StringUUID, nullable=False)
type = db.Column(db.String(255), nullable=False)
name = db.Column(db.String(255), nullable=False)
subscription_plan = db.Column(db.String(255), nullable=False)
operation = db.Column(db.String(255), nullable=False)
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
created_by = db.Column(StringUUID, nullable=False)
updated_by = db.Column(StringUUID, nullable=True)
class DatasetMetadataBinding(db.Model): # type: ignore[name-defined]
__tablename__ = "dataset_metadata_bindings"
__table_args__ = (
db.PrimaryKeyConstraint("id", name="dataset_metadata_binding_pkey"),
db.Index("dataset_metadata_binding_tenant_idx", "tenant_id"),
db.Index("dataset_metadata_binding_dataset_idx", "dataset_id"),
db.Index("dataset_metadata_binding_metadata_idx", "metadata_id"),
db.Index("dataset_metadata_binding_document_idx", "document_id"),
)
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
tenant_id = db.Column(StringUUID, nullable=False)
dataset_id = db.Column(StringUUID, nullable=False)
metadata_id = db.Column(StringUUID, nullable=False)
document_id = db.Column(StringUUID, nullable=False)
created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())
created_by = db.Column(StringUUID, nullable=False)

View File

@ -257,7 +257,7 @@ class App(Base):
provider_id = tool.get("provider_id", "")
if provider_type == ToolProviderType.API.value:
if provider_id not in existing_api_providers:
if uuid.UUID(provider_id) not in existing_api_providers:
deleted_tools.append(
{
"type": ToolProviderType.API.value,

519
api/poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -151,6 +151,7 @@ pytest-benchmark = "~4.0.0"
pytest-env = "~1.1.3"
pytest-mock = "~3.14.0"
types-beautifulsoup4 = "~4.12.0.20241020"
types-deprecated = "~1.2.15.20250304"
types-flask-cors = "~5.0.0.20240902"
types-flask-migrate = "~4.1.0.20250112"
types-html5lib = "~1.1.11.20241018"

View File

@ -22,6 +22,17 @@ class BillingService:
billing_info = cls._send_request("GET", "/subscription/info", params=params)
return billing_info
@classmethod
def get_knowledge_rate_limit(cls, tenant_id: str):
params = {"tenant_id": tenant_id}
knowledge_rate_limit = cls._send_request("GET", "/subscription/knowledge-rate-limit", params=params)
return {
"limit": knowledge_rate_limit.get("limit", 10),
"subscription_plan": knowledge_rate_limit.get("subscription_plan", "sandbox"),
}
@classmethod
def get_subscription(cls, plan: str, interval: str, prefilled_email: str = "", tenant_id: str = ""):
params = {"plan": plan, "interval": interval, "prefilled_email": prefilled_email, "tenant_id": tenant_id}

View File

@ -246,7 +246,7 @@ class DatasetService:
"No Embedding Model available. Please configure a valid provider in the Settings -> Model Provider."
)
except ProviderTokenNotInitError as ex:
raise ValueError(f"The dataset in unavailable, due to: {ex.description}")
raise ValueError(ex.description)
@staticmethod
def update_dataset(dataset_id, data, user):
@ -328,31 +328,75 @@ class DatasetService:
raise ValueError(ex.description)
else:
# add default plugin id to both setting sets, to make sure the plugin model provider is consistent
plugin_model_provider = dataset.embedding_model_provider
plugin_model_provider = str(ModelProviderID(plugin_model_provider))
new_plugin_model_provider = data["embedding_model_provider"]
new_plugin_model_provider = str(ModelProviderID(new_plugin_model_provider))
# Skip embedding model checks if not provided in the update request
if (
new_plugin_model_provider != plugin_model_provider
or data["embedding_model"] != dataset.embedding_model
"embedding_model_provider" not in data
or "embedding_model" not in data
or not data.get("embedding_model_provider")
or not data.get("embedding_model")
):
action = "update"
# If the dataset already has embedding model settings, use those
if dataset.embedding_model_provider and dataset.embedding_model:
# Keep existing values
filtered_data["embedding_model_provider"] = dataset.embedding_model_provider
filtered_data["embedding_model"] = dataset.embedding_model
# If collection_binding_id exists, keep it too
if dataset.collection_binding_id:
filtered_data["collection_binding_id"] = dataset.collection_binding_id
# Otherwise, don't try to update embedding model settings at all
# Remove these fields from filtered_data if they exist but are None/empty
if "embedding_model_provider" in filtered_data and not filtered_data["embedding_model_provider"]:
del filtered_data["embedding_model_provider"]
if "embedding_model" in filtered_data and not filtered_data["embedding_model"]:
del filtered_data["embedding_model"]
else:
skip_embedding_update = False
try:
model_manager = ModelManager()
embedding_model = model_manager.get_model_instance(
tenant_id=current_user.current_tenant_id,
provider=data["embedding_model_provider"],
model_type=ModelType.TEXT_EMBEDDING,
model=data["embedding_model"],
)
filtered_data["embedding_model"] = embedding_model.model
filtered_data["embedding_model_provider"] = embedding_model.provider
dataset_collection_binding = DatasetCollectionBindingService.get_dataset_collection_binding(
embedding_model.provider, embedding_model.model
)
filtered_data["collection_binding_id"] = dataset_collection_binding.id
# Handle existing model provider
plugin_model_provider = dataset.embedding_model_provider
plugin_model_provider_str = None
if plugin_model_provider:
plugin_model_provider_str = str(ModelProviderID(plugin_model_provider))
# Handle new model provider from request
new_plugin_model_provider = data["embedding_model_provider"]
new_plugin_model_provider_str = None
if new_plugin_model_provider:
new_plugin_model_provider_str = str(ModelProviderID(new_plugin_model_provider))
# Only update embedding model if both values are provided and different from current
if (
plugin_model_provider_str != new_plugin_model_provider_str
or data["embedding_model"] != dataset.embedding_model
):
action = "update"
model_manager = ModelManager()
try:
embedding_model = model_manager.get_model_instance(
tenant_id=current_user.current_tenant_id,
provider=data["embedding_model_provider"],
model_type=ModelType.TEXT_EMBEDDING,
model=data["embedding_model"],
)
except ProviderTokenNotInitError:
# If we can't get the embedding model, skip updating it
# and keep the existing settings if available
if dataset.embedding_model_provider and dataset.embedding_model:
filtered_data["embedding_model_provider"] = dataset.embedding_model_provider
filtered_data["embedding_model"] = dataset.embedding_model
if dataset.collection_binding_id:
filtered_data["collection_binding_id"] = dataset.collection_binding_id
# Skip the rest of the embedding model update
skip_embedding_update = True
if not skip_embedding_update:
filtered_data["embedding_model"] = embedding_model.model
filtered_data["embedding_model_provider"] = embedding_model.provider
dataset_collection_binding = (
DatasetCollectionBindingService.get_dataset_collection_binding(
embedding_model.provider, embedding_model.model
)
)
filtered_data["collection_binding_id"] = dataset_collection_binding.id
except LLMBadRequestError:
raise ValueError(
"No Embedding Model available. Please configure a valid provider "

View File

@ -42,6 +42,7 @@ class FeatureModel(BaseModel):
members: LimitationModel = LimitationModel(size=0, limit=1)
apps: LimitationModel = LimitationModel(size=0, limit=10)
vector_space: LimitationModel = LimitationModel(size=0, limit=5)
knowledge_rate_limit: int = 10
annotation_quota_limit: LimitationModel = LimitationModel(size=0, limit=10)
documents_upload_quota: LimitationModel = LimitationModel(size=0, limit=50)
docs_processing: str = "standard"
@ -53,6 +54,12 @@ class FeatureModel(BaseModel):
model_config = ConfigDict(protected_namespaces=())
class KnowledgeRateLimitModel(BaseModel):
enabled: bool = False
limit: int = 10
subscription_plan: str = ""
class SystemFeatureModel(BaseModel):
sso_enforced_for_signin: bool = False
sso_enforced_for_signin_protocol: str = ""
@ -82,6 +89,16 @@ class FeatureService:
return features
@classmethod
def get_knowledge_rate_limit(cls, tenant_id: str):
knowledge_rate_limit = KnowledgeRateLimitModel()
if dify_config.BILLING_ENABLED and tenant_id:
knowledge_rate_limit.enabled = True
limit_info = BillingService.get_knowledge_rate_limit(tenant_id)
knowledge_rate_limit.limit = limit_info.get("limit", 10)
knowledge_rate_limit.subscription_plan = limit_info.get("subscription_plan", "sandbox")
return knowledge_rate_limit
@classmethod
def get_system_features(cls) -> SystemFeatureModel:
system_features = SystemFeatureModel()
@ -151,6 +168,9 @@ class FeatureService:
if "model_load_balancing_enabled" in billing_info:
features.model_load_balancing_enabled = billing_info["model_load_balancing_enabled"]
if "knowledge_rate_limit" in billing_info:
features.knowledge_rate_limit = billing_info["knowledge_rate_limit"]["limit"]
@classmethod
def _fulfill_params_from_enterprise(cls, features):
enterprise_info = EnterpriseService.get_info()

View File

@ -29,7 +29,9 @@ logger = logging.getLogger(__name__)
class ToolTransformService:
@classmethod
def get_plugin_icon_url(cls, tenant_id: str, filename: str) -> str:
url_prefix = URL(dify_config.CONSOLE_API_URL) / "console" / "api" / "workspaces" / "current" / "plugin" / "icon"
url_prefix = (
URL(dify_config.CONSOLE_API_URL or "/") / "console" / "api" / "workspaces" / "current" / "plugin" / "icon"
)
return str(url_prefix % {"tenant_id": tenant_id, "filename": filename})
@classmethod
@ -37,7 +39,9 @@ class ToolTransformService:
"""
get tool provider icon url
"""
url_prefix = URL(dify_config.CONSOLE_API_URL) / "console" / "api" / "workspaces" / "current" / "tool-provider"
url_prefix = (
URL(dify_config.CONSOLE_API_URL or "/") / "console" / "api" / "workspaces" / "current" / "tool-provider"
)
if provider_type == ToolProviderType.BUILT_IN.value:
return str(url_prefix / "builtin" / provider_name / "icon")

View File

@ -18,6 +18,12 @@ def test_yarl_urls():
assert str(URL("https://dify.ai/api") / "v1") == expected_3
assert str(URL("https://dify.ai/api/") / "v1") == expected_3
expected_4 = "api"
assert str(URL("") / "api") == expected_4
expected_5 = "/api"
assert str(URL("/") / "api") == expected_5
with pytest.raises(ValueError) as e1:
str(URL("https://dify.ai") / "/api")
assert str(e1.value) == "Appending path '/api' starting from slash is forbidden"

View File

@ -959,6 +959,7 @@ PLUGIN_DEBUGGING_PORT=5003
EXPOSE_PLUGIN_DEBUGGING_HOST=localhost
EXPOSE_PLUGIN_DEBUGGING_PORT=5003
# If this key is changed, DIFY_INNER_API_KEY in plugin_daemon service must also be updated or agent node will fail.
PLUGIN_DIFY_INNER_API_KEY=QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1
PLUGIN_DIFY_INNER_API_URL=http://api:5001
@ -971,3 +972,5 @@ FORCE_VERIFYING_SIGNATURE=true
PLUGIN_PYTHON_ENV_INIT_TIMEOUT=120
PLUGIN_MAX_EXECUTION_TIMEOUT=600
# PIP_MIRROR_URL=https://pypi.tuna.tsinghua.edu.cn/simple
PIP_MIRROR_URL=

View File

@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: langgenius/dify-api:1.0.0
image: langgenius/dify-api:1.0.1
restart: always
environment:
# Use the shared environment variables.
@ -29,7 +29,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: langgenius/dify-api:1.0.0
image: langgenius/dify-api:1.0.1
restart: always
environment:
# Use the shared environment variables.
@ -53,7 +53,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.0.0
image: langgenius/dify-web:1.0.1
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
@ -91,8 +91,6 @@ services:
interval: 1s
timeout: 3s
retries: 30
ports:
- '${EXPOSE_DB_PORT:-5432}:5432'
# The redis cache.
redis:
@ -133,7 +131,7 @@ services:
# plugin daemon
plugin_daemon:
image: langgenius/dify-plugin-daemon:0.0.3-local
image: langgenius/dify-plugin-daemon:0.0.4-local
restart: always
environment:
# Use the shared environment variables.
@ -151,6 +149,7 @@ services:
FORCE_VERIFYING_SIGNATURE: ${FORCE_VERIFYING_SIGNATURE:-true}
PYTHON_ENV_INIT_TIMEOUT: ${PLUGIN_PYTHON_ENV_INIT_TIMEOUT:-120}
PLUGIN_MAX_EXECUTION_TIMEOUT: ${PLUGIN_MAX_EXECUTION_TIMEOUT:-600}
PIP_MIRROR_URL: ${PIP_MIRROR_URL:-}
ports:
- "${EXPOSE_PLUGIN_DEBUGGING_PORT:-5003}:${PLUGIN_DEBUGGING_PORT:-5003}"
volumes:

View File

@ -66,7 +66,7 @@ services:
# plugin daemon
plugin_daemon:
image: langgenius/dify-plugin-daemon:0.0.3-local
image: langgenius/dify-plugin-daemon:0.0.4-local
restart: always
environment:
# Use the shared environment variables.
@ -90,6 +90,7 @@ services:
FORCE_VERIFYING_SIGNATURE: ${FORCE_VERIFYING_SIGNATURE:-true}
PYTHON_ENV_INIT_TIMEOUT: ${PLUGIN_PYTHON_ENV_INIT_TIMEOUT:-120}
PLUGIN_MAX_EXECUTION_TIMEOUT: ${PLUGIN_MAX_EXECUTION_TIMEOUT:-600}
PIP_MIRROR_URL: ${PIP_MIRROR_URL:-}
ports:
- "${EXPOSE_PLUGIN_DAEMON_PORT:-5002}:${PLUGIN_DAEMON_PORT:-5002}"
- "${EXPOSE_PLUGIN_DEBUGGING_PORT:-5003}:${PLUGIN_DEBUGGING_PORT:-5003}"

View File

@ -415,11 +415,12 @@ x-shared-env: &shared-api-worker-env
FORCE_VERIFYING_SIGNATURE: ${FORCE_VERIFYING_SIGNATURE:-true}
PLUGIN_PYTHON_ENV_INIT_TIMEOUT: ${PLUGIN_PYTHON_ENV_INIT_TIMEOUT:-120}
PLUGIN_MAX_EXECUTION_TIMEOUT: ${PLUGIN_MAX_EXECUTION_TIMEOUT:-600}
PIP_MIRROR_URL: ${PIP_MIRROR_URL:-}
services:
# API service
api:
image: langgenius/dify-api:1.0.0
image: langgenius/dify-api:1.0.1
restart: always
environment:
# Use the shared environment variables.
@ -446,7 +447,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: langgenius/dify-api:1.0.0
image: langgenius/dify-api:1.0.1
restart: always
environment:
# Use the shared environment variables.
@ -470,7 +471,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.0.0
image: langgenius/dify-web:1.0.1
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
@ -508,8 +509,6 @@ services:
interval: 1s
timeout: 3s
retries: 30
ports:
- '${EXPOSE_DB_PORT:-5432}:5432'
# The redis cache.
redis:
@ -550,7 +549,7 @@ services:
# plugin daemon
plugin_daemon:
image: langgenius/dify-plugin-daemon:0.0.3-local
image: langgenius/dify-plugin-daemon:0.0.4-local
restart: always
environment:
# Use the shared environment variables.
@ -568,6 +567,7 @@ services:
FORCE_VERIFYING_SIGNATURE: ${FORCE_VERIFYING_SIGNATURE:-true}
PYTHON_ENV_INIT_TIMEOUT: ${PLUGIN_PYTHON_ENV_INIT_TIMEOUT:-120}
PLUGIN_MAX_EXECUTION_TIMEOUT: ${PLUGIN_MAX_EXECUTION_TIMEOUT:-600}
PIP_MIRROR_URL: ${PIP_MIRROR_URL:-}
ports:
- "${EXPOSE_PLUGIN_DEBUGGING_PORT:-5003}:${PLUGIN_DEBUGGING_PORT:-5003}"
volumes:

View File

@ -118,3 +118,5 @@ FORCE_VERIFYING_SIGNATURE=true
PLUGIN_PYTHON_ENV_INIT_TIMEOUT=120
PLUGIN_MAX_EXECUTION_TIMEOUT=600
# PIP_MIRROR_URL=https://pypi.tuna.tsinghua.edu.cn/simple
PIP_MIRROR_URL=

View File

@ -4,19 +4,6 @@ server {
listen ${NGINX_PORT};
server_name ${NGINX_SERVER_NAME};
# Rule 1: Handle application entry points (preserve /app/{id})
location ~ ^/app/[a-f0-9-]+$ {
proxy_pass http://api:5001;
include proxy.conf;
}
# Rule 2: Handle static resource requests (remove /app/{id} prefix)
location ~ ^/app/[a-f0-9-]+/(console/api/.*)$ {
rewrite ^/app/[a-f0-9-]+/(.*)$ /$1 break;
proxy_pass http://api:5001;
include proxy.conf;
}
location /console/api {
proxy_pass http://api:5001;
include proxy.conf;

View File

@ -83,7 +83,7 @@ const VersionInfoModal: FC<VersionInfoModalProps> = ({
</div>
<Input
value={title}
placeholder={t('workflow.versionHistory.nameThisVersion')}
placeholder={`${t('workflow.versionHistory.nameThisVersion')}${t('workflow.panel.optional')}`}
onChange={handleTitleChange}
destructive={titleError}
/>
@ -94,7 +94,7 @@ const VersionInfoModal: FC<VersionInfoModalProps> = ({
</div>
<Textarea
value={releaseNotes}
placeholder={t('workflow.versionHistory.releaseNotesPlaceholder')}
placeholder={`${t('workflow.versionHistory.releaseNotesPlaceholder')}${t('workflow.panel.optional')}`}
onChange={handleDescriptionChange}
destructive={releaseNotesError}
/>

View File

@ -11,7 +11,7 @@ const GroupName: FC<IGroupNameProps> = ({
}) => {
return (
<div className='flex items-center mb-1'>
<div className='mr-3 leading-[18px] text-xs font-semibold text-gray-500 uppercase'>{name}</div>
<div className='mr-3 leading-[18px] text-xs font-semibold text-text-tertiary uppercase'>{name}</div>
<div className='grow h-[1px]'
style={{
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)',

View File

@ -20,6 +20,7 @@ import {
Clipboard,
ClipboardCheck,
} from '@/app/components/base/icons/src/vender/line/files'
import Button from '@/app/components/base/button'
import Tooltip from '@/app/components/base/tooltip'
import PromptEditor from '@/app/components/base/prompt-editor'
import ConfigContext from '@/context/debug-configuration'
@ -150,19 +151,20 @@ const AdvancedPromptInput: FC<Props> = ({
<RiErrorWarningFill className='mr-1 w-4 h-4 text-[#F79009]' />
<div className='leading-[18px] text-[13px] font-medium text-[#DC6803]'>{t('appDebug.promptMode.contextMissing')}</div>
</div>
<div
className='flex items-center h-6 px-2 rounded-md bg-[#fff] border border-gray-200 shadow-xs text-xs font-medium text-primary-600 cursor-pointer'
<Button
size='small'
variant='secondary-accent'
onClick={onHideContextMissingTip}
>{t('common.operation.ok')}</div>
>{t('common.operation.ok')}</Button>
</div>
)
return (
<div className={`relative ${!isContextMissing ? s.gradientBorder : s.warningBorder}`}>
<div className='rounded-xl bg-white'>
<div className={`bg-gradient-to-r from-components-input-border-active-prompt-1 to-components-input-border-active-prompt-2 rounded-xl p-0.5 shadow-xs ${!isContextMissing ? '' : s.warningBorder}`}>
<div className='rounded-xl bg-background-default'>
{isContextMissing
? contextMissing
: (
<div className={cn(s.boxHeader, 'flex justify-between items-center h-11 pt-2 pr-3 pb-1 pl-4 rounded-tl-xl rounded-tr-xl bg-white hover:shadow-xs')}>
<div className={cn(s.boxHeader, 'flex justify-between items-center h-11 pt-2 pr-3 pb-1 pl-4 rounded-tl-xl rounded-tr-xl bg-background-default hover:shadow-xs')}>
{isChatMode
? (
<MessageTypeSelector value={type} onChange={onTypeChange} />
@ -182,30 +184,30 @@ const AdvancedPromptInput: FC<Props> = ({
</div>)}
<div className={cn(s.optionWrap, 'items-center space-x-1')}>
{canDelete && (
<RiDeleteBinLine onClick={onDelete} className='h-6 w-6 p-1 text-gray-500 cursor-pointer' />
<RiDeleteBinLine onClick={onDelete} className='h-6 w-6 p-1 text-text-tertiary cursor-pointer' />
)}
{!isCopied
? (
<Clipboard className='h-6 w-6 p-1 text-gray-500 cursor-pointer' onClick={() => {
<Clipboard className='h-6 w-6 p-1 text-text-tertiary cursor-pointer' onClick={() => {
copy(value)
setIsCopied(true)
}} />
)
: (
<ClipboardCheck className='h-6 w-6 p-1 text-gray-500' />
<ClipboardCheck className='h-6 w-6 p-1 text-text-tertiary' />
)}
</div>
</div>
)}
<PromptEditorHeightResizeWrap
className='px-4 min-h-[102px] overflow-y-auto text-sm text-gray-700'
className='px-4 min-h-[102px] overflow-y-auto text-sm text-text-secondary'
height={editorHeight}
minHeight={minHeight}
onHeightChange={setEditorHeight}
footer={(
<div className='pl-4 pb-2 flex'>
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{value.length}</div>
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-divider-regular text-xs text-text-tertiary">{value.length}</div>
</div>
)}
hideResize={noResize}

View File

@ -39,21 +39,17 @@ const ConfirmAddVar: FC<IConfirmAddVarProps> = ({
}}>
<div
ref={mainContentRef}
className='w-[420px] rounded-xl bg-gray-50 p-6'
className='w-[420px] rounded-xl bg-components-panel-bg p-6'
style={{
boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)',
}}
>
<div className='flex items-start space-x-3'>
<div
className='shrink-0 flex items-center justify-center h-10 w-10 rounded-xl border border-gray-100'
style={{
backgroundColor: 'rgba(255, 255, 255, 0.9)',
boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)',
}}
className='shrink-0 flex items-center justify-center h-10 w-10 bg-components-card-bg-alt rounded-xl border border-components-card-border shadow-lg'
>{VarIcon}</div>
<div className='grow-1'>
<div className='text-sm font-medium text-gray-900'>{t('appDebug.autoAddVar')}</div>
<div className='text-sm font-medium text-text-primary'>{t('appDebug.autoAddVar')}</div>
<div className='flex flex-wrap mt-[15px] max-h-[66px] overflow-y-auto px-1 space-x-1'>
{varNameArr.map(name => (
<VarHighlight key={name} name={name} />

View File

@ -8,6 +8,7 @@ import {
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import SimplePromptInput from './simple-prompt-input'
import Button from '@/app/components/base/button'
import AdvancedMessageInput from '@/app/components/app/configuration/config-prompt/advanced-prompt-input'
import { PromptRole } from '@/models/debug'
import type { PromptItem, PromptVariable } from '@/models/debug'
@ -155,12 +156,12 @@ const Prompt: FC<IPromptProps> = ({
}
</div>
{(modelModeType === ModelModeType.chat && (currentAdvancedPrompt as PromptItem[]).length < MAX_PROMPT_MESSAGE_LENGTH) && (
<div
<Button
onClick={handleAddMessage}
className='mt-3 flex items-center h-8 justify-center bg-gray-50 rounded-lg cursor-pointer text-[13px] font-medium text-gray-700 space-x-2'>
<RiAddLine className='w-4 h-4' />
className='mt-3 w-full'>
<RiAddLine className='w-4 h-4 mr-2' />
<div>{t('appDebug.promptMode.operation.addMessage')}</div>
</div>
</Button>
)}
</div>
)

View File

@ -29,7 +29,7 @@ const MessageTypeSelector: FC<Props> = ({
<ChevronSelectorVertical className='w-3 h-3 ' />
</div>
{showOption && (
<div className='absolute z-10 top-[30px] p-1 border border-gray-200 shadow-lg rounded-lg bg-white'>
<div className='absolute z-10 top-[30px] p-1 border border-components-panel-border shadow-lg rounded-lg bg-components-panel-bg'>
{allTypes.map(type => (
<div
key={type}
@ -37,7 +37,7 @@ const MessageTypeSelector: FC<Props> = ({
setHide()
onChange(type)
}}
className='flex items-center h-9 min-w-[44px] px-3 rounded-lg cursor-pointer text-sm font-medium text-gray-700 uppercase hover:bg-gray-50'
className='flex items-center h-9 min-w-[44px] px-3 rounded-lg cursor-pointer text-sm font-medium text-text-secondary uppercase hover:bg-state-base-hover'
>{type}</div>
))
}

View File

@ -6,7 +6,6 @@ import { useBoolean } from 'ahooks'
import produce from 'immer'
import { useContext } from 'use-context-selector'
import ConfirmAddVar from './confirm-add-var'
import s from './style.module.css'
import PromptEditorHeightResizeWrap from './prompt-editor-height-resize-wrap'
import cn from '@/utils/classnames'
import type { PromptVariable } from '@/models/debug'
@ -48,7 +47,6 @@ const Prompt: FC<ISimplePromptInput> = ({
readonly = false,
onChange,
noTitle,
gradientBorder,
editorHeight: initEditorHeight,
noResize,
}) => {
@ -161,12 +159,12 @@ const Prompt: FC<ISimplePromptInput> = ({
const [editorHeight, setEditorHeight] = useState(minHeight)
return (
<div className={cn((!readonly || gradientBorder) ? `${s.gradientBorder}` : 'bg-gray-50', ' relative shadow-md')}>
<div className='rounded-xl bg-[#EEF4FF]'>
<div className={cn('relative bg-gradient-to-r from-components-input-border-active-prompt-1 to-components-input-border-active-prompt-2 rounded-xl p-0.5 shadow-xs')}>
<div className='rounded-xl bg-background-section-burn'>
{!noTitle && (
<div className="flex justify-between items-center h-11 pl-3 pr-6">
<div className="flex justify-between items-center h-11 pl-3 pr-2.5">
<div className="flex items-center space-x-1">
<div className='h2'>{mode !== AppType.completion ? t('appDebug.chatSubTitle') : t('appDebug.completionSubTitle')}</div>
<div className='h2 system-sm-semibold-uppercase text-text-secondary'>{mode !== AppType.completion ? t('appDebug.chatSubTitle') : t('appDebug.completionSubTitle')}</div>
{!readonly && (
<Tooltip
popupContent={
@ -186,14 +184,14 @@ const Prompt: FC<ISimplePromptInput> = ({
)}
<PromptEditorHeightResizeWrap
className='px-4 pt-2 min-h-[228px] bg-white rounded-t-xl text-sm text-gray-700'
className='px-4 pt-2 min-h-[228px] bg-background-default rounded-t-xl text-sm text-text-secondary'
height={editorHeight}
minHeight={minHeight}
onHeightChange={setEditorHeight}
hideResize={noResize}
footer={(
<div className='pl-4 pb-2 flex bg-white rounded-b-xl'>
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{promptTemplate.length}</div>
<div className='pl-4 pb-2 flex bg-background-default rounded-b-xl'>
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-components-badge-bg-gray-soft text-xs text-text-tertiary">{promptTemplate.length}</div>
</div>
)}
>

View File

@ -2,7 +2,10 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Generator } from '@/app/components/base/icons/src/vender/other'
import {
RiSparklingFill,
} from '@remixicon/react'
import Button from '@/app/components/base/button'
export type IAutomaticBtnProps = {
onClick: () => void
@ -13,12 +16,10 @@ const AutomaticBtn: FC<IAutomaticBtnProps> = ({
const { t } = useTranslation()
return (
<div className='flex space-x-1 items-center !h-8 cursor-pointer'
onClick={onClick}
>
<Generator className='w-3.5 h-3.5 text-indigo-600' />
<span className='text-xs font-semibold text-indigo-600'>{t('appDebug.operation.automatic')}</span>
</div>
<Button variant='secondary-accent' size='small' onClick={onClick}>
<RiSparklingFill className='w-3.5 h-3.5 mr-1' />
<span className=''>{t('appDebug.operation.automatic')}</span>
</Button>
)
}
export default React.memo(AutomaticBtn)

View File

@ -38,7 +38,7 @@ import ModelName from '@/app/components/header/account-setting/model-provider-pa
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
export interface IGetAutomaticResProps {
export type IGetAutomaticResProps = {
mode: AppType
model: Model
isShow: boolean
@ -54,11 +54,11 @@ const TryLabel: FC<{
}> = ({ Icon, text, onClick }) => {
return (
<div
className='mt-2 mr-1 shrink-0 flex h-7 items-center px-2 bg-gray-100 rounded-lg cursor-pointer'
className='mt-2 mr-1 shrink-0 flex h-7 items-center px-2 bg-components-button-secondary-bg rounded-lg cursor-pointer'
onClick={onClick}
>
<Icon className='w-4 h-4 text-gray-500'></Icon>
<div className='ml-1 text-xs font-medium text-gray-700'>{text}</div>
<Icon className='w-4 h-4 text-text-tertiary'></Icon>
<div className='ml-1 text-xs font-medium text-text-secondary'>{text}</div>
</div>
)
}
@ -140,14 +140,14 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
const renderLoading = (
<div className='w-0 grow flex flex-col items-center justify-center h-full space-y-3'>
<Loading />
<div className='text-[13px] text-gray-400'>{t('appDebug.generate.loading')}</div>
<div className='text-[13px] text-text-tertiary'>{t('appDebug.generate.loading')}</div>
</div>
)
const renderNoData = (
<div className='w-0 grow flex flex-col items-center px-8 justify-center h-full space-y-3'>
<Generator className='w-14 h-14 text-gray-300' />
<div className='leading-5 text-center text-[13px] font-normal text-gray-400'>
<Generator className='w-14 h-14 text-text-tertiary' />
<div className='leading-5 text-center text-[13px] font-normal text-text-tertiary'>
<div>{t('appDebug.generate.noDataLine1')}</div>
<div>{t('appDebug.generate.noDataLine2')}</div>
</div>
@ -193,10 +193,10 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
closable
>
<div className='flex h-[680px] flex-wrap'>
<div className='w-[570px] shrink-0 p-6 h-full overflow-y-auto border-r border-gray-100'>
<div className='w-[570px] shrink-0 p-6 h-full overflow-y-auto border-r border-divider-regular'>
<div className='mb-8'>
<div className={`leading-[28px] text-lg font-bold ${s.textGradient}`}>{t('appDebug.generate.title')}</div>
<div className='mt-1 text-[13px] font-normal text-gray-500'>{t('appDebug.generate.description')}</div>
<div className='mt-1 text-[13px] font-normal text-text-tertiary'>{t('appDebug.generate.description')}</div>
</div>
<div className='flex items-center mb-8'>
<ModelIcon
@ -213,7 +213,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
</div>
<div >
<div className='flex items-center'>
<div className='mr-3 shrink-0 leading-[18px] text-xs font-semibold text-gray-500 uppercase'>{t('appDebug.generate.tryIt')}</div>
<div className='mr-3 shrink-0 leading-[18px] text-xs font-semibold text-text-tertiary uppercase'>{t('appDebug.generate.tryIt')}</div>
<div className='grow h-px' style={{
background: 'linear-gradient(to right, rgba(243, 244, 246, 1), rgba(243, 244, 246, 0))',
}}></div>
@ -232,7 +232,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
{/* inputs */}
<div className='mt-6'>
<div className='text-[0px]'>
<div className='mb-2 leading-5 text-sm font-medium text-gray-900'>{t('appDebug.generate.instruction')}</div>
<div className='mb-2 leading-5 text-sm font-medium text-text-primary'>{t('appDebug.generate.instruction')}</div>
<Textarea
className="h-[200px] resize-none"
placeholder={t('appDebug.generate.instructionPlaceHolder') as string}
@ -256,7 +256,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
{(!isLoading && res) && (
<div className='w-0 grow p-6 pb-0 h-full'>
<div className='shrink-0 mb-3 leading-[160%] text-base font-semibold text-gray-800'>{t('appDebug.generate.resTitle')}</div>
<div className='shrink-0 mb-3 leading-[160%] text-base font-semibold text-text-secondary'>{t('appDebug.generate.resTitle')}</div>
<div className={cn('max-h-[555px] overflow-y-auto', !isInLLMNode && 'pb-2')}>
<ConfigPrompt
mode={mode}
@ -301,7 +301,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
)}
</div>
<div className='flex justify-end py-4 bg-white'>
<div className='flex justify-end py-4 bg-background-default'>
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button variant='primary' className='ml-2' onClick={() => {
setShowConfirmOverwrite(true)

View File

@ -56,7 +56,7 @@ const WorkflowProcessItem = ({
>
{
running && (
<RiLoader2Line className='shrink-0 mr-1 w-3.5 h-3.5 text-text-tertiary' />
<RiLoader2Line className='shrink-0 mr-1 w-3.5 h-3.5 animate-spin text-text-tertiary' />
)
}
{

View File

@ -3,6 +3,7 @@ import mermaid from 'mermaid'
import { usePrevious } from 'ahooks'
import { useTranslation } from 'react-i18next'
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
import { cleanUpSvgCode } from './utils'
import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
import cn from '@/utils/classnames'
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
@ -44,7 +45,7 @@ const Flowchart = React.forwardRef((props: {
try {
if (typeof window !== 'undefined' && mermaidAPI) {
const svgGraph = await mermaidAPI.render('flowchart', PrimitiveCode)
const base64Svg: any = await svgToBase64(svgGraph.svg)
const base64Svg: any = await svgToBase64(cleanUpSvgCode(svgGraph.svg))
setSvgCode(base64Svg)
setIsLoading(false)
}

View File

@ -0,0 +1,8 @@
import { cleanUpSvgCode } from './utils'
describe('cleanUpSvgCode', () => {
it('replaces old-style <br> tags with the new style', () => {
const result = cleanUpSvgCode('<br>test<br>')
expect(result).toEqual('<br/>test<br/>')
})
})

View File

@ -0,0 +1,3 @@
export function cleanUpSvgCode(svgCode: string): string {
return svgCode.replaceAll('<br>', '<br/>')
}

View File

@ -2,7 +2,7 @@
@layer components {
.premium-badge {
@apply inline-flex justify-center items-center rounded-full border box-border border-[rgba(255,255,255,0.8)] text-white
@apply inline-flex justify-center items-center rounded-md border box-border border-white/95 text-white
}
/* m is for the regular button */

View File

@ -62,7 +62,7 @@ const PremiumBadge: React.FC<PremiumBadgeProps> = ({
<Highlight
className={classNames(
'absolute top-0 opacity-50 hover:opacity-80',
size === 's' ? 'h-4.5 w-12' : 'h-6 w-12',
size === 's' ? 'h-[18px] w-12' : 'h-6 w-12',
)}
style={{
right: '50%',

View File

@ -23,7 +23,7 @@ import FloatRightContainer from '@/app/components/base/float-right-container'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { LayoutRight2LineMod } from '@/app/components/base/icons/src/public/knowledge'
import { useCheckSegmentBatchImportProgress, useChildSegmentListKey, useSegmentBatchImport, useSegmentListKey } from '@/service/knowledge/use-segment'
import { useDocumentDetail, useDocumentMetadata } from '@/service/knowledge/use-document'
import { useDocumentDetail, useDocumentMetadata, useInvalidDocumentList } from '@/service/knowledge/use-document'
import { useInvalid } from '@/service/use-base'
type DocumentContextValue = {
@ -152,17 +152,22 @@ const DocumentDetail: FC<Props> = ({ datasetId, documentId }) => {
const invalidChunkList = useInvalid(useSegmentListKey)
const invalidChildChunkList = useInvalid(useChildSegmentListKey)
const invalidDocumentList = useInvalidDocumentList(datasetId)
const handleOperate = (operateName?: string) => {
invalidDocumentList()
if (operateName === 'delete') {
backToPrev()
}
else {
detailMutate()
setTimeout(() => {
invalidChunkList()
invalidChildChunkList()
}, 5000)
// If operation is not rename, refresh the chunk list after 5 seconds
if (operateName) {
setTimeout(() => {
invalidChunkList()
invalidChildChunkList()
}, 5000)
}
}
}

View File

@ -1,11 +1,10 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import { useRouter } from 'next/navigation'
import { useDebounce, useDebounceFn } from 'ahooks'
import { groupBy, omit } from 'lodash-es'
import { groupBy } from 'lodash-es'
import { PlusIcon } from '@heroicons/react/24/solid'
import { RiDraftLine, RiExternalLinkLine } from '@remixicon/react'
import AutoDisabledDocument from '../common/document-status-with-action/auto-disabled-document'
@ -15,16 +14,16 @@ import Loading from '@/app/components/base/loading'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import { get } from '@/service/base'
import { createDocument, fetchDocuments } from '@/service/datasets'
import { createDocument } from '@/service/datasets'
import { useDatasetDetailContext } from '@/context/dataset-detail'
import { NotionPageSelectorModal } from '@/app/components/base/notion-page-selector'
import type { NotionPage } from '@/models/common'
import type { CreateDocumentReq } from '@/models/datasets'
import { DataSourceType } from '@/models/datasets'
import { DataSourceType, ProcessMode } from '@/models/datasets'
import IndexFailed from '@/app/components/datasets/common/document-status-with-action/index-failed'
import { useProviderContext } from '@/context/provider-context'
import cn from '@/utils/classnames'
import { useInvalidDocumentDetailKey } from '@/service/knowledge/use-document'
import { useDocumentList, useInvalidDocumentDetailKey, useInvalidDocumentList } from '@/service/knowledge/use-document'
import { useInvalid } from '@/service/use-base'
import { useChildSegmentListKey, useSegmentListKey } from '@/service/knowledge/use-segment'
import useEditDocumentMetadata from '../metadata/hooks/use-edit-dataset-metadata'
@ -81,7 +80,7 @@ type IDocumentsProps = {
}
export const fetcher = (url: string) => get(url, {}, {})
const DEFAULT_LIMIT = 15
const DEFAULT_LIMIT = 10
const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
const { t } = useTranslation()
@ -102,33 +101,33 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
const debouncedSearchValue = useDebounce(searchValue, { wait: 500 })
const query = useMemo(() => {
return { page: currPage + 1, limit, keyword: debouncedSearchValue, fetch: isDataSourceNotion ? true : '' }
}, [currPage, debouncedSearchValue, isDataSourceNotion, limit])
const { data: documentsRes, mutate, isLoading: isListLoading } = useSWR(
{
action: 'fetchDocuments',
datasetId,
params: query,
const { data: documentsRes, isFetching: isListLoading } = useDocumentList({
datasetId,
query: {
page: currPage + 1,
limit,
keyword: debouncedSearchValue,
},
apiParams => fetchDocuments(omit(apiParams, 'action')),
{ refreshInterval: (isDataSourceNotion && timerCanRun) ? 2500 : 0 },
)
refetchInterval: (isDataSourceNotion && timerCanRun) ? 2500 : 0,
})
const invalidDocumentList = useInvalidDocumentList(datasetId)
const [isMuting, setIsMuting] = useState(false)
useEffect(() => {
if (!isListLoading && isMuting)
setIsMuting(false)
}, [isListLoading, isMuting])
if (documentsRes) {
const totalPages = Math.ceil(documentsRes.total / limit)
if (totalPages < currPage + 1)
setCurrPage(totalPages === 0 ? 0 : totalPages - 1)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [documentsRes])
const invalidDocumentDetail = useInvalidDocumentDetailKey()
const invalidChunkList = useInvalid(useSegmentListKey)
const invalidChildChunkList = useInvalid(useChildSegmentListKey)
const handleUpdate = useCallback(() => {
setIsMuting(true)
mutate()
invalidDocumentList()
invalidDocumentDetail()
setTimeout(() => {
invalidChunkList()
@ -178,8 +177,6 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
router.push(`/datasets/${datasetId}/documents/create`)
}
const isLoading = isListLoading // !documentsRes && !error
const handleSaveNotionPageSelected = async (selectedPages: NotionPage[]) => {
const workspacesMap = groupBy(selectedPages, 'workspace_id')
const workspaces = Object.keys(workspacesMap).map((workspaceId) => {
@ -212,7 +209,7 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
indexing_technique: dataset?.indexing_technique,
process_rule: {
rules: {},
mode: 'automatic',
mode: ProcessMode.general,
},
} as CreateDocumentReq
@ -220,7 +217,7 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
datasetId,
body: params,
})
mutate()
invalidDocumentList()
setTimerCanRun(true)
// mutateDatasetIndexingStatus(undefined, { revalidate: true })
setNotionPageSelectorModalVisible(false)
@ -310,7 +307,7 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
)}
</div>
</div>
{(isLoading && !isMuting)
{isListLoading
? <Loading type='app' />
: total > 0
? <List

View File

@ -516,54 +516,52 @@ const DocumentList: FC<IDocumentListProps> = ({
}
return (
<div className='relative w-full h-full overflow-x-auto'>
<table className={`min-w-[700px] max-w-full w-full border-collapse border-0 text-sm mt-3 ${s.documentTable}`}>
<thead className="h-8 leading-8 border-b border-divider-subtle text-text-tertiary font-medium text-xs uppercase">
<tr>
<td className='w-12'>
<div className='flex items-center' onClick={e => e.stopPropagation()}>
{embeddingAvailable && (
<div className='flex flex-col relative w-full h-full'>
<div className='grow overflow-x-auto'>
<table className={`min-w-[700px] max-w-full w-full border-collapse border-0 text-sm mt-3 ${s.documentTable}`}>
<thead className="h-8 leading-8 border-b border-divider-subtle text-text-tertiary font-medium text-xs uppercase">
<tr>
<td className='w-12'>
<div className='flex items-center' onClick={e => e.stopPropagation()}>
<Checkbox
className='shrink-0 mr-2'
checked={isAllSelected}
mixed={!isAllSelected && isSomeSelected}
onCheck={onSelectedAll}
/>
)}
#
</div>
</td>
<td>
<div className='flex'>
{t('datasetDocuments.list.table.header.fileName')}
</div>
</td>
<td className='w-[130px]'>{t('datasetDocuments.list.table.header.chunkingMode')}</td>
<td className='w-24'>{t('datasetDocuments.list.table.header.words')}</td>
<td className='w-44'>{t('datasetDocuments.list.table.header.hitCount')}</td>
<td className='w-44'>
<div className='flex items-center' onClick={onClickSort}>
{t('datasetDocuments.list.table.header.uploadTime')}
<ArrowDownIcon className={cn('ml-0.5 h-3 w-3 stroke-current stroke-2 cursor-pointer', enableSort ? 'text-text-tertiary' : 'text-text-disabled')} />
</div>
</td>
<td className='w-40'>{t('datasetDocuments.list.table.header.status')}</td>
<td className='w-20'>{t('datasetDocuments.list.table.header.action')}</td>
</tr>
</thead>
<tbody className="text-text-secondary">
{localDocs.map((doc, index) => {
const isFile = doc.data_source_type === DataSourceType.FILE
const fileType = isFile ? doc.data_source_detail_dict?.upload_file?.extension : ''
return <tr
key={doc.id}
className={'border-b border-divider-subtle h-8 hover:bg-background-default-hover cursor-pointer'}
onClick={() => {
router.push(`/datasets/${datasetId}/documents/${doc.id}`)
}}>
<td className='text-left align-middle text-text-tertiary text-xs'>
<div className='flex items-center' onClick={e => e.stopPropagation()}>
{embeddingAvailable && (
#
</div>
</td>
<td>
<div className='flex'>
{t('datasetDocuments.list.table.header.fileName')}
</div>
</td>
<td className='w-[130px]'>{t('datasetDocuments.list.table.header.chunkingMode')}</td>
<td className='w-24'>{t('datasetDocuments.list.table.header.words')}</td>
<td className='w-44'>{t('datasetDocuments.list.table.header.hitCount')}</td>
<td className='w-44'>
<div className='flex items-center' onClick={onClickSort}>
{t('datasetDocuments.list.table.header.uploadTime')}
<ArrowDownIcon className={cn('ml-0.5 h-3 w-3 stroke-current stroke-2 cursor-pointer', enableSort ? 'text-text-tertiary' : 'text-text-disabled')} />
</div>
</td>
<td className='w-40'>{t('datasetDocuments.list.table.header.status')}</td>
<td className='w-20'>{t('datasetDocuments.list.table.header.action')}</td>
</tr>
</thead>
<tbody className="text-text-secondary">
{localDocs.map((doc, index) => {
const isFile = doc.data_source_type === DataSourceType.FILE
const fileType = isFile ? doc.data_source_detail_dict?.upload_file?.extension : ''
return <tr
key={doc.id}
className={'border-b border-divider-subtle h-8 hover:bg-background-default-hover cursor-pointer'}
onClick={() => {
router.push(`/datasets/${datasetId}/documents/${doc.id}`)
}}>
<td className='text-left align-middle text-text-tertiary text-xs'>
<div className='flex items-center' onClick={e => e.stopPropagation()}>
<Checkbox
className='shrink-0 mr-2'
checked={selectedIds.includes(doc.id)}
@ -575,67 +573,66 @@ const DocumentList: FC<IDocumentListProps> = ({
)
}}
/>
)}
{/* {doc.position} */}
{index + 1}
</div>
</td>
<td>
<div className={'group flex items-center mr-6 hover:mr-0 max-w-[460px]'}>
<div className='shrink-0'>
{doc?.data_source_type === DataSourceType.NOTION && <NotionIcon className='inline-flex -mt-[3px] mr-1.5 align-middle' type='page' src={doc.data_source_info.notion_page_icon} />}
{doc?.data_source_type === DataSourceType.FILE && <FileTypeIcon type={extensionToFileType(doc?.data_source_info?.upload_file?.extension ?? fileType)} className='mr-1.5' />}
{doc?.data_source_type === DataSourceType.WEB && <Globe01 className='inline-flex -mt-[3px] mr-1.5 align-middle' />}
{/* {doc.position} */}
{index + 1}
</div>
<span className='text-sm truncate grow-1'>{doc.name}</span>
<div className='group-hover:flex group-hover:ml-auto hidden shrink-0'>
<Tooltip
popupContent={t('datasetDocuments.list.table.rename')}
>
<div
className='p-1 rounded-md cursor-pointer hover:bg-state-base-hover'
onClick={(e) => {
e.stopPropagation()
handleShowRenameModal(doc)
}}
</td>
<td>
<div className={'group flex items-center mr-6 hover:mr-0 max-w-[460px]'}>
<div className='shrink-0'>
{doc?.data_source_type === DataSourceType.NOTION && <NotionIcon className='inline-flex mt-[-3px] mr-1.5 align-middle' type='page' src={doc.data_source_info.notion_page_icon} />}
{doc?.data_source_type === DataSourceType.FILE && <FileTypeIcon type={extensionToFileType(doc?.data_source_info?.upload_file?.extension ?? fileType)} className='mr-1.5' />}
{doc?.data_source_type === DataSourceType.WEB && <Globe01 className='inline-flex mt-[-3px] mr-1.5 align-middle' />}
</div>
<span className='text-sm truncate grow-1'>{doc.name}</span>
<div className='group-hover:flex group-hover:ml-auto hidden shrink-0'>
<Tooltip
popupContent={t('datasetDocuments.list.table.rename')}
>
<Edit03 className='w-4 h-4 text-text-tertiary' />
</div>
</Tooltip>
<div
className='p-1 rounded-md cursor-pointer hover:bg-state-base-hover'
onClick={(e) => {
e.stopPropagation()
handleShowRenameModal(doc)
}}
>
<Edit03 className='w-4 h-4 text-text-tertiary' />
</div>
</Tooltip>
</div>
</div>
</div>
</td>
<td>
<ChunkingModeLabel
isGeneralMode={isGeneralMode}
isQAMode={isQAMode}
/>
</td>
<td>{renderCount(doc.word_count)}</td>
<td>{renderCount(doc.hit_count)}</td>
<td className='text-text-secondary text-[13px]'>
{formatTime(doc.created_at, t('datasetHitTesting.dateTimeFormat') as string)}
</td>
<td>
{
(['indexing', 'splitting', 'parsing', 'cleaning'].includes(doc.indexing_status) && doc?.data_source_type === DataSourceType.NOTION)
? <ProgressBar percent={doc.percent || 0} />
: <StatusItem status={doc.display_status} />
}
</td>
<td>
<OperationAction
embeddingAvailable={embeddingAvailable}
datasetId={datasetId}
detail={pick(doc, ['name', 'enabled', 'archived', 'id', 'data_source_type', 'doc_form'])}
onUpdate={onUpdate}
/>
</td>
</tr>
})}
</tbody>
</table>
</td>
<td>
<ChunkingModeLabel
isGeneralMode={isGeneralMode}
isQAMode={isQAMode}
/>
</td>
<td>{renderCount(doc.word_count)}</td>
<td>{renderCount(doc.hit_count)}</td>
<td className='text-text-secondary text-[13px]'>
{formatTime(doc.created_at, t('datasetHitTesting.dateTimeFormat') as string)}
</td>
<td>
{
(['indexing', 'splitting', 'parsing', 'cleaning'].includes(doc.indexing_status) && doc?.data_source_type === DataSourceType.NOTION)
? <ProgressBar percent={doc.percent || 0} />
: <StatusItem status={doc.display_status} />
}
</td>
<td>
<OperationAction
embeddingAvailable={embeddingAvailable}
datasetId={datasetId}
detail={pick(doc, ['name', 'enabled', 'archived', 'id', 'data_source_type', 'doc_form'])}
onUpdate={onUpdate}
/>
</td>
</tr>
})}
</tbody>
</table>
</div>
{(selectedIds.length > 0) && (
<BatchAction
className='absolute left-0 bottom-16 z-20'
@ -651,10 +648,10 @@ const DocumentList: FC<IDocumentListProps> = ({
/>
)}
{/* Show Pagination only if the total is more than the limit */}
{pagination.total && pagination.total > (pagination.limit || 10) && (
{pagination.total && (
<Pagination
{...pagination}
className='absolute bottom-0 left-0 w-full px-0 pb-0'
className='shrink-0 w-full px-0 pb-0'
/>
)}

View File

@ -1,5 +0,0 @@
.modal {
max-width: 480px !important;
width: 480px !important;
padding: 24px 32px !important;
}

View File

@ -3,9 +3,8 @@ import { useTranslation } from 'react-i18next'
import Link from 'next/link'
import dayjs from 'dayjs'
import { RiCloseLine } from '@remixicon/react'
import s from './index.module.css'
import classNames from '@/utils/classnames'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import type { LangGeniusVersionResponse } from '@/models/common'
import { IS_CE_EDITION } from '@/config'
import LogoSite from '@/app/components/base/logo/logo-site'
@ -29,18 +28,18 @@ export default function AccountAbout({
<Modal
isShow
onClose={() => { }}
className={s.modal}
className='!w-[480px] !max-w-[480px] !px-6 !py-4'
>
<div className='relative pt-4'>
<div className='absolute -top-2 -right-4 flex justify-center items-center w-8 h-8 cursor-pointer' onClick={onCancel}>
<RiCloseLine className='w-4 h-4 text-gray-500' />
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
</div>
<div>
<LogoSite className='mx-auto mb-2' />
<div className='mb-3 text-center text-xs font-normal text-gray-500'>Version {langeniusVersionInfo?.current_version}</div>
<div className='mb-4 text-center text-xs font-normal text-gray-700'>
<div className='mb-3 text-center text-xs font-normal text-text-tertiary'>Version {langeniusVersionInfo?.current_version}</div>
<div className='mb-4 text-center text-xs font-normal text-text-secondary'>
<div>© {dayjs().year()} LangGenius, Inc., Contributors.</div>
<div className='text-[#1C64F2]'>
<div className='text-text-accent'>
{
IS_CE_EDITION
? <Link href={'https://github.com/langgenius/dify/blob/main/LICENSE'} target='_blank' rel='noopener noreferrer'>Open Source License</Link>
@ -52,9 +51,9 @@ export default function AccountAbout({
</div>
</div>
</div>
<div className='mb-4 -mx-8 h-[0.5px] bg-gray-200' />
<div className='mb-4 -mx-8 h-[0.5px] bg-divider-regular' />
<div className='flex justify-between items-center'>
<div className='text-xs font-medium text-gray-800'>
<div className='text-xs font-medium text-text-primary'>
{
isLatest
? t('common.about.latestAvailable', { version: langeniusVersionInfo.latest_version })
@ -62,22 +61,24 @@ export default function AccountAbout({
}
</div>
<div className='flex items-center'>
<Link
className={classNames(buttonClassName, 'mr-2')}
href={'https://github.com/langgenius/dify/releases'}
target='_blank' rel='noopener noreferrer'
>
{t('common.about.changeLog')}
</Link>
<Button className='mr-2'>
<Link
href={'https://github.com/langgenius/dify/releases'}
target='_blank' rel='noopener noreferrer'
>
{t('common.about.changeLog')}
</Link>
</Button>
{
!isLatest && !IS_CE_EDITION && (
<Link
className={classNames(buttonClassName, 'text-primary-600')}
href={langeniusVersionInfo.release_notes}
target='_blank' rel='noopener noreferrer'
>
{t('common.about.updateNow')}
</Link>
<Button variant='primary'>
<Link
href={langeniusVersionInfo.release_notes}
target='_blank' rel='noopener noreferrer'
>
{t('common.about.updateNow')}
</Link>
</Button>
)
}
</div>

View File

@ -106,17 +106,17 @@ export default function AppSelector({ isMobile }: IAppSelector) {
)}
href='/account'
target='_self' rel='noopener noreferrer'>
<RiAccountCircleLine className='size-4 flex-shrink-0 text-text-tertiary' />
<div className='flex-grow system-md-regular text-text-secondary px-1'>{t('common.account.account')}</div>
<RiArrowRightUpLine className='size-[14px] flex-shrink-0 text-text-tertiary' />
<RiAccountCircleLine className='size-4 shrink-0 text-text-tertiary' />
<div className='grow system-md-regular text-text-secondary px-1'>{t('common.account.account')}</div>
<RiArrowRightUpLine className='size-[14px] shrink-0 text-text-tertiary' />
</Link>}
</Menu.Item>
<Menu.Item>
{({ active }) => <div className={classNames(itemClassName,
active && 'bg-state-base-hover',
)} onClick={() => setShowAccountSettingModal({ payload: 'members' })}>
<RiSettings3Line className='size-4 flex-shrink-0 text-text-tertiary' />
<div className='flex-grow system-md-regular text-text-secondary px-1'>{t('common.userProfile.settings')}</div>
<RiSettings3Line className='size-4 shrink-0 text-text-tertiary' />
<div className='grow system-md-regular text-text-secondary px-1'>{t('common.userProfile.settings')}</div>
</div>}
</Menu.Item>
</div>
@ -130,9 +130,9 @@ export default function AppSelector({ isMobile }: IAppSelector) {
locale !== LanguagesSupported[1] ? 'https://docs.dify.ai/' : `https://docs.dify.ai/v/${locale.toLowerCase()}/`
}
target='_blank' rel='noopener noreferrer'>
<RiBookOpenLine className='flex-shrink-0 size-4 text-text-tertiary' />
<div className='flex-grow system-md-regular text-text-secondary px-1'>{t('common.userProfile.helpCenter')}</div>
<RiArrowRightUpLine className='flex-shrink-0 size-[14px] text-text-tertiary' />
<RiBookOpenLine className='shrink-0 size-4 text-text-tertiary' />
<div className='grow system-md-regular text-text-secondary px-1'>{t('common.userProfile.helpCenter')}</div>
<RiArrowRightUpLine className='shrink-0 size-[14px] text-text-tertiary' />
</Link>}
</Menu.Item>
<Support />
@ -146,9 +146,9 @@ export default function AppSelector({ isMobile }: IAppSelector) {
)}
href='https://roadmap.dify.ai'
target='_blank' rel='noopener noreferrer'>
<RiMap2Line className='flex-shrink-0 size-4 text-text-tertiary' />
<div className='flex-grow system-md-regular text-text-secondary px-1'>{t('common.userProfile.roadmap')}</div>
<RiArrowRightUpLine className='flex-shrink-0 size-[14px] text-text-tertiary' />
<RiMap2Line className='shrink-0 size-4 text-text-tertiary' />
<div className='grow system-md-regular text-text-secondary px-1'>{t('common.userProfile.roadmap')}</div>
<RiArrowRightUpLine className='shrink-0 size-[14px] text-text-tertiary' />
</Link>}
</Menu.Item>
{systemFeatures.license.status === LicenseStatus.NONE && <Menu.Item>
@ -156,12 +156,12 @@ export default function AppSelector({ isMobile }: IAppSelector) {
className={classNames(itemClassName, 'group justify-between',
active && 'bg-state-base-hover',
)}
href='https://github.com/langgenius/dify/stargazers'
href='https://github.com/langgenius/dify'
target='_blank' rel='noopener noreferrer'>
<RiGithubLine className='flex-shrink-0 size-4 text-text-tertiary' />
<div className='flex-grow system-md-regular text-text-secondary px-1'>{t('common.userProfile.github')}</div>
<RiGithubLine className='shrink-0 size-4 text-text-tertiary' />
<div className='grow system-md-regular text-text-secondary px-1'>{t('common.userProfile.github')}</div>
<div className='flex items-center gap-0.5 px-[5px] py-[3px] border border-divider-deep rounded-[5px] bg-components-badge-bg-dimm'>
<RiStarLine className='flex-shrink-0 size-3 text-text-tertiary' />
<RiStarLine className='shrink-0 size-3 text-text-tertiary' />
<GithubStar className='system-2xs-medium-uppercase text-text-tertiary' />
</div>
</Link>}
@ -172,9 +172,9 @@ export default function AppSelector({ isMobile }: IAppSelector) {
{({ active }) => <div className={classNames(itemClassName, 'justify-between',
active && 'bg-state-base-hover',
)} onClick={() => setAboutVisible(true)}>
<RiInformation2Line className='flex-shrink-0 size-4 text-text-tertiary' />
<div className='flex-grow system-md-regular text-text-secondary px-1'>{t('common.userProfile.about')}</div>
<div className='flex-shrink-0 flex items-center'>
<RiInformation2Line className='shrink-0 size-4 text-text-tertiary' />
<div className='grow system-md-regular text-text-secondary px-1'>{t('common.userProfile.about')}</div>
<div className='shrink-0 flex items-center'>
<div className='mr-2 system-xs-regular text-text-tertiary'>{langeniusVersionInfo.current_version}</div>
<Indicator color={langeniusVersionInfo.current_version === langeniusVersionInfo.latest_version ? 'green' : 'orange'} />
</div>
@ -190,8 +190,8 @@ export default function AppSelector({ isMobile }: IAppSelector) {
active && 'bg-state-base-hover',
)}
>
<RiLogoutBoxRLine className='flex-shrink-0 size-4 text-text-tertiary' />
<div className='flex-grow system-md-regular text-text-secondary px-1'>{t('common.userProfile.logout')}</div>
<RiLogoutBoxRLine className='shrink-0 size-4 text-text-tertiary' />
<div className='grow system-md-regular text-text-secondary px-1'>{t('common.userProfile.logout')}</div>
</div>
</div>}
</Menu.Item>

View File

@ -7,6 +7,8 @@ import cn from '@/utils/classnames'
import { switchWorkspace } from '@/service/common'
import { useWorkspacesContext } from '@/context/workspace-context'
import { ToastContext } from '@/app/components/base/toast'
import PlanBadge from '../../plan-badge'
import type { Plan } from '@/app/components/billing/type'
const WorkplaceSelector = () => {
const { t } = useTranslation()
@ -69,6 +71,7 @@ const WorkplaceSelector = () => {
<div className='flex py-1 pl-3 pr-2 items-center gap-2 self-stretch hover:bg-state-base-hover rounded-lg' key={workspace.id} onClick={() => handleSwitchWorkspace(workspace.id)}>
<div className='flex items-center justify-center w-6 h-6 bg-[#EFF4FF] rounded-md text-xs font-medium text-primary-600'>{workspace.name[0].toLocaleUpperCase()}</div>
<div className='line-clamp-1 grow overflow-hidden text-text-secondary text-ellipsis system-md-regular cursor-pointer'>{workspace.name}</div>
<PlanBadge plan={workspace.plan as Plan} />
</div>
))
}

View File

@ -1,5 +1,4 @@
import { useProviderContext } from '@/context/provider-context'
import classNames from '@/utils/classnames'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { SparklesSoft } from '../../base/icons/src/public/common'
@ -8,61 +7,52 @@ import { Plan } from '../../billing/type'
type PlanBadgeProps = {
plan: Plan
size?: 's' | 'm'
allowHover?: boolean
sandboxAsUpgrade?: boolean
onClick?: () => void
}
const PlanBadge: FC<PlanBadgeProps> = ({ plan, allowHover, size = 'm', sandboxAsUpgrade = false, onClick }) => {
const PlanBadge: FC<PlanBadgeProps> = ({ plan, allowHover, sandboxAsUpgrade = false, onClick }) => {
const { isFetchedPlan } = useProviderContext()
const { t } = useTranslation()
if (!isFetchedPlan) return null
if (plan === Plan.sandbox && sandboxAsUpgrade) {
return <div className='select-none'>
<PremiumBadge color='blue' allowHover={allowHover} onClick={onClick}>
<SparklesSoft className='flex items-center py-[1px] pl-[3px] w-3.5 h-3.5 text-components-premium-badge-indigo-text-stop-0' />
<div className='system-xs-medium'>
<span className='p-1'>
{t('billing.upgradeBtn.encourageShort')}
</span>
</div>
</PremiumBadge>
</div>
return <PremiumBadge className='select-none' color='blue' allowHover={allowHover} onClick={onClick}>
<SparklesSoft className='flex items-center py-[1px] pl-[3px] w-3.5 h-3.5 text-components-premium-badge-indigo-text-stop-0' />
<div className='system-xs-medium'>
<span className='p-1'>
{t('billing.upgradeBtn.encourageShort')}
</span>
</div>
</PremiumBadge>
}
if (plan === Plan.sandbox) {
return <div className='select-none'>
<PremiumBadge size={size} color='gray' allowHover={allowHover} onClick={onClick}>
<div className={classNames(size === 's' ? 'system-2xs-medium-uppercase' : 'system-xs-medium-uppercase')}>
<span className='p-1'>
{plan}
</span>
</div>
</PremiumBadge>
</div>
return <PremiumBadge className='select-none' size='s' color='gray' allowHover={allowHover} onClick={onClick}>
<div className='system-2xs-medium-uppercase'>
<span className='p-1'>
{plan}
</span>
</div>
</PremiumBadge>
}
if (plan === Plan.professional) {
return <div className='select-none'>
<PremiumBadge size={size} color='blue' allowHover={allowHover} onClick={onClick}>
<div className={classNames(size === 's' ? 'system-2xs-medium-uppercase' : 'system-xs-medium-uppercase')}>
<span className='p-1'>
pro
</span>
</div>
</PremiumBadge>
</div>
return <PremiumBadge className='select-none' size='s' color='blue' allowHover={allowHover} onClick={onClick}>
<div className='system-2xs-medium-uppercase'>
<span className='p-1'>
pro
</span>
</div>
</PremiumBadge>
}
if (plan === Plan.team) {
return <div className='select-none'>
<PremiumBadge size={size} color='indigo' allowHover={allowHover} onClick={onClick}>
<div className={classNames(size === 's' ? 'system-2xs-medium-uppercase' : 'system-xs-medium-uppercase')}>
<span className='p-1'>
{plan}
</span>
</div>
</PremiumBadge>
</div>
return <PremiumBadge className='select-none' size='s' color='indigo' allowHover={allowHover} onClick={onClick}>
<div className='system-2xs-medium-uppercase'>
<span className='p-1'>
{plan}
</span>
</div>
</PremiumBadge>
}
return null
}

View File

@ -109,6 +109,7 @@ const Item: FC<ItemProps> = ({
'relative w-full flex items-center h-6 pl-3 rounded-md cursor-pointer')
}
onClick={handleChosen}
onMouseDown={e => e.preventDefault()}
>
<div className='flex items-center w-0 grow'>
{!isEnv && !isChatVar && <Variable02 className={cn('shrink-0 w-3.5 h-3.5 text-text-accent', isException && 'text-text-warning')} />}

View File

@ -1,6 +1,6 @@
{
"name": "dify-web",
"version": "1.0.0",
"version": "1.0.1",
"private": true,
"engines": {
"node": ">=18.17.0"

View File

@ -5,7 +5,6 @@ import type {
CreateDocumentReq,
DataSet,
DataSetListResponse,
DocumentListResponse,
ErrorDocsResponse,
ExternalAPIDeleteResponse,
ExternalAPIItem,
@ -122,10 +121,6 @@ export const fetchProcessRule: Fetcher<ProcessRuleResponse, { params: { document
return get<ProcessRuleResponse>('/datasets/process-rule', { params: { document_id: documentId } })
}
export const fetchDocuments: Fetcher<DocumentListResponse, { datasetId: string; params: { keyword: string; page: number; limit: number; sort?: SortType } }> = ({ datasetId, params }) => {
return get<DocumentListResponse>(`/datasets/${datasetId}/documents`, { params })
}
export const createFirstDocument: Fetcher<createDocumentResponse, { body: CreateDocumentReq }> = ({ body }) => {
return post<createDocumentResponse>('/datasets/init', { body })
}

View File

@ -4,8 +4,8 @@ import {
} from '@tanstack/react-query'
import { del, get, patch } from '../base'
import { useInvalid } from '../use-base'
import type { MetadataType } from '../datasets'
import type { DocumentDetailResponse, SimpleDocumentDetail, UpdateDocumentBatchParams } from '@/models/datasets'
import type { MetadataType, SortType } from '../datasets'
import type { DocumentDetailResponse, DocumentListResponse, UpdateDocumentBatchParams } from '@/models/datasets'
import { DocumentActionType } from '@/models/datasets'
import type { CommonResponse } from '@/models/common'
@ -18,19 +18,23 @@ export const useDocumentList = (payload: {
keyword: string
page: number
limit: number
}
sort?: SortType
},
refetchInterval?: number | false
}) => {
const { query, datasetId } = payload
return useQuery<{ data: SimpleDocumentDetail[] }>({
queryKey: [...useDocumentListKey, datasetId, query],
queryFn: () => get<{ data: SimpleDocumentDetail[] }>(`/datasets/${datasetId}/documents`, {
const { query, datasetId, refetchInterval } = payload
const { keyword, page, limit, sort } = query
return useQuery<DocumentListResponse>({
queryKey: [...useDocumentListKey, datasetId, keyword, page, limit, sort],
queryFn: () => get<DocumentListResponse>(`/datasets/${datasetId}/documents`, {
params: query,
}),
refetchInterval,
})
}
export const useInvalidDocumentList = () => {
return useInvalid(useDocumentListKey)
export const useInvalidDocumentList = (datasetId?: string) => {
return useInvalid(datasetId ? [...useDocumentListKey, datasetId] : useDocumentListKey)
}
const useAutoDisabledDocumentKey = [NAME_SPACE, 'autoDisabledDocument']