Merge branch 'langgenius:main' into add-vscode-debugger

pull/20668/head
GuanMu 12 months ago committed by GitHub
commit ec73b192ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -491,3 +491,10 @@ OTEL_METRIC_EXPORT_TIMEOUT=30000
# Prevent Clickjacking # Prevent Clickjacking
ALLOW_EMBED=false ALLOW_EMBED=false
# Dataset queue monitor configuration
QUEUE_MONITOR_THRESHOLD=200
# You can configure multiple ones, separated by commas. eg: test1@dify.ai,test2@dify.ai
QUEUE_MONITOR_ALERT_EMAILS=
# Monitor interval in minutes, default is 30 minutes
QUEUE_MONITOR_INTERVAL=30

@ -2,7 +2,7 @@ import os
from typing import Any, Literal, Optional from typing import Any, Literal, Optional
from urllib.parse import parse_qsl, quote_plus from urllib.parse import parse_qsl, quote_plus
from pydantic import Field, NonNegativeInt, PositiveFloat, PositiveInt, computed_field from pydantic import Field, NonNegativeFloat, NonNegativeInt, PositiveFloat, PositiveInt, computed_field
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
from .cache.redis_config import RedisConfig from .cache.redis_config import RedisConfig
@ -256,6 +256,25 @@ class InternalTestConfig(BaseSettings):
) )
class DatasetQueueMonitorConfig(BaseSettings):
"""
Configuration settings for Dataset Queue Monitor
"""
QUEUE_MONITOR_THRESHOLD: Optional[NonNegativeInt] = Field(
description="Threshold for dataset queue monitor",
default=200,
)
QUEUE_MONITOR_ALERT_EMAILS: Optional[str] = Field(
description="Emails for dataset queue monitor alert, separated by commas",
default=None,
)
QUEUE_MONITOR_INTERVAL: Optional[NonNegativeFloat] = Field(
description="Interval for dataset queue monitor in minutes",
default=30,
)
class MiddlewareConfig( class MiddlewareConfig(
# place the configs in alphabet order # place the configs in alphabet order
CeleryConfig, CeleryConfig,
@ -303,5 +322,6 @@ class MiddlewareConfig(
BaiduVectorDBConfig, BaiduVectorDBConfig,
OpenGaussConfig, OpenGaussConfig,
TableStoreConfig, TableStoreConfig,
DatasetQueueMonitorConfig,
): ):
pass pass

@ -369,6 +369,7 @@ class DatasetTagsApi(DatasetApiResource):
) )
parser.add_argument("tag_id", nullable=False, required=True, help="Id of a tag.", type=str) parser.add_argument("tag_id", nullable=False, required=True, help="Id of a tag.", type=str)
args = parser.parse_args() args = parser.parse_args()
args["type"] = "knowledge"
tag = TagService.update_tags(args, args.get("tag_id")) tag = TagService.update_tags(args, args.get("tag_id"))
binding_count = TagService.get_tag_binding_count(args.get("tag_id")) binding_count = TagService.get_tag_binding_count(args.get("tag_id"))

@ -175,8 +175,11 @@ class DocumentAddByFileApi(DatasetApiResource):
if not dataset: if not dataset:
raise ValueError("Dataset does not exist.") raise ValueError("Dataset does not exist.")
if not dataset.indexing_technique and not args.get("indexing_technique"):
indexing_technique = args.get("indexing_technique") or dataset.indexing_technique
if not indexing_technique:
raise ValueError("indexing_technique is required.") raise ValueError("indexing_technique is required.")
args["indexing_technique"] = indexing_technique
# save file info # save file info
file = request.files["file"] file = request.files["file"]
@ -206,12 +209,16 @@ class DocumentAddByFileApi(DatasetApiResource):
knowledge_config = KnowledgeConfig(**args) knowledge_config = KnowledgeConfig(**args)
DocumentService.document_create_args_validate(knowledge_config) DocumentService.document_create_args_validate(knowledge_config)
dataset_process_rule = dataset.latest_process_rule if "process_rule" not in args else None
if not knowledge_config.original_document_id and not dataset_process_rule and not knowledge_config.process_rule:
raise ValueError("process_rule is required.")
try: try:
documents, batch = DocumentService.save_document_with_dataset_id( documents, batch = DocumentService.save_document_with_dataset_id(
dataset=dataset, dataset=dataset,
knowledge_config=knowledge_config, knowledge_config=knowledge_config,
account=dataset.created_by_account, account=dataset.created_by_account,
dataset_process_rule=dataset.latest_process_rule if "process_rule" not in args else None, dataset_process_rule=dataset_process_rule,
created_from="api", created_from="api",
) )
except ProviderTokenNotInitError as ex: except ProviderTokenNotInitError as ex:

@ -55,6 +55,25 @@ class ProviderModelWithStatusEntity(ProviderModel):
status: ModelStatus status: ModelStatus
load_balancing_enabled: bool = False load_balancing_enabled: bool = False
def raise_for_status(self) -> None:
"""
Check model status and raise ValueError if not active.
:raises ValueError: When model status is not active, with a descriptive message
"""
if self.status == ModelStatus.ACTIVE:
return
error_messages = {
ModelStatus.NO_CONFIGURE: "Model is not configured",
ModelStatus.QUOTA_EXCEEDED: "Model quota has been exceeded",
ModelStatus.NO_PERMISSION: "No permission to use this model",
ModelStatus.DISABLED: "Model is disabled",
}
if self.status in error_messages:
raise ValueError(error_messages[self.status])
class ModelWithProviderEntity(ProviderModelWithStatusEntity): class ModelWithProviderEntity(ProviderModelWithStatusEntity):
""" """

@ -41,45 +41,53 @@ class Extensible:
extensions = [] extensions = []
position_map: dict[str, int] = {} position_map: dict[str, int] = {}
# get the path of the current class # Get the package name from the module path
current_path = os.path.abspath(cls.__module__.replace(".", os.path.sep) + ".py") package_name = ".".join(cls.__module__.split(".")[:-1])
current_dir_path = os.path.dirname(current_path)
try:
# traverse subdirectories # Get package directory path
for subdir_name in os.listdir(current_dir_path): package_spec = importlib.util.find_spec(package_name)
if subdir_name.startswith("__"): if not package_spec or not package_spec.origin:
continue raise ImportError(f"Could not find package {package_name}")
subdir_path = os.path.join(current_dir_path, subdir_name) package_dir = os.path.dirname(package_spec.origin)
extension_name = subdir_name
if os.path.isdir(subdir_path): # Traverse subdirectories
for subdir_name in os.listdir(package_dir):
if subdir_name.startswith("__"):
continue
subdir_path = os.path.join(package_dir, subdir_name)
if not os.path.isdir(subdir_path):
continue
extension_name = subdir_name
file_names = os.listdir(subdir_path) file_names = os.listdir(subdir_path)
# is builtin extension, builtin extension # Check for extension module file
# in the front-end page and business logic, there are special treatments. if (extension_name + ".py") not in file_names:
logging.warning(f"Missing {extension_name}.py file in {subdir_path}, Skip.")
continue
# Check for builtin flag and position
builtin = False builtin = False
# default position is 0 can not be None for sort_to_dict_by_position_map
position = 0 position = 0
if "__builtin__" in file_names: if "__builtin__" in file_names:
builtin = True builtin = True
builtin_file_path = os.path.join(subdir_path, "__builtin__") builtin_file_path = os.path.join(subdir_path, "__builtin__")
if os.path.exists(builtin_file_path): if os.path.exists(builtin_file_path):
position = int(Path(builtin_file_path).read_text(encoding="utf-8").strip()) position = int(Path(builtin_file_path).read_text(encoding="utf-8").strip())
position_map[extension_name] = position position_map[extension_name] = position
if (extension_name + ".py") not in file_names: # Import the extension module
logging.warning(f"Missing {extension_name}.py file in {subdir_path}, Skip.") module_name = f"{package_name}.{extension_name}.{extension_name}"
continue spec = importlib.util.find_spec(module_name)
# Dynamic loading {subdir_name}.py file and find the subclass of Extensible
py_path = os.path.join(subdir_path, extension_name + ".py")
spec = importlib.util.spec_from_file_location(extension_name, py_path)
if not spec or not spec.loader: if not spec or not spec.loader:
raise Exception(f"Failed to load module {extension_name} from {py_path}") raise ImportError(f"Failed to load module {module_name}")
mod = importlib.util.module_from_spec(spec) mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod) spec.loader.exec_module(mod)
# Find extension class
extension_class = None extension_class = None
for name, obj in vars(mod).items(): for name, obj in vars(mod).items():
if isinstance(obj, type) and issubclass(obj, cls) and obj != cls: if isinstance(obj, type) and issubclass(obj, cls) and obj != cls:
@ -87,21 +95,21 @@ class Extensible:
break break
if not extension_class: if not extension_class:
logging.warning(f"Missing subclass of {cls.__name__} in {py_path}, Skip.") logging.warning(f"Missing subclass of {cls.__name__} in {module_name}, Skip.")
continue continue
# Load schema if not builtin
json_data: dict[str, Any] = {} json_data: dict[str, Any] = {}
if not builtin: if not builtin:
if "schema.json" not in file_names: json_path = os.path.join(subdir_path, "schema.json")
if not os.path.exists(json_path):
logging.warning(f"Missing schema.json file in {subdir_path}, Skip.") logging.warning(f"Missing schema.json file in {subdir_path}, Skip.")
continue continue
json_path = os.path.join(subdir_path, "schema.json") with open(json_path, encoding="utf-8") as f:
json_data = {} json_data = json.load(f)
if os.path.exists(json_path):
with open(json_path, encoding="utf-8") as f:
json_data = json.load(f)
# Create extension
extensions.append( extensions.append(
ModuleExtension( ModuleExtension(
extension_class=extension_class, extension_class=extension_class,
@ -113,6 +121,11 @@ class Extensible:
) )
) )
except Exception as e:
logging.exception("Error scanning extensions")
raise
# Sort extensions by position
sorted_extensions = sort_to_dict_by_position_map( sorted_extensions = sort_to_dict_by_position_map(
position_map=position_map, data=extensions, name_func=lambda x: x.name position_map=position_map, data=extensions, name_func=lambda x: x.name
) )

@ -160,6 +160,10 @@ class ProviderModel(BaseModel):
deprecated: bool = False deprecated: bool = False
model_config = ConfigDict(protected_namespaces=()) model_config = ConfigDict(protected_namespaces=())
@property
def support_structure_output(self) -> bool:
return self.features is not None and ModelFeature.STRUCTURED_OUTPUT in self.features
class ParameterRule(BaseModel): class ParameterRule(BaseModel):
""" """

@ -3,7 +3,9 @@ from collections import defaultdict
from json import JSONDecodeError from json import JSONDecodeError
from typing import Any, Optional, cast from typing import Any, Optional, cast
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from configs import dify_config from configs import dify_config
from core.entities.model_entities import DefaultModelEntity, DefaultModelProviderEntity from core.entities.model_entities import DefaultModelEntity, DefaultModelProviderEntity
@ -393,19 +395,13 @@ class ProviderManager:
@staticmethod @staticmethod
def _get_all_providers(tenant_id: str) -> dict[str, list[Provider]]: def _get_all_providers(tenant_id: str) -> dict[str, list[Provider]]:
"""
Get all provider records of the workspace.
:param tenant_id: workspace id
:return:
"""
providers = db.session.query(Provider).filter(Provider.tenant_id == tenant_id, Provider.is_valid == True).all()
provider_name_to_provider_records_dict = defaultdict(list) provider_name_to_provider_records_dict = defaultdict(list)
for provider in providers: with Session(db.engine, expire_on_commit=False) as session:
# TODO: Use provider name with prefix after the data migration stmt = select(Provider).where(Provider.tenant_id == tenant_id, Provider.is_valid == True)
provider_name_to_provider_records_dict[str(ModelProviderID(provider.provider_name))].append(provider) providers = session.scalars(stmt)
for provider in providers:
# Use provider name with prefix after the data migration
provider_name_to_provider_records_dict[str(ModelProviderID(provider.provider_name))].append(provider)
return provider_name_to_provider_records_dict return provider_name_to_provider_records_dict
@staticmethod @staticmethod
@ -416,17 +412,12 @@ class ProviderManager:
:param tenant_id: workspace id :param tenant_id: workspace id
:return: :return:
""" """
# Get all provider model records of the workspace
provider_models = (
db.session.query(ProviderModel)
.filter(ProviderModel.tenant_id == tenant_id, ProviderModel.is_valid == True)
.all()
)
provider_name_to_provider_model_records_dict = defaultdict(list) provider_name_to_provider_model_records_dict = defaultdict(list)
for provider_model in provider_models: with Session(db.engine, expire_on_commit=False) as session:
provider_name_to_provider_model_records_dict[provider_model.provider_name].append(provider_model) stmt = select(ProviderModel).where(ProviderModel.tenant_id == tenant_id, ProviderModel.is_valid == True)
provider_models = session.scalars(stmt)
for provider_model in provider_models:
provider_name_to_provider_model_records_dict[provider_model.provider_name].append(provider_model)
return provider_name_to_provider_model_records_dict return provider_name_to_provider_model_records_dict
@staticmethod @staticmethod
@ -437,17 +428,14 @@ class ProviderManager:
:param tenant_id: workspace id :param tenant_id: workspace id
:return: :return:
""" """
preferred_provider_types = ( provider_name_to_preferred_provider_type_records_dict = {}
db.session.query(TenantPreferredModelProvider) with Session(db.engine, expire_on_commit=False) as session:
.filter(TenantPreferredModelProvider.tenant_id == tenant_id) stmt = select(TenantPreferredModelProvider).where(TenantPreferredModelProvider.tenant_id == tenant_id)
.all() preferred_provider_types = session.scalars(stmt)
) provider_name_to_preferred_provider_type_records_dict = {
preferred_provider_type.provider_name: preferred_provider_type
provider_name_to_preferred_provider_type_records_dict = { for preferred_provider_type in preferred_provider_types
preferred_provider_type.provider_name: preferred_provider_type }
for preferred_provider_type in preferred_provider_types
}
return provider_name_to_preferred_provider_type_records_dict return provider_name_to_preferred_provider_type_records_dict
@staticmethod @staticmethod
@ -458,18 +446,14 @@ class ProviderManager:
:param tenant_id: workspace id :param tenant_id: workspace id
:return: :return:
""" """
provider_model_settings = (
db.session.query(ProviderModelSetting).filter(ProviderModelSetting.tenant_id == tenant_id).all()
)
provider_name_to_provider_model_settings_dict = defaultdict(list) provider_name_to_provider_model_settings_dict = defaultdict(list)
for provider_model_setting in provider_model_settings: with Session(db.engine, expire_on_commit=False) as session:
( stmt = select(ProviderModelSetting).where(ProviderModelSetting.tenant_id == tenant_id)
provider_model_settings = session.scalars(stmt)
for provider_model_setting in provider_model_settings:
provider_name_to_provider_model_settings_dict[provider_model_setting.provider_name].append( provider_name_to_provider_model_settings_dict[provider_model_setting.provider_name].append(
provider_model_setting provider_model_setting
) )
)
return provider_name_to_provider_model_settings_dict return provider_name_to_provider_model_settings_dict
@staticmethod @staticmethod
@ -492,15 +476,14 @@ class ProviderManager:
if not model_load_balancing_enabled: if not model_load_balancing_enabled:
return {} return {}
provider_load_balancing_configs = (
db.session.query(LoadBalancingModelConfig).filter(LoadBalancingModelConfig.tenant_id == tenant_id).all()
)
provider_name_to_provider_load_balancing_model_configs_dict = defaultdict(list) provider_name_to_provider_load_balancing_model_configs_dict = defaultdict(list)
for provider_load_balancing_config in provider_load_balancing_configs: with Session(db.engine, expire_on_commit=False) as session:
provider_name_to_provider_load_balancing_model_configs_dict[ stmt = select(LoadBalancingModelConfig).where(LoadBalancingModelConfig.tenant_id == tenant_id)
provider_load_balancing_config.provider_name provider_load_balancing_configs = session.scalars(stmt)
].append(provider_load_balancing_config) for provider_load_balancing_config in provider_load_balancing_configs:
provider_name_to_provider_load_balancing_model_configs_dict[
provider_load_balancing_config.provider_name
].append(provider_load_balancing_config)
return provider_name_to_provider_load_balancing_model_configs_dict return provider_name_to_provider_load_balancing_model_configs_dict
@ -626,10 +609,9 @@ class ProviderManager:
if not cached_provider_credentials: if not cached_provider_credentials:
try: try:
# fix origin data # fix origin data
if ( if custom_provider_record.encrypted_config is None:
custom_provider_record.encrypted_config raise ValueError("No credentials found")
and not custom_provider_record.encrypted_config.startswith("{") if not custom_provider_record.encrypted_config.startswith("{"):
):
provider_credentials = {"openai_api_key": custom_provider_record.encrypted_config} provider_credentials = {"openai_api_key": custom_provider_record.encrypted_config}
else: else:
provider_credentials = json.loads(custom_provider_record.encrypted_config) provider_credentials = json.loads(custom_provider_record.encrypted_config)
@ -733,7 +715,7 @@ class ProviderManager:
return SystemConfiguration(enabled=False) return SystemConfiguration(enabled=False)
# Convert provider_records to dict # Convert provider_records to dict
quota_type_to_provider_records_dict = {} quota_type_to_provider_records_dict: dict[ProviderQuotaType, Provider] = {}
for provider_record in provider_records: for provider_record in provider_records:
if provider_record.provider_type != ProviderType.SYSTEM.value: if provider_record.provider_type != ProviderType.SYSTEM.value:
continue continue
@ -758,6 +740,11 @@ class ProviderManager:
else: else:
provider_record = quota_type_to_provider_records_dict[provider_quota.quota_type] provider_record = quota_type_to_provider_records_dict[provider_quota.quota_type]
if provider_record.quota_used is None:
raise ValueError("quota_used is None")
if provider_record.quota_limit is None:
raise ValueError("quota_limit is None")
quota_configuration = QuotaConfiguration( quota_configuration = QuotaConfiguration(
quota_type=provider_quota.quota_type, quota_type=provider_quota.quota_type,
quota_unit=provider_hosting_configuration.quota_unit or QuotaUnit.TOKENS, quota_unit=provider_hosting_configuration.quota_unit or QuotaUnit.TOKENS,
@ -791,10 +778,9 @@ class ProviderManager:
cached_provider_credentials = provider_credentials_cache.get() cached_provider_credentials = provider_credentials_cache.get()
if not cached_provider_credentials: if not cached_provider_credentials:
try: provider_credentials: dict[str, Any] = {}
provider_credentials: dict[str, Any] = json.loads(provider_record.encrypted_config) if provider_records and provider_records[0].encrypted_config:
except JSONDecodeError: provider_credentials = json.loads(provider_records[0].encrypted_config)
provider_credentials = {}
# Get provider credential secret variables # Get provider credential secret variables
provider_credential_secret_variables = self._extract_secret_variables( provider_credential_secret_variables = self._extract_secret_variables(

@ -720,7 +720,7 @@ STOPWORDS = {
"", "",
"", "",
"", "",
" ", " ",
"0", "0",
"1", "1",
"2", "2",
@ -731,16 +731,6 @@ STOPWORDS = {
"7", "7",
"8", "8",
"9", "9",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"", "",
"", "",
"", "",

@ -261,7 +261,7 @@ class OracleVector(BaseVector):
words = pseg.cut(query) words = pseg.cut(query)
current_entity = "" current_entity = ""
for word, pos in words: for word, pos in words:
if pos in {"nr", "Ng", "eng", "nz", "n", "ORG", "v"}: # nr: 人名, ns: 地名, nt: 机构名 if pos in {"nr", "Ng", "eng", "nz", "n", "ORG", "v"}: # nr: 人名ns: 地名,nt: 机构名
current_entity += word current_entity += word
else: else:
if current_entity: if current_entity:

@ -66,7 +66,8 @@ class LLMNodeData(BaseNodeData):
context: ContextConfig context: ContextConfig
vision: VisionConfig = Field(default_factory=VisionConfig) vision: VisionConfig = Field(default_factory=VisionConfig)
structured_output: dict | None = None structured_output: dict | None = None
structured_output_enabled: bool = False # We used 'structured_output_enabled' in the past, but it's not a good name.
structured_output_switch_on: bool = Field(False, alias="structured_output_enabled")
@field_validator("prompt_config", mode="before") @field_validator("prompt_config", mode="before")
@classmethod @classmethod
@ -74,3 +75,7 @@ class LLMNodeData(BaseNodeData):
if v is None: if v is None:
return PromptConfig() return PromptConfig()
return v return v
@property
def structured_output_enabled(self) -> bool:
return self.structured_output_switch_on and self.structured_output is not None

@ -12,9 +12,7 @@ from sqlalchemy.orm import Session
from configs import dify_config from configs import dify_config
from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity
from core.entities.model_entities import ModelStatus
from core.entities.provider_entities import QuotaUnit from core.entities.provider_entities import QuotaUnit
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
from core.file import FileType, file_manager from core.file import FileType, file_manager
from core.helper.code_executor import CodeExecutor, CodeLanguage from core.helper.code_executor import CodeExecutor, CodeLanguage
from core.memory.token_buffer_memory import TokenBufferMemory from core.memory.token_buffer_memory import TokenBufferMemory
@ -74,7 +72,6 @@ from core.workflow.nodes.event import (
from core.workflow.utils.structured_output.entities import ( from core.workflow.utils.structured_output.entities import (
ResponseFormat, ResponseFormat,
SpecialModelType, SpecialModelType,
SupportStructuredOutputStatus,
) )
from core.workflow.utils.structured_output.prompt import STRUCTURED_OUTPUT_PROMPT from core.workflow.utils.structured_output.prompt import STRUCTURED_OUTPUT_PROMPT
from core.workflow.utils.variable_template_parser import VariableTemplateParser from core.workflow.utils.variable_template_parser import VariableTemplateParser
@ -277,7 +274,7 @@ class LLMNode(BaseNode[LLMNodeData]):
llm_usage=usage, llm_usage=usage,
) )
) )
except LLMNodeError as e: except ValueError as e:
yield RunCompletedEvent( yield RunCompletedEvent(
run_result=NodeRunResult( run_result=NodeRunResult(
status=WorkflowNodeExecutionStatus.FAILED, status=WorkflowNodeExecutionStatus.FAILED,
@ -527,65 +524,53 @@ class LLMNode(BaseNode[LLMNodeData]):
def _fetch_model_config( def _fetch_model_config(
self, node_data_model: ModelConfig self, node_data_model: ModelConfig
) -> tuple[ModelInstance, ModelConfigWithCredentialsEntity]: ) -> tuple[ModelInstance, ModelConfigWithCredentialsEntity]:
model_name = node_data_model.name if not node_data_model.mode:
provider_name = node_data_model.provider raise LLMModeRequiredError("LLM mode is required.")
model_manager = ModelManager() model = ModelManager().get_model_instance(
model_instance = model_manager.get_model_instance( tenant_id=self.tenant_id,
tenant_id=self.tenant_id, model_type=ModelType.LLM, provider=provider_name, model=model_name model_type=ModelType.LLM,
provider=node_data_model.provider,
model=node_data_model.name,
) )
provider_model_bundle = model_instance.provider_model_bundle model.model_type_instance = cast(LargeLanguageModel, model.model_type_instance)
model_type_instance = model_instance.model_type_instance
model_type_instance = cast(LargeLanguageModel, model_type_instance)
model_credentials = model_instance.credentials
# check model # check model
provider_model = provider_model_bundle.configuration.get_provider_model( provider_model = model.provider_model_bundle.configuration.get_provider_model(
model=model_name, model_type=ModelType.LLM model=node_data_model.name, model_type=ModelType.LLM
) )
if provider_model is None: if provider_model is None:
raise ModelNotExistError(f"Model {model_name} not exist.") raise ModelNotExistError(f"Model {node_data_model.name} not exist.")
provider_model.raise_for_status()
if provider_model.status == ModelStatus.NO_CONFIGURE:
raise ProviderTokenNotInitError(f"Model {model_name} credentials is not initialized.")
elif provider_model.status == ModelStatus.NO_PERMISSION:
raise ModelCurrentlyNotSupportError(f"Dify Hosted OpenAI {model_name} currently not support.")
elif provider_model.status == ModelStatus.QUOTA_EXCEEDED:
raise QuotaExceededError(f"Model provider {provider_name} quota exceeded.")
# model config # model config
completion_params = node_data_model.completion_params stop: list[str] = []
stop = [] if "stop" in node_data_model.completion_params:
if "stop" in completion_params: stop = node_data_model.completion_params.pop("stop")
stop = completion_params["stop"]
del completion_params["stop"]
# get model mode
model_mode = node_data_model.mode
if not model_mode:
raise LLMModeRequiredError("LLM mode is required.")
model_schema = model_type_instance.get_model_schema(model_name, model_credentials)
model_schema = model.model_type_instance.get_model_schema(node_data_model.name, model.credentials)
if not model_schema: if not model_schema:
raise ModelNotExistError(f"Model {model_name} not exist.") raise ModelNotExistError(f"Model {node_data_model.name} not exist.")
support_structured_output = self._check_model_structured_output_support()
if support_structured_output == SupportStructuredOutputStatus.SUPPORTED: if self.node_data.structured_output_enabled:
completion_params = self._handle_native_json_schema(completion_params, model_schema.parameter_rules) if model_schema.support_structure_output:
elif support_structured_output == SupportStructuredOutputStatus.UNSUPPORTED: node_data_model.completion_params = self._handle_native_json_schema(
# Set appropriate response format based on model capabilities node_data_model.completion_params, model_schema.parameter_rules
self._set_response_format(completion_params, model_schema.parameter_rules) )
return model_instance, ModelConfigWithCredentialsEntity( else:
provider=provider_name, # Set appropriate response format based on model capabilities
model=model_name, self._set_response_format(node_data_model.completion_params, model_schema.parameter_rules)
return model, ModelConfigWithCredentialsEntity(
provider=node_data_model.provider,
model=node_data_model.name,
model_schema=model_schema, model_schema=model_schema,
mode=model_mode, mode=node_data_model.mode,
provider_model_bundle=provider_model_bundle, provider_model_bundle=model.provider_model_bundle,
credentials=model_credentials, credentials=model.credentials,
parameters=completion_params, parameters=node_data_model.completion_params,
stop=stop, stop=stop,
) )
@ -786,13 +771,25 @@ class LLMNode(BaseNode[LLMNodeData]):
"No prompt found in the LLM configuration. " "No prompt found in the LLM configuration. "
"Please ensure a prompt is properly configured before proceeding." "Please ensure a prompt is properly configured before proceeding."
) )
support_structured_output = self._check_model_structured_output_support()
if support_structured_output == SupportStructuredOutputStatus.UNSUPPORTED: model = ModelManager().get_model_instance(
filtered_prompt_messages = self._handle_prompt_based_schema( tenant_id=self.tenant_id,
prompt_messages=filtered_prompt_messages, model_type=ModelType.LLM,
) provider=self.node_data.model.provider,
stop = model_config.stop model=self.node_data.model.name,
return filtered_prompt_messages, stop )
model_schema = model.model_type_instance.get_model_schema(
model=self.node_data.model.name,
credentials=model.credentials,
)
if not model_schema:
raise ModelNotExistError(f"Model {self.node_data.model.name} not exist.")
if self.node_data.structured_output_enabled:
if not model_schema.support_structure_output:
filtered_prompt_messages = self._handle_prompt_based_schema(
prompt_messages=filtered_prompt_messages,
)
return filtered_prompt_messages, model_config.stop
def _parse_structured_output(self, result_text: str) -> dict[str, Any]: def _parse_structured_output(self, result_text: str) -> dict[str, Any]:
structured_output: dict[str, Any] = {} structured_output: dict[str, Any] = {}
@ -903,7 +900,7 @@ class LLMNode(BaseNode[LLMNodeData]):
variable_mapping["#context#"] = node_data.context.variable_selector variable_mapping["#context#"] = node_data.context.variable_selector
if node_data.vision.enabled: if node_data.vision.enabled:
variable_mapping["#files#"] = ["sys", SystemVariableKey.FILES.value] variable_mapping["#files#"] = node_data.vision.configs.variable_selector
if node_data.memory: if node_data.memory:
variable_mapping["#sys.query#"] = ["sys", SystemVariableKey.QUERY.value] variable_mapping["#sys.query#"] = ["sys", SystemVariableKey.QUERY.value]
@ -1185,32 +1182,6 @@ class LLMNode(BaseNode[LLMNodeData]):
except json.JSONDecodeError: except json.JSONDecodeError:
raise LLMNodeError("structured_output_schema is not valid JSON format") raise LLMNodeError("structured_output_schema is not valid JSON format")
def _check_model_structured_output_support(self) -> SupportStructuredOutputStatus:
"""
Check if the current model supports structured output.
Returns:
SupportStructuredOutput: The support status of structured output
"""
# Early return if structured output is disabled
if (
not isinstance(self.node_data, LLMNodeData)
or not self.node_data.structured_output_enabled
or not self.node_data.structured_output
):
return SupportStructuredOutputStatus.DISABLED
# Get model schema and check if it exists
model_schema = self._fetch_model_schema(self.node_data.model.provider)
if not model_schema:
return SupportStructuredOutputStatus.DISABLED
# Check if model supports structured output feature
return (
SupportStructuredOutputStatus.SUPPORTED
if bool(model_schema.features and ModelFeature.STRUCTURED_OUTPUT in model_schema.features)
else SupportStructuredOutputStatus.UNSUPPORTED
)
def _save_multimodal_output_and_convert_result_to_markdown( def _save_multimodal_output_and_convert_result_to_markdown(
self, self,
contents: str | list[PromptMessageContentUnionTypes] | None, contents: str | list[PromptMessageContentUnionTypes] | None,

@ -14,11 +14,3 @@ class SpecialModelType(StrEnum):
GEMINI = "gemini" GEMINI = "gemini"
OLLAMA = "ollama" OLLAMA = "ollama"
class SupportStructuredOutputStatus(StrEnum):
"""Constants for structured output support status"""
SUPPORTED = "supported"
UNSUPPORTED = "unsupported"
DISABLED = "disabled"

@ -70,6 +70,7 @@ def init_app(app: DifyApp) -> Celery:
"schedule.update_tidb_serverless_status_task", "schedule.update_tidb_serverless_status_task",
"schedule.clean_messages", "schedule.clean_messages",
"schedule.mail_clean_document_notify_task", "schedule.mail_clean_document_notify_task",
"schedule.queue_monitor_task",
] ]
day = dify_config.CELERY_BEAT_SCHEDULER_TIME day = dify_config.CELERY_BEAT_SCHEDULER_TIME
beat_schedule = { beat_schedule = {
@ -98,6 +99,12 @@ def init_app(app: DifyApp) -> Celery:
"task": "schedule.mail_clean_document_notify_task.mail_clean_document_notify_task", "task": "schedule.mail_clean_document_notify_task.mail_clean_document_notify_task",
"schedule": crontab(minute="0", hour="10", day_of_week="1"), "schedule": crontab(minute="0", hour="10", day_of_week="1"),
}, },
"datasets-queue-monitor": {
"task": "schedule.queue_monitor_task.queue_monitor_task",
"schedule": timedelta(
minutes=dify_config.QUEUE_MONITOR_INTERVAL if dify_config.QUEUE_MONITOR_INTERVAL else 30
),
},
} }
celery_app.conf.update(beat_schedule=beat_schedule, imports=imports) celery_app.conf.update(beat_schedule=beat_schedule, imports=imports)

@ -18,6 +18,7 @@ from flask_restful import fields
from configs import dify_config from configs import dify_config
from core.app.features.rate_limiting.rate_limit import RateLimitGenerator from core.app.features.rate_limiting.rate_limit import RateLimitGenerator
from core.file import helpers as file_helpers from core.file import helpers as file_helpers
from core.model_runtime.utils.encoders import jsonable_encoder
from extensions.ext_redis import redis_client from extensions.ext_redis import redis_client
if TYPE_CHECKING: if TYPE_CHECKING:
@ -196,7 +197,7 @@ def generate_text_hash(text: str) -> str:
def compact_generate_response(response: Union[Mapping, Generator, RateLimitGenerator]) -> Response: def compact_generate_response(response: Union[Mapping, Generator, RateLimitGenerator]) -> Response:
if isinstance(response, dict): if isinstance(response, dict):
return Response(response=json.dumps(response), status=200, mimetype="application/json") return Response(response=json.dumps(jsonable_encoder(response)), status=200, mimetype="application/json")
else: else:
def generate() -> Generator: def generate() -> Generator:

@ -1,6 +1,9 @@
from datetime import datetime
from enum import Enum from enum import Enum
from typing import Optional
from sqlalchemy import func from sqlalchemy import func, text
from sqlalchemy.orm import Mapped, mapped_column
from .base import Base from .base import Base
from .engine import db from .engine import db
@ -51,20 +54,24 @@ class Provider(Base):
), ),
) )
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()"))
tenant_id = db.Column(StringUUID, nullable=False) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
provider_name = db.Column(db.String(255), nullable=False) provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
provider_type = db.Column(db.String(40), nullable=False, server_default=db.text("'custom'::character varying")) provider_type: Mapped[str] = mapped_column(
encrypted_config = db.Column(db.Text, nullable=True) db.String(40), nullable=False, server_default=text("'custom'::character varying")
is_valid = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) )
last_used = db.Column(db.DateTime, nullable=True) encrypted_config: Mapped[Optional[str]] = mapped_column(db.Text, nullable=True)
is_valid: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=text("false"))
last_used: Mapped[Optional[datetime]] = mapped_column(db.DateTime, nullable=True)
quota_type = db.Column(db.String(40), nullable=True, server_default=db.text("''::character varying")) quota_type: Mapped[Optional[str]] = mapped_column(
quota_limit = db.Column(db.BigInteger, nullable=True) db.String(40), nullable=True, server_default=text("''::character varying")
quota_used = db.Column(db.BigInteger, default=0) )
quota_limit: Mapped[Optional[int]] = mapped_column(db.BigInteger, nullable=True)
quota_used: Mapped[Optional[int]] = mapped_column(db.BigInteger, default=0)
created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
def __repr__(self): def __repr__(self):
return ( return (
@ -104,15 +111,15 @@ class ProviderModel(Base):
), ),
) )
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()"))
tenant_id = db.Column(StringUUID, nullable=False) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
provider_name = db.Column(db.String(255), nullable=False) provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
model_name = db.Column(db.String(255), nullable=False) model_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
model_type = db.Column(db.String(40), nullable=False) model_type: Mapped[str] = mapped_column(db.String(40), nullable=False)
encrypted_config = db.Column(db.Text, nullable=True) encrypted_config: Mapped[Optional[str]] = mapped_column(db.Text, nullable=True)
is_valid = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) is_valid: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=text("false"))
created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
class TenantDefaultModel(Base): class TenantDefaultModel(Base):
@ -122,13 +129,13 @@ class TenantDefaultModel(Base):
db.Index("tenant_default_model_tenant_id_provider_type_idx", "tenant_id", "provider_name", "model_type"), db.Index("tenant_default_model_tenant_id_provider_type_idx", "tenant_id", "provider_name", "model_type"),
) )
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()"))
tenant_id = db.Column(StringUUID, nullable=False) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
provider_name = db.Column(db.String(255), nullable=False) provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
model_name = db.Column(db.String(255), nullable=False) model_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
model_type = db.Column(db.String(40), nullable=False) model_type: Mapped[str] = mapped_column(db.String(40), nullable=False)
created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
class TenantPreferredModelProvider(Base): class TenantPreferredModelProvider(Base):
@ -138,12 +145,12 @@ class TenantPreferredModelProvider(Base):
db.Index("tenant_preferred_model_provider_tenant_provider_idx", "tenant_id", "provider_name"), db.Index("tenant_preferred_model_provider_tenant_provider_idx", "tenant_id", "provider_name"),
) )
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()"))
tenant_id = db.Column(StringUUID, nullable=False) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
provider_name = db.Column(db.String(255), nullable=False) provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
preferred_provider_type = db.Column(db.String(40), nullable=False) preferred_provider_type: Mapped[str] = mapped_column(db.String(40), nullable=False)
created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
class ProviderOrder(Base): class ProviderOrder(Base):
@ -153,22 +160,24 @@ class ProviderOrder(Base):
db.Index("provider_order_tenant_provider_idx", "tenant_id", "provider_name"), db.Index("provider_order_tenant_provider_idx", "tenant_id", "provider_name"),
) )
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()"))
tenant_id = db.Column(StringUUID, nullable=False) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
provider_name = db.Column(db.String(255), nullable=False) provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
account_id = db.Column(StringUUID, nullable=False) account_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
payment_product_id = db.Column(db.String(191), nullable=False) payment_product_id: Mapped[str] = mapped_column(db.String(191), nullable=False)
payment_id = db.Column(db.String(191)) payment_id: Mapped[Optional[str]] = mapped_column(db.String(191))
transaction_id = db.Column(db.String(191)) transaction_id: Mapped[Optional[str]] = mapped_column(db.String(191))
quantity = db.Column(db.Integer, nullable=False, server_default=db.text("1")) quantity: Mapped[int] = mapped_column(db.Integer, nullable=False, server_default=text("1"))
currency = db.Column(db.String(40)) currency: Mapped[Optional[str]] = mapped_column(db.String(40))
total_amount = db.Column(db.Integer) total_amount: Mapped[Optional[int]] = mapped_column(db.Integer)
payment_status = db.Column(db.String(40), nullable=False, server_default=db.text("'wait_pay'::character varying")) payment_status: Mapped[str] = mapped_column(
paid_at = db.Column(db.DateTime) db.String(40), nullable=False, server_default=text("'wait_pay'::character varying")
pay_failed_at = db.Column(db.DateTime) )
refunded_at = db.Column(db.DateTime) paid_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime)
created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) pay_failed_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime)
updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) refunded_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime)
created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
class ProviderModelSetting(Base): class ProviderModelSetting(Base):
@ -182,15 +191,15 @@ class ProviderModelSetting(Base):
db.Index("provider_model_setting_tenant_provider_model_idx", "tenant_id", "provider_name", "model_type"), db.Index("provider_model_setting_tenant_provider_model_idx", "tenant_id", "provider_name", "model_type"),
) )
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()"))
tenant_id = db.Column(StringUUID, nullable=False) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
provider_name = db.Column(db.String(255), nullable=False) provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
model_name = db.Column(db.String(255), nullable=False) model_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
model_type = db.Column(db.String(40), nullable=False) model_type: Mapped[str] = mapped_column(db.String(40), nullable=False)
enabled = db.Column(db.Boolean, nullable=False, server_default=db.text("true")) enabled: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=text("true"))
load_balancing_enabled = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) load_balancing_enabled: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=text("false"))
created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
class LoadBalancingModelConfig(Base): class LoadBalancingModelConfig(Base):
@ -204,13 +213,13 @@ class LoadBalancingModelConfig(Base):
db.Index("load_balancing_model_config_tenant_provider_model_idx", "tenant_id", "provider_name", "model_type"), db.Index("load_balancing_model_config_tenant_provider_model_idx", "tenant_id", "provider_name", "model_type"),
) )
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()"))
tenant_id = db.Column(StringUUID, nullable=False) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
provider_name = db.Column(db.String(255), nullable=False) provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
model_name = db.Column(db.String(255), nullable=False) model_name: Mapped[str] = mapped_column(db.String(255), nullable=False)
model_type = db.Column(db.String(40), nullable=False) model_type: Mapped[str] = mapped_column(db.String(40), nullable=False)
name = db.Column(db.String(255), nullable=False) name: Mapped[str] = mapped_column(db.String(255), nullable=False)
encrypted_config = db.Column(db.Text, nullable=True) encrypted_config: Mapped[Optional[str]] = mapped_column(db.Text, nullable=True)
enabled = db.Column(db.Boolean, nullable=False, server_default=db.text("true")) enabled: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=text("true"))
created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())

@ -0,0 +1,62 @@
import logging
from datetime import datetime
from urllib.parse import urlparse
import click
from flask import render_template
from redis import Redis
import app
from configs import dify_config
from extensions.ext_database import db
from extensions.ext_mail import mail
# Create a dedicated Redis connection (using the same configuration as Celery)
celery_broker_url = dify_config.CELERY_BROKER_URL
parsed = urlparse(celery_broker_url)
host = parsed.hostname or "localhost"
port = parsed.port or 6379
password = parsed.password or None
redis_db = parsed.path.strip("/") or "1" # type: ignore
celery_redis = Redis(host=host, port=port, password=password, db=redis_db)
@app.celery.task(queue="monitor")
def queue_monitor_task():
queue_name = "dataset"
threshold = dify_config.QUEUE_MONITOR_THRESHOLD
try:
queue_length = celery_redis.llen(f"{queue_name}")
logging.info(click.style(f"Start monitor {queue_name}", fg="green"))
logging.info(click.style(f"Queue length: {queue_length}", fg="green"))
if queue_length >= threshold:
warning_msg = f"Queue {queue_name} task count exceeded the limit.: {queue_length}/{threshold}"
logging.warning(click.style(warning_msg, fg="red"))
alter_emails = dify_config.QUEUE_MONITOR_ALERT_EMAILS
if alter_emails:
to_list = alter_emails.split(",")
for to in to_list:
try:
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
html_content = render_template(
"queue_monitor_alert_email_template_en-US.html",
queue_name=queue_name,
queue_length=queue_length,
threshold=threshold,
alert_time=current_time,
)
mail.send(
to=to, subject="Alert: Dataset Queue pending tasks exceeded the limit", html=html_content
)
except Exception as e:
logging.exception(click.style("Exception occurred during sending email", fg="red"))
except Exception as e:
logging.exception(click.style("Exception occurred during queue monitoring", fg="red"))
finally:
if db.session.is_active:
db.session.close()

@ -46,6 +46,8 @@ class TagService:
@staticmethod @staticmethod
def get_tag_by_tag_name(tag_type: str, current_tenant_id: str, tag_name: str) -> list: def get_tag_by_tag_name(tag_type: str, current_tenant_id: str, tag_name: str) -> list:
if not tag_type or not tag_name:
return []
tags = ( tags = (
db.session.query(Tag) db.session.query(Tag)
.filter(Tag.name == tag_name, Tag.tenant_id == current_tenant_id, Tag.type == tag_type) .filter(Tag.name == tag_name, Tag.tenant_id == current_tenant_id, Tag.type == tag_type)
@ -88,7 +90,7 @@ class TagService:
@staticmethod @staticmethod
def update_tags(args: dict, tag_id: str) -> Tag: def update_tags(args: dict, tag_id: str) -> Tag:
if TagService.get_tag_by_tag_name(args["type"], current_user.current_tenant_id, args["name"]): if TagService.get_tag_by_tag_name(args.get("type", ""), current_user.current_tenant_id, args.get("name", "")):
raise ValueError("Tag name already exists") raise ValueError("Tag name already exists")
tag = db.session.query(Tag).filter(Tag.id == tag_id).first() tag = db.session.query(Tag).filter(Tag.id == tag_id).first()
if not tag: if not tag:

@ -5,7 +5,7 @@ import uuid
import click import click
from celery import shared_task # type: ignore from celery import shared_task # type: ignore
from sqlalchemy import func, select from sqlalchemy import func
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from core.model_manager import ModelManager from core.model_manager import ModelManager
@ -68,11 +68,6 @@ def batch_create_segment_to_index_task(
model_type=ModelType.TEXT_EMBEDDING, model_type=ModelType.TEXT_EMBEDDING,
model=dataset.embedding_model, model=dataset.embedding_model,
) )
word_count_change = 0
segments_to_insert: list[str] = []
max_position_stmt = select(func.max(DocumentSegment.position)).where(
DocumentSegment.document_id == dataset_document.id
)
word_count_change = 0 word_count_change = 0
if embedding_model: if embedding_model:
tokens_list = embedding_model.get_text_embedding_num_tokens( tokens_list = embedding_model.get_text_embedding_num_tokens(

@ -0,0 +1,129 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 600px;
min-height: 605px;
margin: 40px auto;
padding: 36px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 2px 4px -2px rgba(9, 9, 11, 0.08);
}
.header {
margin-bottom: 24px;
}
.header img {
max-width: 100px;
height: auto;
}
.title {
font-weight: 600;
font-size: 24px;
line-height: 28.8px;
}
.description {
font-size: 13px;
line-height: 16px;
color: #676f83;
margin-top: 12px;
}
.alert-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #fef0f0;
margin: 16px auto;
border: 1px solid #fda29b;
}
.alert-title {
line-height: 24px;
font-weight: 700;
font-size: 18px;
color: #d92d20;
}
.alert-detail {
line-height: 20px;
font-size: 14px;
margin-top: 8px;
}
.typography {
letter-spacing: -0.07px;
font-weight: 400;
font-style: normal;
font-size: 14px;
line-height: 20px;
color: #354052;
margin-top: 12px;
margin-bottom: 12px;
}
.typography p{
margin: 0 auto;
}
.typography-title {
color: #101828;
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px;
margin-top: 12px;
margin-bottom: 4px;
}
.tip-list{
margin: 0;
padding-left: 10px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
</div>
<p class="title">Queue Monitoring Alert</p>
<p class="typography">Our system has detected an abnormal queue status that requires your attention:</p>
<div class="alert-content">
<div class="alert-title">Queue Task Alert</div>
<div class="alert-detail">
Queue "{{queue_name}}" has {{queue_length}} pending tasks (Threshold: {{threshold}})
</div>
</div>
<div class="typography">
<p style="margin-bottom:4px">Recommended actions:</p>
<p>1. Check the queue processing status in the system dashboard</p>
<p>2. Verify if there are any processing bottlenecks</p>
<p>3. Consider scaling up workers if needed</p>
</div>
<p class="typography-title">Additional Information:</p>
<ul class="typography tip-list">
<li>Alert triggered at: {{alert_time}}</li>
</ul>
</div>
</body>
</html>

@ -3,11 +3,16 @@ import os
import time import time
import uuid import uuid
from collections.abc import Generator from collections.abc import Generator
from unittest.mock import MagicMock from decimal import Decimal
from unittest.mock import MagicMock, patch
import pytest import pytest
from app_factory import create_app
from configs import dify_config
from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.app_invoke_entities import InvokeFrom
from core.model_runtime.entities.llm_entities import LLMResult, LLMUsage
from core.model_runtime.entities.message_entities import AssistantPromptMessage
from core.workflow.entities.variable_pool import VariablePool from core.workflow.entities.variable_pool import VariablePool
from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus
from core.workflow.enums import SystemVariableKey from core.workflow.enums import SystemVariableKey
@ -19,13 +24,27 @@ from core.workflow.nodes.llm.node import LLMNode
from extensions.ext_database import db from extensions.ext_database import db
from models.enums import UserFrom from models.enums import UserFrom
from models.workflow import WorkflowType from models.workflow import WorkflowType
from tests.integration_tests.workflow.nodes.__mock.model import get_mocked_fetch_model_config
"""FOR MOCK FIXTURES, DO NOT REMOVE""" """FOR MOCK FIXTURES, DO NOT REMOVE"""
from tests.integration_tests.model_runtime.__mock.plugin_daemon import setup_model_mock from tests.integration_tests.model_runtime.__mock.plugin_daemon import setup_model_mock
from tests.integration_tests.workflow.nodes.__mock.code_executor import setup_code_executor_mock from tests.integration_tests.workflow.nodes.__mock.code_executor import setup_code_executor_mock
@pytest.fixture(scope="session")
def app():
# Set up storage configuration
os.environ["STORAGE_TYPE"] = "opendal"
os.environ["OPENDAL_SCHEME"] = "fs"
os.environ["OPENDAL_FS_ROOT"] = "storage"
# Ensure storage directory exists
os.makedirs("storage", exist_ok=True)
app = create_app()
dify_config.LOGIN_DISABLED = True
return app
def init_llm_node(config: dict) -> LLMNode: def init_llm_node(config: dict) -> LLMNode:
graph_config = { graph_config = {
"edges": [ "edges": [
@ -40,13 +59,19 @@ def init_llm_node(config: dict) -> LLMNode:
graph = Graph.init(graph_config=graph_config) graph = Graph.init(graph_config=graph_config)
# Use proper UUIDs for database compatibility
tenant_id = "9d2074fc-6f86-45a9-b09d-6ecc63b9056b"
app_id = "9d2074fc-6f86-45a9-b09d-6ecc63b9056c"
workflow_id = "9d2074fc-6f86-45a9-b09d-6ecc63b9056d"
user_id = "9d2074fc-6f86-45a9-b09d-6ecc63b9056e"
init_params = GraphInitParams( init_params = GraphInitParams(
tenant_id="1", tenant_id=tenant_id,
app_id="1", app_id=app_id,
workflow_type=WorkflowType.WORKFLOW, workflow_type=WorkflowType.WORKFLOW,
workflow_id="1", workflow_id=workflow_id,
graph_config=graph_config, graph_config=graph_config,
user_id="1", user_id=user_id,
user_from=UserFrom.ACCOUNT, user_from=UserFrom.ACCOUNT,
invoke_from=InvokeFrom.DEBUGGER, invoke_from=InvokeFrom.DEBUGGER,
call_depth=0, call_depth=0,
@ -77,115 +102,197 @@ def init_llm_node(config: dict) -> LLMNode:
return node return node
def test_execute_llm(setup_model_mock): def test_execute_llm(app):
node = init_llm_node( with app.app_context():
config={ node = init_llm_node(
"id": "llm", config={
"data": { "id": "llm",
"title": "123", "data": {
"type": "llm", "title": "123",
"model": { "type": "llm",
"provider": "langgenius/openai/openai", "model": {
"name": "gpt-3.5-turbo", "provider": "langgenius/openai/openai",
"mode": "chat", "name": "gpt-3.5-turbo",
"completion_params": {}, "mode": "chat",
"completion_params": {},
},
"prompt_template": [
{
"role": "system",
"text": "you are a helpful assistant.\ntoday's weather is {{#abc.output#}}.",
},
{"role": "user", "text": "{{#sys.query#}}"},
],
"memory": None,
"context": {"enabled": False},
"vision": {"enabled": False},
}, },
"prompt_template": [
{"role": "system", "text": "you are a helpful assistant.\ntoday's weather is {{#abc.output#}}."},
{"role": "user", "text": "{{#sys.query#}}"},
],
"memory": None,
"context": {"enabled": False},
"vision": {"enabled": False},
}, },
}, )
)
credentials = {"openai_api_key": os.environ.get("OPENAI_API_KEY")} credentials = {"openai_api_key": os.environ.get("OPENAI_API_KEY")}
# Mock db.session.close() # Create a proper LLM result with real entities
db.session.close = MagicMock() mock_usage = LLMUsage(
prompt_tokens=30,
prompt_unit_price=Decimal("0.001"),
prompt_price_unit=Decimal("1000"),
prompt_price=Decimal("0.00003"),
completion_tokens=20,
completion_unit_price=Decimal("0.002"),
completion_price_unit=Decimal("1000"),
completion_price=Decimal("0.00004"),
total_tokens=50,
total_price=Decimal("0.00007"),
currency="USD",
latency=0.5,
)
node._fetch_model_config = get_mocked_fetch_model_config( mock_message = AssistantPromptMessage(content="This is a test response from the mocked LLM.")
provider="langgenius/openai/openai",
model="gpt-3.5-turbo", mock_llm_result = LLMResult(
mode="chat", model="gpt-3.5-turbo",
credentials=credentials, prompt_messages=[],
) message=mock_message,
usage=mock_usage,
)
# Create a simple mock model instance that doesn't call real providers
mock_model_instance = MagicMock()
mock_model_instance.invoke_llm.return_value = mock_llm_result
# Create a simple mock model config with required attributes
mock_model_config = MagicMock()
mock_model_config.mode = "chat"
mock_model_config.provider = "langgenius/openai/openai"
mock_model_config.model = "gpt-3.5-turbo"
mock_model_config.provider_model_bundle.configuration.tenant_id = "9d2074fc-6f86-45a9-b09d-6ecc63b9056b"
# Mock the _fetch_model_config method
def mock_fetch_model_config_func(_node_data_model):
return mock_model_instance, mock_model_config
# Also mock ModelManager.get_model_instance to avoid database calls
def mock_get_model_instance(_self, **kwargs):
return mock_model_instance
# execute node with (
result = node._run() patch.object(node, "_fetch_model_config", mock_fetch_model_config_func),
assert isinstance(result, Generator) patch("core.model_manager.ModelManager.get_model_instance", mock_get_model_instance),
):
# execute node
result = node._run()
assert isinstance(result, Generator)
for item in result: for item in result:
if isinstance(item, RunCompletedEvent): if isinstance(item, RunCompletedEvent):
assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED
assert item.run_result.process_data is not None assert item.run_result.process_data is not None
assert item.run_result.outputs is not None assert item.run_result.outputs is not None
assert item.run_result.outputs.get("text") is not None assert item.run_result.outputs.get("text") is not None
assert item.run_result.outputs.get("usage", {})["total_tokens"] > 0 assert item.run_result.outputs.get("usage", {})["total_tokens"] > 0
@pytest.mark.parametrize("setup_code_executor_mock", [["none"]], indirect=True) @pytest.mark.parametrize("setup_code_executor_mock", [["none"]], indirect=True)
def test_execute_llm_with_jinja2(setup_code_executor_mock, setup_model_mock): def test_execute_llm_with_jinja2(app, setup_code_executor_mock):
""" """
Test execute LLM node with jinja2 Test execute LLM node with jinja2
""" """
node = init_llm_node( with app.app_context():
config={ node = init_llm_node(
"id": "llm", config={
"data": { "id": "llm",
"title": "123", "data": {
"type": "llm", "title": "123",
"model": {"provider": "openai", "name": "gpt-3.5-turbo", "mode": "chat", "completion_params": {}}, "type": "llm",
"prompt_config": { "model": {"provider": "openai", "name": "gpt-3.5-turbo", "mode": "chat", "completion_params": {}},
"jinja2_variables": [ "prompt_config": {
{"variable": "sys_query", "value_selector": ["sys", "query"]}, "jinja2_variables": [
{"variable": "output", "value_selector": ["abc", "output"]}, {"variable": "sys_query", "value_selector": ["sys", "query"]},
] {"variable": "output", "value_selector": ["abc", "output"]},
}, ]
"prompt_template": [
{
"role": "system",
"text": "you are a helpful assistant.\ntoday's weather is {{#abc.output#}}",
"jinja2_text": "you are a helpful assistant.\ntoday's weather is {{output}}.",
"edition_type": "jinja2",
}, },
{ "prompt_template": [
"role": "user", {
"text": "{{#sys.query#}}", "role": "system",
"jinja2_text": "{{sys_query}}", "text": "you are a helpful assistant.\ntoday's weather is {{#abc.output#}}",
"edition_type": "basic", "jinja2_text": "you are a helpful assistant.\ntoday's weather is {{output}}.",
}, "edition_type": "jinja2",
], },
"memory": None, {
"context": {"enabled": False}, "role": "user",
"vision": {"enabled": False}, "text": "{{#sys.query#}}",
"jinja2_text": "{{sys_query}}",
"edition_type": "basic",
},
],
"memory": None,
"context": {"enabled": False},
"vision": {"enabled": False},
},
}, },
}, )
)
credentials = {"openai_api_key": os.environ.get("OPENAI_API_KEY")} # Mock db.session.close()
db.session.close = MagicMock()
# Mock db.session.close() # Create a proper LLM result with real entities
db.session.close = MagicMock() mock_usage = LLMUsage(
prompt_tokens=30,
prompt_unit_price=Decimal("0.001"),
prompt_price_unit=Decimal("1000"),
prompt_price=Decimal("0.00003"),
completion_tokens=20,
completion_unit_price=Decimal("0.002"),
completion_price_unit=Decimal("1000"),
completion_price=Decimal("0.00004"),
total_tokens=50,
total_price=Decimal("0.00007"),
currency="USD",
latency=0.5,
)
node._fetch_model_config = get_mocked_fetch_model_config( mock_message = AssistantPromptMessage(content="Test response: sunny weather and what's the weather today?")
provider="langgenius/openai/openai",
model="gpt-3.5-turbo", mock_llm_result = LLMResult(
mode="chat", model="gpt-3.5-turbo",
credentials=credentials, prompt_messages=[],
) message=mock_message,
usage=mock_usage,
)
# Create a simple mock model instance that doesn't call real providers
mock_model_instance = MagicMock()
mock_model_instance.invoke_llm.return_value = mock_llm_result
# Create a simple mock model config with required attributes
mock_model_config = MagicMock()
mock_model_config.mode = "chat"
mock_model_config.provider = "openai"
mock_model_config.model = "gpt-3.5-turbo"
mock_model_config.provider_model_bundle.configuration.tenant_id = "9d2074fc-6f86-45a9-b09d-6ecc63b9056b"
# Mock the _fetch_model_config method
def mock_fetch_model_config_func(_node_data_model):
return mock_model_instance, mock_model_config
# Also mock ModelManager.get_model_instance to avoid database calls
def mock_get_model_instance(_self, **kwargs):
return mock_model_instance
# execute node with (
result = node._run() patch.object(node, "_fetch_model_config", mock_fetch_model_config_func),
patch("core.model_manager.ModelManager.get_model_instance", mock_get_model_instance),
):
# execute node
result = node._run()
for item in result: for item in result:
if isinstance(item, RunCompletedEvent): if isinstance(item, RunCompletedEvent):
assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED
assert item.run_result.process_data is not None assert item.run_result.process_data is not None
assert "sunny" in json.dumps(item.run_result.process_data) assert "sunny" in json.dumps(item.run_result.process_data)
assert "what's the weather today?" in json.dumps(item.run_result.process_data) assert "what's the weather today?" in json.dumps(item.run_result.process_data)
def test_extract_json(): def test_extract_json():

@ -1057,7 +1057,7 @@ PLUGIN_MAX_EXECUTION_TIMEOUT=600
PIP_MIRROR_URL= PIP_MIRROR_URL=
# https://github.com/langgenius/dify-plugin-daemon/blob/main/.env.example # https://github.com/langgenius/dify-plugin-daemon/blob/main/.env.example
# Plugin storage type, local aws_s3 tencent_cos azure_blob aliyun_oss # Plugin storage type, local aws_s3 tencent_cos azure_blob aliyun_oss volcengine_tos
PLUGIN_STORAGE_TYPE=local PLUGIN_STORAGE_TYPE=local
PLUGIN_STORAGE_LOCAL_ROOT=/app/storage PLUGIN_STORAGE_LOCAL_ROOT=/app/storage
PLUGIN_WORKING_PATH=/app/storage/cwd PLUGIN_WORKING_PATH=/app/storage/cwd
@ -1087,6 +1087,11 @@ PLUGIN_ALIYUN_OSS_ACCESS_KEY_ID=
PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET= PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET=
PLUGIN_ALIYUN_OSS_AUTH_VERSION=v4 PLUGIN_ALIYUN_OSS_AUTH_VERSION=v4
PLUGIN_ALIYUN_OSS_PATH= PLUGIN_ALIYUN_OSS_PATH=
# Plugin oss volcengine tos
PLUGIN_VOLCENGINE_TOS_ENDPOINT=
PLUGIN_VOLCENGINE_TOS_ACCESS_KEY=
PLUGIN_VOLCENGINE_TOS_SECRET_KEY=
PLUGIN_VOLCENGINE_TOS_REGION=
# ------------------------------ # ------------------------------
# OTLP Collector Configuration # OTLP Collector Configuration
@ -1106,3 +1111,10 @@ OTEL_METRIC_EXPORT_TIMEOUT=30000
# Prevent Clickjacking # Prevent Clickjacking
ALLOW_EMBED=false ALLOW_EMBED=false
# Dataset queue monitor configuration
QUEUE_MONITOR_THRESHOLD=200
# You can configure multiple ones, separated by commas. eg: test1@dify.ai,test2@dify.ai
QUEUE_MONITOR_ALERT_EMAILS=
# Monitor interval in minutes, default is 30 minutes
QUEUE_MONITOR_INTERVAL=30

@ -184,6 +184,10 @@ services:
ALIYUN_OSS_ACCESS_KEY_SECRET: ${PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET:-} ALIYUN_OSS_ACCESS_KEY_SECRET: ${PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET:-}
ALIYUN_OSS_AUTH_VERSION: ${PLUGIN_ALIYUN_OSS_AUTH_VERSION:-v4} ALIYUN_OSS_AUTH_VERSION: ${PLUGIN_ALIYUN_OSS_AUTH_VERSION:-v4}
ALIYUN_OSS_PATH: ${PLUGIN_ALIYUN_OSS_PATH:-} ALIYUN_OSS_PATH: ${PLUGIN_ALIYUN_OSS_PATH:-}
VOLCENGINE_TOS_ENDPOINT: ${PLUGIN_VOLCENGINE_TOS_ENDPOINT:-}
VOLCENGINE_TOS_ACCESS_KEY: ${PLUGIN_VOLCENGINE_TOS_ACCESS_KEY:-}
VOLCENGINE_TOS_SECRET_KEY: ${PLUGIN_VOLCENGINE_TOS_SECRET_KEY:-}
VOLCENGINE_TOS_REGION: ${PLUGIN_VOLCENGINE_TOS_REGION:-}
ports: ports:
- "${EXPOSE_PLUGIN_DEBUGGING_PORT:-5003}:${PLUGIN_DEBUGGING_PORT:-5003}" - "${EXPOSE_PLUGIN_DEBUGGING_PORT:-5003}:${PLUGIN_DEBUGGING_PORT:-5003}"
volumes: volumes:

@ -121,6 +121,10 @@ services:
ALIYUN_OSS_ACCESS_KEY_SECRET: ${PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET:-} ALIYUN_OSS_ACCESS_KEY_SECRET: ${PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET:-}
ALIYUN_OSS_AUTH_VERSION: ${PLUGIN_ALIYUN_OSS_AUTH_VERSION:-v4} ALIYUN_OSS_AUTH_VERSION: ${PLUGIN_ALIYUN_OSS_AUTH_VERSION:-v4}
ALIYUN_OSS_PATH: ${PLUGIN_ALIYUN_OSS_PATH:-} ALIYUN_OSS_PATH: ${PLUGIN_ALIYUN_OSS_PATH:-}
VOLCENGINE_TOS_ENDPOINT: ${PLUGIN_VOLCENGINE_TOS_ENDPOINT:-}
VOLCENGINE_TOS_ACCESS_KEY: ${PLUGIN_VOLCENGINE_TOS_ACCESS_KEY:-}
VOLCENGINE_TOS_SECRET_KEY: ${PLUGIN_VOLCENGINE_TOS_SECRET_KEY:-}
VOLCENGINE_TOS_REGION: ${PLUGIN_VOLCENGINE_TOS_REGION:-}
ports: ports:
- "${EXPOSE_PLUGIN_DAEMON_PORT:-5002}:${PLUGIN_DAEMON_PORT:-5002}" - "${EXPOSE_PLUGIN_DAEMON_PORT:-5002}:${PLUGIN_DAEMON_PORT:-5002}"
- "${EXPOSE_PLUGIN_DEBUGGING_PORT:-5003}:${PLUGIN_DEBUGGING_PORT:-5003}" - "${EXPOSE_PLUGIN_DEBUGGING_PORT:-5003}:${PLUGIN_DEBUGGING_PORT:-5003}"

@ -484,6 +484,10 @@ x-shared-env: &shared-api-worker-env
PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET: ${PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET:-} PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET: ${PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET:-}
PLUGIN_ALIYUN_OSS_AUTH_VERSION: ${PLUGIN_ALIYUN_OSS_AUTH_VERSION:-v4} PLUGIN_ALIYUN_OSS_AUTH_VERSION: ${PLUGIN_ALIYUN_OSS_AUTH_VERSION:-v4}
PLUGIN_ALIYUN_OSS_PATH: ${PLUGIN_ALIYUN_OSS_PATH:-} PLUGIN_ALIYUN_OSS_PATH: ${PLUGIN_ALIYUN_OSS_PATH:-}
PLUGIN_VOLCENGINE_TOS_ENDPOINT: ${PLUGIN_VOLCENGINE_TOS_ENDPOINT:-}
PLUGIN_VOLCENGINE_TOS_ACCESS_KEY: ${PLUGIN_VOLCENGINE_TOS_ACCESS_KEY:-}
PLUGIN_VOLCENGINE_TOS_SECRET_KEY: ${PLUGIN_VOLCENGINE_TOS_SECRET_KEY:-}
PLUGIN_VOLCENGINE_TOS_REGION: ${PLUGIN_VOLCENGINE_TOS_REGION:-}
ENABLE_OTEL: ${ENABLE_OTEL:-false} ENABLE_OTEL: ${ENABLE_OTEL:-false}
OTLP_BASE_ENDPOINT: ${OTLP_BASE_ENDPOINT:-http://localhost:4318} OTLP_BASE_ENDPOINT: ${OTLP_BASE_ENDPOINT:-http://localhost:4318}
OTLP_API_KEY: ${OTLP_API_KEY:-} OTLP_API_KEY: ${OTLP_API_KEY:-}
@ -497,6 +501,9 @@ x-shared-env: &shared-api-worker-env
OTEL_BATCH_EXPORT_TIMEOUT: ${OTEL_BATCH_EXPORT_TIMEOUT:-10000} OTEL_BATCH_EXPORT_TIMEOUT: ${OTEL_BATCH_EXPORT_TIMEOUT:-10000}
OTEL_METRIC_EXPORT_TIMEOUT: ${OTEL_METRIC_EXPORT_TIMEOUT:-30000} OTEL_METRIC_EXPORT_TIMEOUT: ${OTEL_METRIC_EXPORT_TIMEOUT:-30000}
ALLOW_EMBED: ${ALLOW_EMBED:-false} ALLOW_EMBED: ${ALLOW_EMBED:-false}
QUEUE_MONITOR_THRESHOLD: ${QUEUE_MONITOR_THRESHOLD:-200}
QUEUE_MONITOR_ALERT_EMAILS: ${QUEUE_MONITOR_ALERT_EMAILS:-}
QUEUE_MONITOR_INTERVAL: ${QUEUE_MONITOR_INTERVAL:-30}
services: services:
# API service # API service
@ -683,6 +690,10 @@ services:
ALIYUN_OSS_ACCESS_KEY_SECRET: ${PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET:-} ALIYUN_OSS_ACCESS_KEY_SECRET: ${PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET:-}
ALIYUN_OSS_AUTH_VERSION: ${PLUGIN_ALIYUN_OSS_AUTH_VERSION:-v4} ALIYUN_OSS_AUTH_VERSION: ${PLUGIN_ALIYUN_OSS_AUTH_VERSION:-v4}
ALIYUN_OSS_PATH: ${PLUGIN_ALIYUN_OSS_PATH:-} ALIYUN_OSS_PATH: ${PLUGIN_ALIYUN_OSS_PATH:-}
VOLCENGINE_TOS_ENDPOINT: ${PLUGIN_VOLCENGINE_TOS_ENDPOINT:-}
VOLCENGINE_TOS_ACCESS_KEY: ${PLUGIN_VOLCENGINE_TOS_ACCESS_KEY:-}
VOLCENGINE_TOS_SECRET_KEY: ${PLUGIN_VOLCENGINE_TOS_SECRET_KEY:-}
VOLCENGINE_TOS_REGION: ${PLUGIN_VOLCENGINE_TOS_REGION:-}
ports: ports:
- "${EXPOSE_PLUGIN_DEBUGGING_PORT:-5003}:${PLUGIN_DEBUGGING_PORT:-5003}" - "${EXPOSE_PLUGIN_DEBUGGING_PORT:-5003}:${PLUGIN_DEBUGGING_PORT:-5003}"
volumes: volumes:

@ -152,3 +152,8 @@ PLUGIN_ALIYUN_OSS_ACCESS_KEY_ID=
PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET= PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET=
PLUGIN_ALIYUN_OSS_AUTH_VERSION=v4 PLUGIN_ALIYUN_OSS_AUTH_VERSION=v4
PLUGIN_ALIYUN_OSS_PATH= PLUGIN_ALIYUN_OSS_PATH=
# Plugin oss volcengine tos
PLUGIN_VOLCENGINE_TOS_ENDPOINT=
PLUGIN_VOLCENGINE_TOS_ACCESS_KEY=
PLUGIN_VOLCENGINE_TOS_SECRET_KEY=
PLUGIN_VOLCENGINE_TOS_REGION=

@ -4,7 +4,7 @@ import { useContext, useContextSelector } from 'use-context-selector'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill } from '@remixicon/react' import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill, RiVerifiedBadgeLine } from '@remixicon/react'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { App } from '@/types/app' import type { App } from '@/types/app'
import Confirm from '@/app/components/base/confirm' import Confirm from '@/app/components/base/confirm'
@ -338,7 +338,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
</div> </div>
<div className='flex h-5 w-5 shrink-0 items-center justify-center'> <div className='flex h-5 w-5 shrink-0 items-center justify-center'>
{app.access_mode === AccessMode.PUBLIC && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.anyone')}> {app.access_mode === AccessMode.PUBLIC && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.anyone')}>
<RiGlobalLine className='h-4 w-4 text-text-accent' /> <RiGlobalLine className='h-4 w-4 text-text-quaternary' />
</Tooltip>} </Tooltip>}
{app.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.specific')}> {app.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.specific')}>
<RiLockLine className='h-4 w-4 text-text-quaternary' /> <RiLockLine className='h-4 w-4 text-text-quaternary' />
@ -346,6 +346,9 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
{app.access_mode === AccessMode.ORGANIZATION && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.organization')}> {app.access_mode === AccessMode.ORGANIZATION && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.organization')}>
<RiBuildingLine className='h-4 w-4 text-text-quaternary' /> <RiBuildingLine className='h-4 w-4 text-text-quaternary' />
</Tooltip>} </Tooltip>}
{app.access_mode === AccessMode.EXTERNAL_MEMBERS && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.external')}>
<RiVerifiedBadgeLine className='h-4 w-4 text-text-quaternary' />
</Tooltip>}
</div> </div>
</div> </div>
<div className='title-wrapper h-[90px] px-[14px] text-xs leading-normal text-text-tertiary'> <div className='title-wrapper h-[90px] px-[14px] text-xs leading-normal text-text-tertiary'>

@ -87,7 +87,7 @@ const Container = () => {
return ( return (
<div ref={containerRef} className='scroll-container relative flex grow flex-col overflow-y-auto bg-background-body'> <div ref={containerRef} className='scroll-container relative flex grow flex-col overflow-y-auto bg-background-body'>
<div className='sticky top-0 z-10 flex flex-wrap items-center justify-between gap-y-2 bg-background-body px-12 pb-2 pt-4 leading-[56px]'> <div className='sticky top-0 z-10 flex h-[80px] shrink-0 flex-wrap items-center justify-between gap-y-2 bg-background-body px-12 pb-2 pt-4 leading-[56px]'>
<TabSliderNew <TabSliderNew
value={activeTab} value={activeTab}
onChange={newActiveTab => setActiveTab(newActiveTab)} onChange={newActiveTab => setActiveTab(newActiveTab)}

@ -192,15 +192,15 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
- original_document_id が渡されない場合、新しい操作が実行され、process_rule が必要です。 - original_document_id が渡されない場合、新しい操作が実行され、process_rule が必要です。
- <code>indexing_technique</code> インデックスモード - <code>indexing_technique</code> インデックスモード
- <code>high_quality</code> 高品質: 埋め込みモデルを使用してベクトルデータベースインデックスを構築 - <code>high_quality</code> 高品質埋め込みモデルを使用してベクトルデータベースインデックスを構築
- <code>economy</code> 経済: キーワードテーブルインデックスの反転インデックスを構築 - <code>economy</code> 経済キーワードテーブルインデックスの反転インデックスを構築
- <code>doc_form</code> インデックス化された内容の形式 - <code>doc_form</code> インデックス化された内容の形式
- <code>text_model</code> テキストドキュメントは直接埋め込まれます; `economy` モードではこの形式がデフォルト - <code>text_model</code> テキストドキュメントは直接埋め込まれます; `economy` モードではこの形式がデフォルト
- <code>hierarchical_model</code> 親子モード - <code>hierarchical_model</code> 親子モード
- <code>qa_model</code> Q&A モード: 分割されたドキュメントの質問と回答ペアを生成し、質問を埋め込みます - <code>qa_model</code> Q&A モード分割されたドキュメントの質問と回答ペアを生成し、質問を埋め込みます
- <code>doc_language</code> Q&A モードでは、ドキュメントの言語を指定します。例: <code>English</code>, <code>Chinese</code> - <code>doc_language</code> Q&A モードでは、ドキュメントの言語を指定します。例<code>English</code>, <code>Chinese</code>
- <code>process_rule</code> 処理ルール - <code>process_rule</code> 処理ルール
- <code>mode</code> (string) クリーニング、セグメンテーションモード、自動 / カスタム - <code>mode</code> (string) クリーニング、セグメンテーションモード、自動 / カスタム
@ -214,7 +214,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
- <code>segmentation</code> (object) セグメンテーションルール - <code>segmentation</code> (object) セグメンテーションルール
- <code>separator</code> カスタムセグメント識別子。現在は 1 つの区切り文字のみ設定可能。デフォルトは \n - <code>separator</code> カスタムセグメント識別子。現在は 1 つの区切り文字のみ設定可能。デフォルトは \n
- <code>max_tokens</code> 最大長 (トークン) デフォルトは 1000 - <code>max_tokens</code> 最大長 (トークン) デフォルトは 1000
- <code>parent_mode</code> 親チャンクの検索モード: <code>full-doc</code> 全文検索 / <code>paragraph</code> 段落検索 - <code>parent_mode</code> 親チャンクの検索モード<code>full-doc</code> 全文検索 / <code>paragraph</code> 段落検索
- <code>subchunk_segmentation</code> (object) 子チャンクルール - <code>subchunk_segmentation</code> (object) 子チャンクルール
- <code>separator</code> セグメンテーション識別子。現在は 1 つの区切り文字のみ許可。デフォルトは <code>***</code> - <code>separator</code> セグメンテーション識別子。現在は 1 つの区切り文字のみ許可。デフォルトは <code>***</code>
- <code>max_tokens</code> 最大長 (トークン) は親チャンクの長さより短いことを検証する必要があります - <code>max_tokens</code> 最大長 (トークン) は親チャンクの長さより短いことを検証する必要があります
@ -324,7 +324,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
- <code>partial_members</code> 一部のメンバー - <code>partial_members</code> 一部のメンバー
</Property> </Property>
<Property name='provider' type='string' key='provider'> <Property name='provider' type='string' key='provider'>
プロバイダー (オプション、デフォルト: vendor) プロバイダー (オプション、デフォルトvendor)
- <code>vendor</code> ベンダー - <code>vendor</code> ベンダー
- <code>external</code> 外部ナレッジ - <code>external</code> 外部ナレッジ
</Property> </Property>
@ -415,16 +415,16 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
検索キーワード、オプション 検索キーワード、オプション
</Property> </Property>
<Property name='tag_ids' type='array[string]' key='tag_ids'> <Property name='tag_ids' type='array[string]' key='tag_ids'>
タグIDリスト、オプション タグ ID リスト、オプション
</Property> </Property>
<Property name='page' type='string' key='page'> <Property name='page' type='string' key='page'>
ページ番号、オプション、デフォルト1 ページ番号、オプション、デフォルト 1
</Property> </Property>
<Property name='limit' type='string' key='limit'> <Property name='limit' type='string' key='limit'>
返されるアイテム数、オプション、デフォルト20、範囲1-100 返されるアイテム数、オプション、デフォルト 20、範囲 1-100
</Property> </Property>
<Property name='include_all' type='boolean' key='include_all'> <Property name='include_all' type='boolean' key='include_all'>
すべてのデータセットを含めるかどうか所有者のみ有効、オプション、デフォルトはfalse すべてのデータセットを含めるかどうか(所有者のみ有効)、オプション、デフォルトは false
</Property> </Property>
</Properties> </Properties>
</Col> </Col>
@ -2013,7 +2013,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
### Request Body ### Request Body
<Properties> <Properties>
<Property name='name' type='string'> <Property name='name' type='string'>
(text) 新しいタグ名、必須、最大長50文字 (text) 新しいタグ名、必須、最大長 50 文字
</Property> </Property>
</Properties> </Properties>
</Col> </Col>
@ -2099,10 +2099,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
### Request Body ### Request Body
<Properties> <Properties>
<Property name='name' type='string'> <Property name='name' type='string'>
(text) 変更後のタグ名、必須、最大長50文字 (text) 変更後のタグ名、必須、最大長 50 文字
</Property> </Property>
<Property name='tag_id' type='string'> <Property name='tag_id' type='string'>
(text) タグID、必須 (text) タグ ID、必須
</Property> </Property>
</Properties> </Properties>
</Col> </Col>
@ -2147,7 +2147,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
### Request Body ### Request Body
<Properties> <Properties>
<Property name='tag_id' type='string'> <Property name='tag_id' type='string'>
(text) タグID、必須 (text) タグ ID、必須
</Property> </Property>
</Properties> </Properties>
</Col> </Col>
@ -2188,10 +2188,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
### Request Body ### Request Body
<Properties> <Properties>
<Property name='tag_ids' type='list'> <Property name='tag_ids' type='list'>
(list) タグIDリスト、必須 (list) タグ ID リスト、必須
</Property> </Property>
<Property name='target_id' type='string'> <Property name='target_id' type='string'>
(text) ナレッジベースID、必須 (text) ナレッジベース ID、必須
</Property> </Property>
</Properties> </Properties>
</Col> </Col>
@ -2230,10 +2230,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
### Request Body ### Request Body
<Properties> <Properties>
<Property name='tag_id' type='string'> <Property name='tag_id' type='string'>
(text) タグID、必須 (text) タグ ID、必須
</Property> </Property>
<Property name='target_id' type='string'> <Property name='target_id' type='string'>
(text) ナレッジベースID、必須 (text) ナレッジベース ID、必須
</Property> </Property>
</Properties> </Properties>
</Col> </Col>
@ -2273,7 +2273,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
### Path ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string'> <Property name='dataset_id' type='string'>
(text) ナレッジベースID (text) ナレッジベース ID
</Property> </Property>
</Properties> </Properties>
</Col> </Col>

@ -207,7 +207,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
- <code>doc_language</code> 在 Q&A 模式下,指定文档的语言,例如:<code>English</code>、<code>Chinese</code> - <code>doc_language</code> 在 Q&A 模式下,指定文档的语言,例如:<code>English</code>、<code>Chinese</code>
- <code>process_rule</code> 处理规则 - <code>process_rule</code> 处理规则
- <code>mode</code> (string) 清洗、分段模式 automatic 自动 / custom 自定义 / hierarchical 父子 - <code>mode</code> (string) 清洗、分段模式automatic 自动 / custom 自定义 / hierarchical 父子
- <code>rules</code> (object) 自定义规则(自动模式下,该字段为空) - <code>rules</code> (object) 自定义规则(自动模式下,该字段为空)
- <code>pre_processing_rules</code> (array[object]) 预处理规则 - <code>pre_processing_rules</code> (array[object]) 预处理规则
- <code>id</code> (string) 预处理规则的唯一标识符 - <code>id</code> (string) 预处理规则的唯一标识符
@ -234,12 +234,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
- <code>hybrid_search</code> 混合检索 - <code>hybrid_search</code> 混合检索
- <code>semantic_search</code> 语义检索 - <code>semantic_search</code> 语义检索
- <code>full_text_search</code> 全文检索 - <code>full_text_search</code> 全文检索
- <code>reranking_enable</code> (bool) 是否开启rerank - <code>reranking_enable</code> (bool) 是否开启 rerank
- <code>reranking_model</code> (object) Rerank 模型配置 - <code>reranking_model</code> (object) Rerank 模型配置
- <code>reranking_provider_name</code> (string) Rerank 模型的提供商 - <code>reranking_provider_name</code> (string) Rerank 模型的提供商
- <code>reranking_model_name</code> (string) Rerank 模型的名称 - <code>reranking_model_name</code> (string) Rerank 模型的名称
- <code>top_k</code> (int) 召回条数 - <code>top_k</code> (int) 召回条数
- <code>score_threshold_enabled</code> (bool)是否开启召回分数限制 - <code>score_threshold_enabled</code> (bool) 是否开启召回分数限制
- <code>score_threshold</code> (float) 召回分数限制 - <code>score_threshold</code> (float) 召回分数限制
</Property> </Property>
<Property name='embedding_model' type='string' key='embedding_model'> <Property name='embedding_model' type='string' key='embedding_model'>
@ -350,12 +350,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
- <code>hybrid_search</code> 混合检索 - <code>hybrid_search</code> 混合检索
- <code>semantic_search</code> 语义检索 - <code>semantic_search</code> 语义检索
- <code>full_text_search</code> 全文检索 - <code>full_text_search</code> 全文检索
- <code>reranking_enable</code> (bool) 是否开启rerank - <code>reranking_enable</code> (bool) 是否开启 rerank
- <code>reranking_model</code> (object) Rerank 模型配置 - <code>reranking_model</code> (object) Rerank 模型配置
- <code>reranking_provider_name</code> (string) Rerank 模型的提供商 - <code>reranking_provider_name</code> (string) Rerank 模型的提供商
- <code>reranking_model_name</code> (string) Rerank 模型的名称 - <code>reranking_model_name</code> (string) Rerank 模型的名称
- <code>top_k</code> (int) 召回条数 - <code>top_k</code> (int) 召回条数
- <code>score_threshold_enabled</code> (bool)是否开启召回分数限制 - <code>score_threshold_enabled</code> (bool) 是否开启召回分数限制
- <code>score_threshold</code> (float) 召回分数限制 - <code>score_threshold</code> (float) 召回分数限制
</Property> </Property>
</Properties> </Properties>
@ -1322,7 +1322,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
文档 ID 文档 ID
</Property> </Property>
<Property name='segment_id' type='string' key='segment_id'> <Property name='segment_id' type='string' key='segment_id'>
文档分段ID 文档分段 ID
</Property> </Property>
</Properties> </Properties>
</Col> </Col>
@ -1435,7 +1435,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
文档 ID 文档 ID
</Property> </Property>
<Property name='segment_id' type='string' key='segment_id'> <Property name='segment_id' type='string' key='segment_id'>
文档分段ID 文档分段 ID
</Property> </Property>
</Properties> </Properties>
@ -2404,7 +2404,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
### Request Body ### Request Body
<Properties> <Properties>
<Property name='name' type='string'> <Property name='name' type='string'>
(text) 新标签名称必填最大长度为50 (text) 新标签名称,必填,最大长度为 50
</Property> </Property>
</Properties> </Properties>
</Col> </Col>
@ -2490,10 +2490,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
### Request Body ### Request Body
<Properties> <Properties>
<Property name='name' type='string'> <Property name='name' type='string'>
(text) 修改后的标签名称必填最大长度为50 (text) 修改后的标签名称,必填,最大长度为 50
</Property> </Property>
<Property name='tag_id' type='string'> <Property name='tag_id' type='string'>
(text) 标签ID必填 (text) 标签 ID必填
</Property> </Property>
</Properties> </Properties>
</Col> </Col>
@ -2538,7 +2538,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
### Request Body ### Request Body
<Properties> <Properties>
<Property name='tag_id' type='string'> <Property name='tag_id' type='string'>
(text) 标签ID必填 (text) 标签 ID必填
</Property> </Property>
</Properties> </Properties>
</Col> </Col>
@ -2579,10 +2579,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
### Request Body ### Request Body
<Properties> <Properties>
<Property name='tag_ids' type='list'> <Property name='tag_ids' type='list'>
(list) 标签ID列表必填 (list) 标签 ID 列表,必填
</Property> </Property>
<Property name='target_id' type='string'> <Property name='target_id' type='string'>
(text) 知识库ID必填 (text) 知识库 ID必填
</Property> </Property>
</Properties> </Properties>
</Col> </Col>
@ -2621,10 +2621,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
### Request Body ### Request Body
<Properties> <Properties>
<Property name='tag_id' type='string'> <Property name='tag_id' type='string'>
(text) 标签ID必填 (text) 标签 ID必填
</Property> </Property>
<Property name='target_id' type='string'> <Property name='target_id' type='string'>
(text) 知识库ID必填 (text) 知识库 ID必填
</Property> </Property>
</Properties> </Properties>
</Col> </Col>
@ -2664,7 +2664,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
### Path ### Path
<Properties> <Properties>
<Property name='dataset_id' type='string'> <Property name='dataset_id' type='string'>
(text) 知识库ID (text) 知识库 ID
</Property> </Property>
</Properties> </Properties>
</Col> </Col>

@ -1,14 +1,42 @@
import React from 'react' 'use client'
import React, { useEffect, useState } from 'react'
import type { FC } from 'react' import type { FC } from 'react'
import type { Metadata } from 'next' import { usePathname, useSearchParams } from 'next/navigation'
import Loading from '../components/base/loading'
export const metadata: Metadata = { import { useGlobalPublicStore } from '@/context/global-public-context'
icons: 'data:,', // prevent browser from using default favicon import { AccessMode } from '@/models/access-control'
} import { getAppAccessModeByAppCode } from '@/service/share'
const Layout: FC<{ const Layout: FC<{
children: React.ReactNode children: React.ReactNode
}> = ({ children }) => { }> = ({ children }) => {
const isGlobalPending = useGlobalPublicStore(s => s.isGlobalPending)
const setWebAppAccessMode = useGlobalPublicStore(s => s.setWebAppAccessMode)
const pathname = usePathname()
const searchParams = useSearchParams()
const redirectUrl = searchParams.get('redirect_url')
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
(async () => {
let appCode: string | null = null
if (redirectUrl)
appCode = redirectUrl?.split('/').pop() || null
else
appCode = pathname.split('/').pop() || null
if (!appCode)
return
setIsLoading(true)
const ret = await getAppAccessModeByAppCode(appCode)
setWebAppAccessMode(ret?.accessMode || AccessMode.PUBLIC)
setIsLoading(false)
})()
}, [pathname, redirectUrl, setWebAppAccessMode])
if (isLoading || isGlobalPending) {
return <div className='flex h-full w-full items-center justify-center'>
<Loading />
</div>
}
return ( return (
<div className="h-full min-w-[300px] pb-[env(safe-area-inset-bottom)]"> <div className="h-full min-w-[300px] pb-[env(safe-area-inset-bottom)]">
{children} {children}

@ -0,0 +1,96 @@
'use client'
import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { useContext } from 'use-context-selector'
import Countdown from '@/app/components/signin/countdown'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
import { sendWebAppResetPasswordCode, verifyWebAppResetPasswordCode } from '@/service/common'
import I18NContext from '@/context/i18n'
export default function CheckCode() {
const { t } = useTranslation()
const router = useRouter()
const searchParams = useSearchParams()
const email = decodeURIComponent(searchParams.get('email') as string)
const token = decodeURIComponent(searchParams.get('token') as string)
const [code, setVerifyCode] = useState('')
const [loading, setIsLoading] = useState(false)
const { locale } = useContext(I18NContext)
const verify = async () => {
try {
if (!code.trim()) {
Toast.notify({
type: 'error',
message: t('login.checkCode.emptyCode'),
})
return
}
if (!/\d{6}/.test(code)) {
Toast.notify({
type: 'error',
message: t('login.checkCode.invalidCode'),
})
return
}
setIsLoading(true)
const ret = await verifyWebAppResetPasswordCode({ email, code, token })
if (ret.is_valid) {
const params = new URLSearchParams(searchParams)
params.set('token', encodeURIComponent(ret.token))
router.push(`/webapp-reset-password/set-password?${params.toString()}`)
}
}
catch (error) { console.error(error) }
finally {
setIsLoading(false)
}
}
const resendCode = async () => {
try {
const res = await sendWebAppResetPasswordCode(email, locale)
if (res.result === 'success') {
const params = new URLSearchParams(searchParams)
params.set('token', encodeURIComponent(res.data))
router.replace(`/webapp-reset-password/check-code?${params.toString()}`)
}
}
catch (error) { console.error(error) }
}
return <div className='flex flex-col gap-3'>
<div className='inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge text-text-accent-light-mode-only shadow-lg'>
<RiMailSendFill className='h-6 w-6 text-2xl' />
</div>
<div className='pb-4 pt-2'>
<h2 className='title-4xl-semi-bold text-text-primary'>{t('login.checkCode.checkYourEmail')}</h2>
<p className='body-md-regular mt-2 text-text-secondary'>
<span dangerouslySetInnerHTML={{ __html: t('login.checkCode.tips', { email }) as string }}></span>
<br />
{t('login.checkCode.validTime')}
</p>
</div>
<form action="">
<input type='text' className='hidden' />
<label htmlFor="code" className='system-md-semibold mb-1 text-text-secondary'>{t('login.checkCode.verificationCode')}</label>
<Input value={code} onChange={e => setVerifyCode(e.target.value)} max-length={6} className='mt-1' placeholder={t('login.checkCode.verificationCodePlaceholder') as string} />
<Button loading={loading} disabled={loading} className='my-3 w-full' variant='primary' onClick={verify}>{t('login.checkCode.verify')}</Button>
<Countdown onResend={resendCode} />
</form>
<div className='py-2'>
<div className='h-px bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div>
</div>
<div onClick={() => router.back()} className='flex h-9 cursor-pointer items-center justify-center text-text-tertiary'>
<div className='bg-background-default-dimm inline-block rounded-full p-1'>
<RiArrowLeftLine size={12} />
</div>
<span className='system-xs-regular ml-2'>{t('login.back')}</span>
</div>
</div>
}

@ -0,0 +1,30 @@
'use client'
import Header from '@/app/signin/_header'
import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
export default function SignInLayout({ children }: any) {
const { systemFeatures } = useGlobalPublicStore()
return <>
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
<Header />
<div className={
cn(
'flex w-full grow flex-col items-center justify-center',
'px-6',
'md:px-[108px]',
)
}>
<div className='flex w-[400px] flex-col'>
{children}
</div>
</div>
{!systemFeatures.branding.enabled && <div className='system-xs-regular px-8 py-6 text-text-tertiary'>
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div>}
</div>
</div>
</>
}

@ -0,0 +1,104 @@
'use client'
import Link from 'next/link'
import { RiArrowLeftLine, RiLockPasswordLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { useContext } from 'use-context-selector'
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
import { emailRegex } from '@/config'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
import { sendResetPasswordCode } from '@/service/common'
import I18NContext from '@/context/i18n'
import { noop } from 'lodash-es'
import useDocumentTitle from '@/hooks/use-document-title'
export default function CheckCode() {
const { t } = useTranslation()
useDocumentTitle('')
const searchParams = useSearchParams()
const router = useRouter()
const [email, setEmail] = useState('')
const [loading, setIsLoading] = useState(false)
const { locale } = useContext(I18NContext)
const handleGetEMailVerificationCode = async () => {
try {
if (!email) {
Toast.notify({ type: 'error', message: t('login.error.emailEmpty') })
return
}
if (!emailRegex.test(email)) {
Toast.notify({
type: 'error',
message: t('login.error.emailInValid'),
})
return
}
setIsLoading(true)
const res = await sendResetPasswordCode(email, locale)
if (res.result === 'success') {
localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`)
const params = new URLSearchParams(searchParams)
params.set('token', encodeURIComponent(res.data))
params.set('email', encodeURIComponent(email))
router.push(`/webapp-reset-password/check-code?${params.toString()}`)
}
else if (res.code === 'account_not_found') {
Toast.notify({
type: 'error',
message: t('login.error.registrationNotAllowed'),
})
}
else {
Toast.notify({
type: 'error',
message: res.data,
})
}
}
catch (error) {
console.error(error)
}
finally {
setIsLoading(false)
}
}
return <div className='flex flex-col gap-3'>
<div className='inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge shadow-lg'>
<RiLockPasswordLine className='h-6 w-6 text-2xl text-text-accent-light-mode-only' />
</div>
<div className='pb-4 pt-2'>
<h2 className='title-4xl-semi-bold text-text-primary'>{t('login.resetPassword')}</h2>
<p className='body-md-regular mt-2 text-text-secondary'>
{t('login.resetPasswordDesc')}
</p>
</div>
<form onSubmit={noop}>
<input type='text' className='hidden' />
<div className='mb-2'>
<label htmlFor="email" className='system-md-semibold my-2 text-text-secondary'>{t('login.email')}</label>
<div className='mt-1'>
<Input id='email' type="email" disabled={loading} value={email} placeholder={t('login.emailPlaceholder') as string} onChange={e => setEmail(e.target.value)} />
</div>
<div className='mt-3'>
<Button loading={loading} disabled={loading} variant='primary' className='w-full' onClick={handleGetEMailVerificationCode}>{t('login.sendVerificationCode')}</Button>
</div>
</div>
</form>
<div className='py-2'>
<div className='h-px bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div>
</div>
<Link href={`/webapp-signin?${searchParams.toString()}`} className='flex h-9 items-center justify-center text-text-tertiary hover:text-text-primary'>
<div className='inline-block rounded-full bg-background-default-dimmed p-1'>
<RiArrowLeftLine size={12} />
</div>
<span className='system-xs-regular ml-2'>{t('login.backToLogin')}</span>
</Link>
</div>
}

@ -0,0 +1,188 @@
'use client'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useRouter, useSearchParams } from 'next/navigation'
import cn from 'classnames'
import { RiCheckboxCircleFill } from '@remixicon/react'
import { useCountDown } from 'ahooks'
import Button from '@/app/components/base/button'
import { changeWebAppPasswordWithToken } from '@/service/common'
import Toast from '@/app/components/base/toast'
import Input from '@/app/components/base/input'
const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
const ChangePasswordForm = () => {
const { t } = useTranslation()
const router = useRouter()
const searchParams = useSearchParams()
const token = decodeURIComponent(searchParams.get('token') || '')
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [showSuccess, setShowSuccess] = useState(false)
const [showPassword, setShowPassword] = useState(false)
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
const showErrorMessage = useCallback((message: string) => {
Toast.notify({
type: 'error',
message,
})
}, [])
const getSignInUrl = () => {
return `/webapp-signin?redirect_url=${searchParams.get('redirect_url') || ''}`
}
const AUTO_REDIRECT_TIME = 5000
const [leftTime, setLeftTime] = useState<number | undefined>(undefined)
const [countdown] = useCountDown({
leftTime,
onEnd: () => {
router.replace(getSignInUrl())
},
})
const valid = useCallback(() => {
if (!password.trim()) {
showErrorMessage(t('login.error.passwordEmpty'))
return false
}
if (!validPassword.test(password)) {
showErrorMessage(t('login.error.passwordInvalid'))
return false
}
if (password !== confirmPassword) {
showErrorMessage(t('common.account.notEqual'))
return false
}
return true
}, [password, confirmPassword, showErrorMessage, t])
const handleChangePassword = useCallback(async () => {
if (!valid())
return
try {
await changeWebAppPasswordWithToken({
url: '/forgot-password/resets',
body: {
token,
new_password: password,
password_confirm: confirmPassword,
},
})
setShowSuccess(true)
setLeftTime(AUTO_REDIRECT_TIME)
}
catch (error) {
console.error(error)
}
}, [password, token, valid, confirmPassword])
return (
<div className={
cn(
'flex w-full grow flex-col items-center justify-center',
'px-6',
'md:px-[108px]',
)
}>
{!showSuccess && (
<div className='flex flex-col md:w-[400px]'>
<div className="mx-auto w-full">
<h2 className="title-4xl-semi-bold text-text-primary">
{t('login.changePassword')}
</h2>
<p className='body-md-regular mt-2 text-text-secondary'>
{t('login.changePasswordTip')}
</p>
</div>
<div className="mx-auto mt-6 w-full">
<div className="bg-white">
{/* Password */}
<div className='mb-5'>
<label htmlFor="password" className="system-md-semibold my-2 text-text-secondary">
{t('common.account.newPassword')}
</label>
<div className='relative mt-1'>
<Input
id="password" type={showPassword ? 'text' : 'password'}
value={password}
onChange={e => setPassword(e.target.value)}
placeholder={t('login.passwordPlaceholder') || ''}
/>
<div className="absolute inset-y-0 right-0 flex items-center">
<Button
type="button"
variant='ghost'
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? '👀' : '😝'}
</Button>
</div>
</div>
<div className='body-xs-regular mt-1 text-text-secondary'>{t('login.error.passwordInvalid')}</div>
</div>
{/* Confirm Password */}
<div className='mb-5'>
<label htmlFor="confirmPassword" className="system-md-semibold my-2 text-text-secondary">
{t('common.account.confirmPassword')}
</label>
<div className='relative mt-1'>
<Input
id="confirmPassword"
type={showConfirmPassword ? 'text' : 'password'}
value={confirmPassword}
onChange={e => setConfirmPassword(e.target.value)}
placeholder={t('login.confirmPasswordPlaceholder') || ''}
/>
<div className="absolute inset-y-0 right-0 flex items-center">
<Button
type="button"
variant='ghost'
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
>
{showConfirmPassword ? '👀' : '😝'}
</Button>
</div>
</div>
</div>
<div>
<Button
variant='primary'
className='w-full'
onClick={handleChangePassword}
>
{t('login.changePasswordBtn')}
</Button>
</div>
</div>
</div>
</div>
)}
{showSuccess && (
<div className="flex flex-col md:w-[400px]">
<div className="mx-auto w-full">
<div className="mb-3 flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle font-bold shadow-lg">
<RiCheckboxCircleFill className='h-6 w-6 text-text-success' />
</div>
<h2 className="title-4xl-semi-bold text-text-primary">
{t('login.passwordChangedTip')}
</h2>
</div>
<div className="mx-auto mt-6 w-full">
<Button variant='primary' className='w-full' onClick={() => {
setLeftTime(undefined)
router.replace(getSignInUrl())
}}>{t('login.passwordChanged')} ({Math.round(countdown / 1000)}) </Button>
</div>
</div>
)}
</div>
)
}
export default ChangePasswordForm

@ -0,0 +1,115 @@
'use client'
import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useCallback, useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { useContext } from 'use-context-selector'
import Countdown from '@/app/components/signin/countdown'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
import { sendWebAppEMailLoginCode, webAppEmailLoginWithCode } from '@/service/common'
import I18NContext from '@/context/i18n'
import { setAccessToken } from '@/app/components/share/utils'
import { fetchAccessToken } from '@/service/share'
export default function CheckCode() {
const { t } = useTranslation()
const router = useRouter()
const searchParams = useSearchParams()
const email = decodeURIComponent(searchParams.get('email') as string)
const token = decodeURIComponent(searchParams.get('token') as string)
const [code, setVerifyCode] = useState('')
const [loading, setIsLoading] = useState(false)
const { locale } = useContext(I18NContext)
const redirectUrl = searchParams.get('redirect_url')
const getAppCodeFromRedirectUrl = useCallback(() => {
const appCode = redirectUrl?.split('/').pop()
if (!appCode)
return null
return appCode
}, [redirectUrl])
const verify = async () => {
try {
const appCode = getAppCodeFromRedirectUrl()
if (!code.trim()) {
Toast.notify({
type: 'error',
message: t('login.checkCode.emptyCode'),
})
return
}
if (!/\d{6}/.test(code)) {
Toast.notify({
type: 'error',
message: t('login.checkCode.invalidCode'),
})
return
}
if (!redirectUrl || !appCode) {
Toast.notify({
type: 'error',
message: t('login.error.redirectUrlMissing'),
})
return
}
setIsLoading(true)
const ret = await webAppEmailLoginWithCode({ email, code, token })
if (ret.result === 'success') {
localStorage.setItem('webapp_access_token', ret.data.access_token)
const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: ret.data.access_token })
await setAccessToken(appCode, tokenResp.access_token)
router.replace(redirectUrl)
}
}
catch (error) { console.error(error) }
finally {
setIsLoading(false)
}
}
const resendCode = async () => {
try {
const ret = await sendWebAppEMailLoginCode(email, locale)
if (ret.result === 'success') {
const params = new URLSearchParams(searchParams)
params.set('token', encodeURIComponent(ret.data))
router.replace(`/webapp-signin/check-code?${params.toString()}`)
}
}
catch (error) { console.error(error) }
}
return <div className='flex w-[400px] flex-col gap-3'>
<div className='inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge shadow-lg'>
<RiMailSendFill className='h-6 w-6 text-2xl text-text-accent-light-mode-only' />
</div>
<div className='pb-4 pt-2'>
<h2 className='title-4xl-semi-bold text-text-primary'>{t('login.checkCode.checkYourEmail')}</h2>
<p className='body-md-regular mt-2 text-text-secondary'>
<span dangerouslySetInnerHTML={{ __html: t('login.checkCode.tips', { email }) as string }}></span>
<br />
{t('login.checkCode.validTime')}
</p>
</div>
<form action="">
<label htmlFor="code" className='system-md-semibold mb-1 text-text-secondary'>{t('login.checkCode.verificationCode')}</label>
<Input value={code} onChange={e => setVerifyCode(e.target.value)} max-length={6} className='mt-1' placeholder={t('login.checkCode.verificationCodePlaceholder') as string} />
<Button loading={loading} disabled={loading} className='my-3 w-full' variant='primary' onClick={verify}>{t('login.checkCode.verify')}</Button>
<Countdown onResend={resendCode} />
</form>
<div className='py-2'>
<div className='h-px bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div>
</div>
<div onClick={() => router.back()} className='flex h-9 cursor-pointer items-center justify-center text-text-tertiary'>
<div className='bg-background-default-dimm inline-block rounded-full p-1'>
<RiArrowLeftLine size={12} />
</div>
<span className='system-xs-regular ml-2'>{t('login.back')}</span>
</div>
</div>
}

@ -0,0 +1,80 @@
'use client'
import { useRouter, useSearchParams } from 'next/navigation'
import React, { useCallback, useEffect } from 'react'
import Toast from '@/app/components/base/toast'
import { fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { SSOProtocol } from '@/types/feature'
import Loading from '@/app/components/base/loading'
import AppUnavailable from '@/app/components/base/app-unavailable'
const ExternalMemberSSOAuth = () => {
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const searchParams = useSearchParams()
const router = useRouter()
const redirectUrl = searchParams.get('redirect_url')
const showErrorToast = (message: string) => {
Toast.notify({
type: 'error',
message,
})
}
const getAppCodeFromRedirectUrl = useCallback(() => {
const appCode = redirectUrl?.split('/').pop()
if (!appCode)
return null
return appCode
}, [redirectUrl])
const handleSSOLogin = useCallback(async () => {
const appCode = getAppCodeFromRedirectUrl()
if (!appCode || !redirectUrl) {
showErrorToast('redirect url or app code is invalid.')
return
}
switch (systemFeatures.webapp_auth.sso_config.protocol) {
case SSOProtocol.SAML: {
const samlRes = await fetchWebSAMLSSOUrl(appCode, redirectUrl)
router.push(samlRes.url)
break
}
case SSOProtocol.OIDC: {
const oidcRes = await fetchWebOIDCSSOUrl(appCode, redirectUrl)
router.push(oidcRes.url)
break
}
case SSOProtocol.OAuth2: {
const oauth2Res = await fetchWebOAuth2SSOUrl(appCode, redirectUrl)
router.push(oauth2Res.url)
break
}
case '':
break
default:
showErrorToast('SSO protocol is not supported.')
}
}, [getAppCodeFromRedirectUrl, redirectUrl, router, systemFeatures.webapp_auth.sso_config.protocol])
useEffect(() => {
handleSSOLogin()
}, [handleSSOLogin])
if (!systemFeatures.webapp_auth.sso_config.protocol) {
return <div className="flex h-full items-center justify-center">
<AppUnavailable code={403} unknownReason='sso protocol is invalid.' />
</div>
}
return (
<div className="flex h-full items-center justify-center">
<Loading />
</div>
)
}
export default React.memo(ExternalMemberSSOAuth)

@ -0,0 +1,68 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useRouter, useSearchParams } from 'next/navigation'
import { useContext } from 'use-context-selector'
import Input from '@/app/components/base/input'
import Button from '@/app/components/base/button'
import { emailRegex } from '@/config'
import Toast from '@/app/components/base/toast'
import { sendWebAppEMailLoginCode } from '@/service/common'
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
import I18NContext from '@/context/i18n'
import { noop } from 'lodash-es'
export default function MailAndCodeAuth() {
const { t } = useTranslation()
const router = useRouter()
const searchParams = useSearchParams()
const emailFromLink = decodeURIComponent(searchParams.get('email') || '')
const [email, setEmail] = useState(emailFromLink)
const [loading, setIsLoading] = useState(false)
const { locale } = useContext(I18NContext)
const handleGetEMailVerificationCode = async () => {
try {
if (!email) {
Toast.notify({ type: 'error', message: t('login.error.emailEmpty') })
return
}
if (!emailRegex.test(email)) {
Toast.notify({
type: 'error',
message: t('login.error.emailInValid'),
})
return
}
setIsLoading(true)
const ret = await sendWebAppEMailLoginCode(email, locale)
if (ret.result === 'success') {
localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`)
const params = new URLSearchParams(searchParams)
params.set('email', encodeURIComponent(email))
params.set('token', encodeURIComponent(ret.data))
router.push(`/webapp-signin/check-code?${params.toString()}`)
}
}
catch (error) {
console.error(error)
}
finally {
setIsLoading(false)
}
}
return (<form onSubmit={noop}>
<input type='text' className='hidden' />
<div className='mb-2'>
<label htmlFor="email" className='system-md-semibold my-2 text-text-secondary'>{t('login.email')}</label>
<div className='mt-1'>
<Input id='email' type="email" value={email} placeholder={t('login.emailPlaceholder') as string} onChange={e => setEmail(e.target.value)} />
</div>
<div className='mt-3'>
<Button loading={loading} disabled={loading || !email} variant='primary' className='w-full' onClick={handleGetEMailVerificationCode}>{t('login.continueWithCode')}</Button>
</div>
</div>
</form>
)
}

@ -0,0 +1,171 @@
import Link from 'next/link'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useRouter, useSearchParams } from 'next/navigation'
import { useContext } from 'use-context-selector'
import Button from '@/app/components/base/button'
import Toast from '@/app/components/base/toast'
import { emailRegex } from '@/config'
import { webAppLogin } from '@/service/common'
import Input from '@/app/components/base/input'
import I18NContext from '@/context/i18n'
import { noop } from 'lodash-es'
import { setAccessToken } from '@/app/components/share/utils'
import { fetchAccessToken } from '@/service/share'
type MailAndPasswordAuthProps = {
isEmailSetup: boolean
}
const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAuthProps) {
const { t } = useTranslation()
const { locale } = useContext(I18NContext)
const router = useRouter()
const searchParams = useSearchParams()
const [showPassword, setShowPassword] = useState(false)
const emailFromLink = decodeURIComponent(searchParams.get('email') || '')
const [email, setEmail] = useState(emailFromLink)
const [password, setPassword] = useState('')
const [isLoading, setIsLoading] = useState(false)
const redirectUrl = searchParams.get('redirect_url')
const getAppCodeFromRedirectUrl = useCallback(() => {
const appCode = redirectUrl?.split('/').pop()
if (!appCode)
return null
return appCode
}, [redirectUrl])
const handleEmailPasswordLogin = async () => {
const appCode = getAppCodeFromRedirectUrl()
if (!email) {
Toast.notify({ type: 'error', message: t('login.error.emailEmpty') })
return
}
if (!emailRegex.test(email)) {
Toast.notify({
type: 'error',
message: t('login.error.emailInValid'),
})
return
}
if (!password?.trim()) {
Toast.notify({ type: 'error', message: t('login.error.passwordEmpty') })
return
}
if (!passwordRegex.test(password)) {
Toast.notify({
type: 'error',
message: t('login.error.passwordInvalid'),
})
return
}
if (!redirectUrl || !appCode) {
Toast.notify({
type: 'error',
message: t('login.error.redirectUrlMissing'),
})
return
}
try {
setIsLoading(true)
const loginData: Record<string, any> = {
email,
password,
language: locale,
remember_me: true,
}
const res = await webAppLogin({
url: '/login',
body: loginData,
})
if (res.result === 'success') {
localStorage.setItem('webapp_access_token', res.data.access_token)
const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: res.data.access_token })
await setAccessToken(appCode, tokenResp.access_token)
router.replace(redirectUrl)
}
else {
Toast.notify({
type: 'error',
message: res.data,
})
}
}
finally {
setIsLoading(false)
}
}
return <form onSubmit={noop}>
<div className='mb-3'>
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">
{t('login.email')}
</label>
<div className="mt-1">
<Input
value={email}
onChange={e => setEmail(e.target.value)}
id="email"
type="email"
autoComplete="email"
placeholder={t('login.emailPlaceholder') || ''}
tabIndex={1}
/>
</div>
</div>
<div className='mb-3'>
<label htmlFor="password" className="my-2 flex items-center justify-between">
<span className='system-md-semibold text-text-secondary'>{t('login.password')}</span>
<Link
href={`/webapp-reset-password?${searchParams.toString()}`}
className={`system-xs-regular ${isEmailSetup ? 'text-components-button-secondary-accent-text' : 'pointer-events-none text-components-button-secondary-accent-text-disabled'}`}
tabIndex={isEmailSetup ? 0 : -1}
aria-disabled={!isEmailSetup}
>
{t('login.forget')}
</Link>
</label>
<div className="relative mt-1">
<Input
id="password"
value={password}
onChange={e => setPassword(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter')
handleEmailPasswordLogin()
}}
type={showPassword ? 'text' : 'password'}
autoComplete="current-password"
placeholder={t('login.passwordPlaceholder') || ''}
tabIndex={2}
/>
<div className="absolute inset-y-0 right-0 flex items-center">
<Button
type="button"
variant='ghost'
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? '👀' : '😝'}
</Button>
</div>
</div>
</div>
<div className='mb-2'>
<Button
tabIndex={2}
variant='primary'
onClick={handleEmailPasswordLogin}
disabled={isLoading || !email || !password}
className="w-full"
>{t('login.signBtn')}</Button>
</div>
</form>
}

@ -0,0 +1,88 @@
'use client'
import { useRouter, useSearchParams } from 'next/navigation'
import type { FC } from 'react'
import { useCallback } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
import Toast from '@/app/components/base/toast'
import Button from '@/app/components/base/button'
import { SSOProtocol } from '@/types/feature'
import { fetchMembersOAuth2SSOUrl, fetchMembersOIDCSSOUrl, fetchMembersSAMLSSOUrl } from '@/service/share'
type SSOAuthProps = {
protocol: SSOProtocol | ''
}
const SSOAuth: FC<SSOAuthProps> = ({
protocol,
}) => {
const router = useRouter()
const { t } = useTranslation()
const searchParams = useSearchParams()
const redirectUrl = searchParams.get('redirect_url')
const getAppCodeFromRedirectUrl = useCallback(() => {
const appCode = redirectUrl?.split('/').pop()
if (!appCode)
return null
return appCode
}, [redirectUrl])
const [isLoading, setIsLoading] = useState(false)
const handleSSOLogin = () => {
const appCode = getAppCodeFromRedirectUrl()
if (!redirectUrl || !appCode) {
Toast.notify({
type: 'error',
message: 'invalid redirect URL or app code',
})
return
}
setIsLoading(true)
if (protocol === SSOProtocol.SAML) {
fetchMembersSAMLSSOUrl(appCode, redirectUrl).then((res) => {
router.push(res.url)
}).finally(() => {
setIsLoading(false)
})
}
else if (protocol === SSOProtocol.OIDC) {
fetchMembersOIDCSSOUrl(appCode, redirectUrl).then((res) => {
router.push(res.url)
}).finally(() => {
setIsLoading(false)
})
}
else if (protocol === SSOProtocol.OAuth2) {
fetchMembersOAuth2SSOUrl(appCode, redirectUrl).then((res) => {
router.push(res.url)
}).finally(() => {
setIsLoading(false)
})
}
else {
Toast.notify({
type: 'error',
message: 'invalid SSO protocol',
})
setIsLoading(false)
}
}
return (
<Button
tabIndex={0}
onClick={() => { handleSSOLogin() }}
disabled={isLoading}
className="w-full"
>
<Lock01 className='mr-2 h-5 w-5 text-text-accent-light-mode-only' />
<span className="truncate">{t('login.withSSO')}</span>
</Button>
)
}
export default SSOAuth

@ -0,0 +1,25 @@
'use client'
import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title'
export default function SignInLayout({ children }: any) {
const { systemFeatures } = useGlobalPublicStore()
useDocumentTitle('')
return <>
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
{/* <Header /> */}
<div className={cn('flex w-full grow flex-col items-center justify-center px-6 md:px-[108px]')}>
<div className='flex justify-center md:w-[440px] lg:w-[600px]'>
{children}
</div>
</div>
{systemFeatures.branding.enabled === false && <div className='system-xs-regular px-8 py-6 text-text-tertiary'>
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div>}
</div>
</div>
</>
}

@ -0,0 +1,176 @@
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Link from 'next/link'
import { RiContractLine, RiDoorLockLine, RiErrorWarningFill } from '@remixicon/react'
import Loading from '@/app/components/base/loading'
import MailAndCodeAuth from './components/mail-and-code-auth'
import MailAndPasswordAuth from './components/mail-and-password-auth'
import SSOAuth from './components/sso-auth'
import cn from '@/utils/classnames'
import { LicenseStatus } from '@/types/feature'
import { IS_CE_EDITION } from '@/config'
import { useGlobalPublicStore } from '@/context/global-public-context'
const NormalForm = () => {
const { t } = useTranslation()
const [isLoading, setIsLoading] = useState(true)
const { systemFeatures } = useGlobalPublicStore()
const [authType, updateAuthType] = useState<'code' | 'password'>('password')
const [showORLine, setShowORLine] = useState(false)
const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false)
const init = useCallback(async () => {
try {
setAllMethodsAreDisabled(!systemFeatures.enable_social_oauth_login && !systemFeatures.enable_email_code_login && !systemFeatures.enable_email_password_login && !systemFeatures.sso_enforced_for_signin)
setShowORLine((systemFeatures.enable_social_oauth_login || systemFeatures.sso_enforced_for_signin) && (systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login))
updateAuthType(systemFeatures.enable_email_password_login ? 'password' : 'code')
}
catch (error) {
console.error(error)
setAllMethodsAreDisabled(true)
}
finally { setIsLoading(false) }
}, [systemFeatures])
useEffect(() => {
init()
}, [init])
if (isLoading) {
return <div className={
cn(
'flex w-full grow flex-col items-center justify-center',
'px-6',
'md:px-[108px]',
)
}>
<Loading type='area' />
</div>
}
if (systemFeatures.license?.status === LicenseStatus.LOST) {
return <div className='mx-auto mt-8 w-full'>
<div className='relative'>
<div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4">
<div className='shadows-shadow-lg relative mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow'>
<RiContractLine className='h-5 w-5' />
<RiErrorWarningFill className='absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary' />
</div>
<p className='system-sm-medium text-text-primary'>{t('login.licenseLost')}</p>
<p className='system-xs-regular mt-1 text-text-tertiary'>{t('login.licenseLostTip')}</p>
</div>
</div>
</div>
}
if (systemFeatures.license?.status === LicenseStatus.EXPIRED) {
return <div className='mx-auto mt-8 w-full'>
<div className='relative'>
<div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4">
<div className='shadows-shadow-lg relative mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow'>
<RiContractLine className='h-5 w-5' />
<RiErrorWarningFill className='absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary' />
</div>
<p className='system-sm-medium text-text-primary'>{t('login.licenseExpired')}</p>
<p className='system-xs-regular mt-1 text-text-tertiary'>{t('login.licenseExpiredTip')}</p>
</div>
</div>
</div>
}
if (systemFeatures.license?.status === LicenseStatus.INACTIVE) {
return <div className='mx-auto mt-8 w-full'>
<div className='relative'>
<div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4">
<div className='shadows-shadow-lg relative mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow'>
<RiContractLine className='h-5 w-5' />
<RiErrorWarningFill className='absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary' />
</div>
<p className='system-sm-medium text-text-primary'>{t('login.licenseInactive')}</p>
<p className='system-xs-regular mt-1 text-text-tertiary'>{t('login.licenseInactiveTip')}</p>
</div>
</div>
</div>
}
return (
<>
<div className="mx-auto mt-8 w-full">
<div className="mx-auto w-full">
<h2 className="title-4xl-semi-bold text-text-primary">{t('login.pageTitle')}</h2>
{!systemFeatures.branding.enabled && <p className='body-md-regular mt-2 text-text-tertiary'>{t('login.welcome')}</p>}
</div>
<div className="relative">
<div className="mt-6 flex flex-col gap-3">
{systemFeatures.sso_enforced_for_signin && <div className='w-full'>
<SSOAuth protocol={systemFeatures.sso_enforced_for_signin_protocol} />
</div>}
</div>
{showORLine && <div className="relative mt-6">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className='h-px w-full bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div>
</div>
<div className="relative flex justify-center">
<span className="system-xs-medium-uppercase px-2 text-text-tertiary">{t('login.or')}</span>
</div>
</div>}
{
(systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login) && <>
{systemFeatures.enable_email_code_login && authType === 'code' && <>
<MailAndCodeAuth />
{systemFeatures.enable_email_password_login && <div className='cursor-pointer py-1 text-center' onClick={() => { updateAuthType('password') }}>
<span className='system-xs-medium text-components-button-secondary-accent-text'>{t('login.usePassword')}</span>
</div>}
</>}
{systemFeatures.enable_email_password_login && authType === 'password' && <>
<MailAndPasswordAuth isEmailSetup={systemFeatures.is_email_setup} />
{systemFeatures.enable_email_code_login && <div className='cursor-pointer py-1 text-center' onClick={() => { updateAuthType('code') }}>
<span className='system-xs-medium text-components-button-secondary-accent-text'>{t('login.useVerificationCode')}</span>
</div>}
</>}
</>
}
{allMethodsAreDisabled && <>
<div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4">
<div className='shadows-shadow-lg mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow'>
<RiDoorLockLine className='h-5 w-5' />
</div>
<p className='system-sm-medium text-text-primary'>{t('login.noLoginMethod')}</p>
<p className='system-xs-regular mt-1 text-text-tertiary'>{t('login.noLoginMethodTip')}</p>
</div>
<div className="relative my-2 py-2">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className='h-px w-full bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div>
</div>
</div>
</>}
{!systemFeatures.branding.enabled && <>
<div className="system-xs-regular mt-2 block w-full text-text-tertiary">
{t('login.tosDesc')}
&nbsp;
<Link
className='system-xs-medium text-text-secondary hover:underline'
target='_blank' rel='noopener noreferrer'
href='https://dify.ai/terms'
>{t('login.tos')}</Link>
&nbsp;&&nbsp;
<Link
className='system-xs-medium text-text-secondary hover:underline'
target='_blank' rel='noopener noreferrer'
href='https://dify.ai/privacy'
>{t('login.pp')}</Link>
</div>
{IS_CE_EDITION && <div className="w-hull system-xs-regular mt-2 block text-text-tertiary">
{t('login.goToInit')}
&nbsp;
<Link
className='system-xs-medium text-text-secondary hover:underline'
href='/install'
>{t('login.setAdminAccount')}</Link>
</div>}
</>}
</div>
</div>
</>
)
}
export default NormalForm

@ -3,19 +3,20 @@ import { useRouter, useSearchParams } from 'next/navigation'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useCallback, useEffect } from 'react' import React, { useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RiDoorLockLine } from '@remixicon/react'
import cn from '@/utils/classnames'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import { fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share' import { removeAccessToken, setAccessToken } from '@/app/components/share/utils'
import { setAccessToken } from '@/app/components/share/utils'
import { useGlobalPublicStore } from '@/context/global-public-context' import { useGlobalPublicStore } from '@/context/global-public-context'
import { SSOProtocol } from '@/types/feature'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import AppUnavailable from '@/app/components/base/app-unavailable' import AppUnavailable from '@/app/components/base/app-unavailable'
import NormalForm from './normalForm'
import { AccessMode } from '@/models/access-control'
import ExternalMemberSsoAuth from './components/external-member-sso-auth'
import { fetchAccessToken } from '@/service/share'
const WebSSOForm: FC = () => { const WebSSOForm: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode)
const searchParams = useSearchParams() const searchParams = useSearchParams()
const router = useRouter() const router = useRouter()
@ -23,10 +24,22 @@ const WebSSOForm: FC = () => {
const tokenFromUrl = searchParams.get('web_sso_token') const tokenFromUrl = searchParams.get('web_sso_token')
const message = searchParams.get('message') const message = searchParams.get('message')
const showErrorToast = (message: string) => { const getSigninUrl = useCallback(() => {
const params = new URLSearchParams(searchParams)
params.delete('message')
return `/webapp-signin?${params.toString()}`
}, [searchParams])
const backToHome = useCallback(() => {
removeAccessToken()
const url = getSigninUrl()
router.replace(url)
}, [getSigninUrl, router])
const showErrorToast = (msg: string) => {
Toast.notify({ Toast.notify({
type: 'error', type: 'error',
message, message: msg,
}) })
} }
@ -38,102 +51,73 @@ const WebSSOForm: FC = () => {
return appCode return appCode
}, [redirectUrl]) }, [redirectUrl])
const processTokenAndRedirect = useCallback(async () => {
const appCode = getAppCodeFromRedirectUrl()
if (!appCode || !tokenFromUrl || !redirectUrl) {
showErrorToast('redirect url or app code or token is invalid.')
return
}
await setAccessToken(appCode, tokenFromUrl)
router.push(redirectUrl)
}, [getAppCodeFromRedirectUrl, redirectUrl, router, tokenFromUrl])
const handleSSOLogin = useCallback(async () => {
const appCode = getAppCodeFromRedirectUrl()
if (!appCode || !redirectUrl) {
showErrorToast('redirect url or app code is invalid.')
return
}
switch (systemFeatures.webapp_auth.sso_config.protocol) {
case SSOProtocol.SAML: {
const samlRes = await fetchWebSAMLSSOUrl(appCode, redirectUrl)
router.push(samlRes.url)
break
}
case SSOProtocol.OIDC: {
const oidcRes = await fetchWebOIDCSSOUrl(appCode, redirectUrl)
router.push(oidcRes.url)
break
}
case SSOProtocol.OAuth2: {
const oauth2Res = await fetchWebOAuth2SSOUrl(appCode, redirectUrl)
router.push(oauth2Res.url)
break
}
case '':
break
default:
showErrorToast('SSO protocol is not supported.')
}
}, [getAppCodeFromRedirectUrl, redirectUrl, router, systemFeatures.webapp_auth.sso_config.protocol])
useEffect(() => { useEffect(() => {
const init = async () => { (async () => {
if (message) { if (message)
showErrorToast(message)
return return
}
if (!tokenFromUrl) { const appCode = getAppCodeFromRedirectUrl()
await handleSSOLogin() if (appCode && tokenFromUrl && redirectUrl) {
localStorage.setItem('webapp_access_token', tokenFromUrl)
const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: tokenFromUrl })
await setAccessToken(appCode, tokenResp.access_token)
router.replace(redirectUrl)
return return
} }
if (appCode && redirectUrl && localStorage.getItem('webapp_access_token')) {
const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: localStorage.getItem('webapp_access_token') })
await setAccessToken(appCode, tokenResp.access_token)
router.replace(redirectUrl)
}
})()
}, [getAppCodeFromRedirectUrl, redirectUrl, router, tokenFromUrl, message])
await processTokenAndRedirect() useEffect(() => {
} if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC && redirectUrl)
router.replace(redirectUrl)
}, [webAppAccessMode, router, redirectUrl])
init() if (tokenFromUrl) {
}, [message, processTokenAndRedirect, tokenFromUrl, handleSSOLogin])
if (tokenFromUrl)
return <div className='flex h-full items-center justify-center'><Loading /></div>
if (message) {
return <div className='flex h-full items-center justify-center'> return <div className='flex h-full items-center justify-center'>
<AppUnavailable code={'App Unavailable'} unknownReason={message} /> <Loading />
</div> </div>
} }
if (systemFeatures.webapp_auth.enabled) { if (message) {
if (systemFeatures.webapp_auth.allow_sso) { return <div className='flex h-full flex-col items-center justify-center gap-y-4'>
return ( <AppUnavailable className='h-auto w-auto' code={t('share.common.appUnavailable')} unknownReason={message} />
<div className="flex h-full items-center justify-center"> <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('share.login.backToHome')}</span>
<div className={cn('flex w-full grow flex-col items-center justify-center', 'px-6', 'md:px-[108px]')}> </div>
<Loading /> }
</div> if (!redirectUrl) {
</div> showErrorToast('redirect url is invalid.')
) return <div className='flex h-full items-center justify-center'>
} <AppUnavailable code={t('share.common.appUnavailable')} unknownReason='redirect url is invalid.' />
return <div className="flex h-full items-center justify-center"> </div>
<div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4"> }
<div className='shadows-shadow-lg mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow'> if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC) {
<RiDoorLockLine className='h-5 w-5' /> return <div className='flex h-full items-center justify-center'>
</div> <Loading />
<p className='system-sm-medium text-text-primary'>{t('login.webapp.noLoginMethod')}</p>
<p className='system-xs-regular mt-1 text-text-tertiary'>{t('login.webapp.noLoginMethodTip')}</p>
</div>
<div className="relative my-2 py-2">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className='h-px w-full bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div>
</div>
</div>
</div> </div>
} }
else { if (!systemFeatures.webapp_auth.enabled) {
return <div className="flex h-full items-center justify-center"> return <div className="flex h-full items-center justify-center">
<p className='system-xs-regular text-text-tertiary'>{t('login.webapp.disabled')}</p> <p className='system-xs-regular text-text-tertiary'>{t('login.webapp.disabled')}</p>
</div> </div>
} }
if (webAppAccessMode && (webAppAccessMode === AccessMode.ORGANIZATION || webAppAccessMode === AccessMode.SPECIFIC_GROUPS_MEMBERS)) {
return <div className='w-full max-w-[400px]'>
<NormalForm />
</div>
}
if (webAppAccessMode && webAppAccessMode === AccessMode.EXTERNAL_MEMBERS)
return <ExternalMemberSsoAuth />
return <div className='flex h-full flex-col items-center justify-center gap-y-4'>
<AppUnavailable className='h-auto w-auto' isUnknownReason={true} />
<span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('share.login.backToHome')}</span>
</div>
} }
export default React.memo(WebSSOForm) export default React.memo(WebSSOForm)

@ -1,6 +1,6 @@
'use client' 'use client'
import { Dialog } from '@headlessui/react' import { Description as DialogDescription, DialogTitle } from '@headlessui/react'
import { RiBuildingLine, RiGlobalLine } from '@remixicon/react' import { RiBuildingLine, RiGlobalLine, RiVerifiedBadgeLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useCallback, useEffect } from 'react' import { useCallback, useEffect } from 'react'
import Button from '../../base/button' import Button from '../../base/button'
@ -67,8 +67,8 @@ export default function AccessControl(props: AccessControlProps) {
return <AccessControlDialog show onClose={onClose}> return <AccessControlDialog show onClose={onClose}>
<div className='flex flex-col gap-y-3'> <div className='flex flex-col gap-y-3'>
<div className='pb-3 pl-6 pr-14 pt-6'> <div className='pb-3 pl-6 pr-14 pt-6'>
<Dialog.Title className='title-2xl-semi-bold text-text-primary'>{t('app.accessControlDialog.title')}</Dialog.Title> <DialogTitle className='title-2xl-semi-bold text-text-primary'>{t('app.accessControlDialog.title')}</DialogTitle>
<Dialog.Description className='system-xs-regular mt-1 text-text-tertiary'>{t('app.accessControlDialog.description')}</Dialog.Description> <DialogDescription className='system-xs-regular mt-1 text-text-tertiary'>{t('app.accessControlDialog.description')}</DialogDescription>
</div> </div>
<div className='flex flex-col gap-y-1 px-6 pb-3'> <div className='flex flex-col gap-y-1 px-6 pb-3'>
<div className='leading-6'> <div className='leading-6'>
@ -80,12 +80,20 @@ export default function AccessControl(props: AccessControlProps) {
<RiBuildingLine className='h-4 w-4 text-text-primary' /> <RiBuildingLine className='h-4 w-4 text-text-primary' />
<p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.organization')}</p> <p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.organization')}</p>
</div> </div>
{!hideTip && <WebAppSSONotEnabledTip />}
</div> </div>
</AccessControlItem> </AccessControlItem>
<AccessControlItem type={AccessMode.SPECIFIC_GROUPS_MEMBERS}> <AccessControlItem type={AccessMode.SPECIFIC_GROUPS_MEMBERS}>
<SpecificGroupsOrMembers /> <SpecificGroupsOrMembers />
</AccessControlItem> </AccessControlItem>
<AccessControlItem type={AccessMode.EXTERNAL_MEMBERS}>
<div className='flex items-center p-3'>
<div className='flex grow items-center gap-x-2'>
<RiVerifiedBadgeLine className='h-4 w-4 text-text-primary' />
<p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.external')}</p>
</div>
{!hideTip && <WebAppSSONotEnabledTip />}
</div>
</AccessControlItem>
<AccessControlItem type={AccessMode.PUBLIC}> <AccessControlItem type={AccessMode.PUBLIC}>
<div className='flex items-center gap-x-2 p-3'> <div className='flex items-center gap-x-2 p-3'>
<RiGlobalLine className='h-4 w-4 text-text-primary' /> <RiGlobalLine className='h-4 w-4 text-text-primary' />

@ -3,12 +3,10 @@ import { RiAlertFill, RiCloseCircleFill, RiLockLine, RiOrganizationChart } from
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useCallback, useEffect } from 'react' import { useCallback, useEffect } from 'react'
import Avatar from '../../base/avatar' import Avatar from '../../base/avatar'
import Divider from '../../base/divider'
import Tooltip from '../../base/tooltip' import Tooltip from '../../base/tooltip'
import Loading from '../../base/loading' import Loading from '../../base/loading'
import useAccessControlStore from '../../../../context/access-control-store' import useAccessControlStore from '../../../../context/access-control-store'
import AddMemberOrGroupDialog from './add-member-or-group-pop' import AddMemberOrGroupDialog from './add-member-or-group-pop'
import { useGlobalPublicStore } from '@/context/global-public-context'
import type { AccessControlAccount, AccessControlGroup } from '@/models/access-control' import type { AccessControlAccount, AccessControlGroup } from '@/models/access-control'
import { AccessMode } from '@/models/access-control' import { AccessMode } from '@/models/access-control'
import { useAppWhiteListSubjects } from '@/service/access-control' import { useAppWhiteListSubjects } from '@/service/access-control'
@ -19,11 +17,6 @@ export default function SpecificGroupsOrMembers() {
const setSpecificGroups = useAccessControlStore(s => s.setSpecificGroups) const setSpecificGroups = useAccessControlStore(s => s.setSpecificGroups)
const setSpecificMembers = useAccessControlStore(s => s.setSpecificMembers) const setSpecificMembers = useAccessControlStore(s => s.setSpecificMembers)
const { t } = useTranslation() const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const hideTip = systemFeatures.webapp_auth.enabled
&& (systemFeatures.webapp_auth.allow_sso
|| systemFeatures.webapp_auth.allow_email_password_login
|| systemFeatures.webapp_auth.allow_email_code_login)
const { isPending, data } = useAppWhiteListSubjects(appId, Boolean(appId) && currentMenu === AccessMode.SPECIFIC_GROUPS_MEMBERS) const { isPending, data } = useAppWhiteListSubjects(appId, Boolean(appId) && currentMenu === AccessMode.SPECIFIC_GROUPS_MEMBERS)
useEffect(() => { useEffect(() => {
@ -37,7 +30,6 @@ export default function SpecificGroupsOrMembers() {
<RiLockLine className='h-4 w-4 text-text-primary' /> <RiLockLine className='h-4 w-4 text-text-primary' />
<p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.specific')}</p> <p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.specific')}</p>
</div> </div>
{!hideTip && <WebAppSSONotEnabledTip />}
</div> </div>
} }
@ -48,10 +40,6 @@ export default function SpecificGroupsOrMembers() {
<p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.specific')}</p> <p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.specific')}</p>
</div> </div>
<div className='flex items-center gap-x-1'> <div className='flex items-center gap-x-1'>
{!hideTip && <>
<WebAppSSONotEnabledTip />
<Divider className='ml-2 mr-0 h-[14px]' type="vertical" />
</>}
<AddMemberOrGroupDialog /> <AddMemberOrGroupDialog />
</div> </div>
</div> </div>

@ -9,11 +9,14 @@ import dayjs from 'dayjs'
import { import {
RiArrowDownSLine, RiArrowDownSLine,
RiArrowRightSLine, RiArrowRightSLine,
RiBuildingLine,
RiGlobalLine,
RiLockLine, RiLockLine,
RiPlanetLine, RiPlanetLine,
RiPlayCircleLine, RiPlayCircleLine,
RiPlayList2Line, RiPlayList2Line,
RiTerminalBoxLine, RiTerminalBoxLine,
RiVerifiedBadgeLine,
} from '@remixicon/react' } from '@remixicon/react'
import { useKeyPress } from 'ahooks' import { useKeyPress } from 'ahooks'
import { getKeyboardKeyCodeBySystem } from '../../workflow/utils' import { getKeyboardKeyCodeBySystem } from '../../workflow/utils'
@ -276,10 +279,30 @@ const AppPublisher = ({
setShowAppAccessControl(true) setShowAppAccessControl(true)
}}> }}>
<div className='flex grow items-center gap-x-1.5 pr-1'> <div className='flex grow items-center gap-x-1.5 pr-1'>
<RiLockLine className='h-4 w-4 shrink-0 text-text-secondary' /> {appDetail?.access_mode === AccessMode.ORGANIZATION
{appDetail?.access_mode === AccessMode.ORGANIZATION && <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.organization')}</p>} && <>
{appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.specific')}</p>} <RiBuildingLine className='h-4 w-4 shrink-0 text-text-secondary' />
{appDetail?.access_mode === AccessMode.PUBLIC && <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.anyone')}</p>} <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.organization')}</p>
</>
}
{appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS
&& <>
<RiLockLine className='h-4 w-4 shrink-0 text-text-secondary' />
<p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.specific')}</p>
</>
}
{appDetail?.access_mode === AccessMode.PUBLIC
&& <>
<RiGlobalLine className='h-4 w-4 shrink-0 text-text-secondary' />
<p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.anyone')}</p>
</>
}
{appDetail?.access_mode === AccessMode.EXTERNAL_MEMBERS
&& <>
<RiVerifiedBadgeLine className='h-4 w-4 shrink-0 text-text-secondary' />
<p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.external')}</p>
</>
}
</div> </div>
{!isAppAccessSet && <p className='system-xs-regular shrink-0 text-text-tertiary'>{t('app.publishApp.notSet')}</p>} {!isAppAccessSet && <p className='system-xs-regular shrink-0 text-text-tertiary'>{t('app.publishApp.notSet')}</p>}
<div className='flex h-4 w-4 shrink-0 items-center justify-center'> <div className='flex h-4 w-4 shrink-0 items-center justify-center'>

@ -5,10 +5,13 @@ import { useTranslation } from 'react-i18next'
import { import {
RiArrowRightSLine, RiArrowRightSLine,
RiBookOpenLine, RiBookOpenLine,
RiBuildingLine,
RiEqualizer2Line, RiEqualizer2Line,
RiExternalLinkLine, RiExternalLinkLine,
RiGlobalLine,
RiLockLine, RiLockLine,
RiPaintBrushLine, RiPaintBrushLine,
RiVerifiedBadgeLine,
RiWindowLine, RiWindowLine,
} from '@remixicon/react' } from '@remixicon/react'
import SettingsModal from './settings' import SettingsModal from './settings'
@ -248,11 +251,30 @@ function AppCard({
<div className='flex h-9 w-full cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal py-1 pl-2.5 pr-2' <div className='flex h-9 w-full cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal py-1 pl-2.5 pr-2'
onClick={handleClickAccessControl}> onClick={handleClickAccessControl}>
<div className='flex grow items-center gap-x-1.5 pr-1'> <div className='flex grow items-center gap-x-1.5 pr-1'>
<RiLockLine className='h-4 w-4 shrink-0 text-text-secondary' /> {appDetail?.access_mode === AccessMode.ORGANIZATION
{appDetail?.access_mode === AccessMode.ORGANIZATION && <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.organization')}</p>} && <>
{appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.specific')}</p>} <RiBuildingLine className='h-4 w-4 shrink-0 text-text-secondary' />
{appDetail?.access_mode === AccessMode.PUBLIC && <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.anyone')}</p>} <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.organization')}</p>
</div> </>
}
{appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS
&& <>
<RiLockLine className='h-4 w-4 shrink-0 text-text-secondary' />
<p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.specific')}</p>
</>
}
{appDetail?.access_mode === AccessMode.PUBLIC
&& <>
<RiGlobalLine className='h-4 w-4 shrink-0 text-text-secondary' />
<p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.anyone')}</p>
</>
}
{appDetail?.access_mode === AccessMode.EXTERNAL_MEMBERS
&& <>
<RiVerifiedBadgeLine className='h-4 w-4 shrink-0 text-text-secondary' />
<p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.external')}</p>
</>
}</div>
{!isAppAccessSet && <p className='system-xs-regular shrink-0 text-text-tertiary'>{t('app.publishApp.notSet')}</p>} {!isAppAccessSet && <p className='system-xs-regular shrink-0 text-text-tertiary'>{t('app.publishApp.notSet')}</p>}
<div className='flex h-4 w-4 shrink-0 items-center justify-center'> <div className='flex h-4 w-4 shrink-0 items-center justify-center'>
<RiArrowRightSLine className='h-4 w-4 text-text-quaternary' /> <RiArrowRightSLine className='h-4 w-4 text-text-quaternary' />

@ -1,4 +1,5 @@
'use client' 'use client'
import classNames from '@/utils/classnames'
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -7,17 +8,19 @@ type IAppUnavailableProps = {
code?: number | string code?: number | string
isUnknownReason?: boolean isUnknownReason?: boolean
unknownReason?: string unknownReason?: string
className?: string
} }
const AppUnavailable: FC<IAppUnavailableProps> = ({ const AppUnavailable: FC<IAppUnavailableProps> = ({
code = 404, code = 404,
isUnknownReason, isUnknownReason,
unknownReason, unknownReason,
className,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className='flex h-screen w-screen items-center justify-center'> <div className={classNames('flex h-screen w-screen items-center justify-center', className)}>
<h1 className='mr-5 h-[50px] pr-5 text-[24px] font-medium leading-[50px]' <h1 className='mr-5 h-[50px] pr-5 text-[24px] font-medium leading-[50px]'
style={{ style={{
borderRight: '1px solid rgba(0,0,0,.3)', borderRight: '1px solid rgba(0,0,0,.3)',

@ -16,14 +16,12 @@ import type {
ConversationItem, ConversationItem,
} from '@/models/share' } from '@/models/share'
import { noop } from 'lodash-es' import { noop } from 'lodash-es'
import { AccessMode } from '@/models/access-control'
export type ChatWithHistoryContextValue = { export type ChatWithHistoryContextValue = {
appInfoError?: any appInfoError?: any
appInfoLoading?: boolean appInfoLoading?: boolean
appMeta?: AppMeta appMeta?: AppMeta
appData?: AppData appData?: AppData
accessMode?: AccessMode
userCanAccess?: boolean userCanAccess?: boolean
appParams?: ChatConfig appParams?: ChatConfig
appChatListDataLoading?: boolean appChatListDataLoading?: boolean
@ -64,7 +62,6 @@ export type ChatWithHistoryContextValue = {
} }
export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS,
userCanAccess: false, userCanAccess: false,
currentConversationId: '', currentConversationId: '',
appPrevChatTree: [], appPrevChatTree: [],

@ -16,7 +16,7 @@ import type {
Feedback, Feedback,
} from '../types' } from '../types'
import { CONVERSATION_ID_INFO } from '../constants' import { CONVERSATION_ID_INFO } from '../constants'
import { buildChatItemTree, getProcessedSystemVariablesFromUrlParams } from '../utils' import { buildChatItemTree, getProcessedSystemVariablesFromUrlParams, getRawInputsFromUrlParams } from '../utils'
import { addFileInfos, sortAgentSorts } from '../../../tools/utils' import { addFileInfos, sortAgentSorts } from '../../../tools/utils'
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
import { import {
@ -43,9 +43,8 @@ import { useAppFavicon } from '@/hooks/use-app-favicon'
import { InputVarType } from '@/app/components/workflow/types' import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app' import { TransferMethod } from '@/types/app'
import { noop } from 'lodash-es' import { noop } from 'lodash-es'
import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control' import { useGetUserCanAccessApp } from '@/service/access-control'
import { useGlobalPublicStore } from '@/context/global-public-context' import { useGlobalPublicStore } from '@/context/global-public-context'
import { AccessMode } from '@/models/access-control'
function getFormattedChatList(messages: any[]) { function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = [] const newChatList: ChatItem[] = []
@ -77,11 +76,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo])
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo) const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo)
const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({
appId: installedAppInfo?.app.id || appInfo?.app_id,
isInstalledApp,
enabled: systemFeatures.webapp_auth.enabled,
})
const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({
appId: installedAppInfo?.app.id || appInfo?.app_id, appId: installedAppInfo?.app.id || appInfo?.app_id,
isInstalledApp, isInstalledApp,
@ -195,6 +189,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const { t } = useTranslation() const { t } = useTranslation()
const newConversationInputsRef = useRef<Record<string, any>>({}) const newConversationInputsRef = useRef<Record<string, any>>({})
const [newConversationInputs, setNewConversationInputs] = useState<Record<string, any>>({}) const [newConversationInputs, setNewConversationInputs] = useState<Record<string, any>>({})
const [initInputs, setInitInputs] = useState<Record<string, any>>({})
const handleNewConversationInputsChange = useCallback((newInputs: Record<string, any>) => { const handleNewConversationInputsChange = useCallback((newInputs: Record<string, any>) => {
newConversationInputsRef.current = newInputs newConversationInputsRef.current = newInputs
setNewConversationInputs(newInputs) setNewConversationInputs(newInputs)
@ -202,20 +197,29 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const inputsForms = useMemo(() => { const inputsForms = useMemo(() => {
return (appParams?.user_input_form || []).filter((item: any) => !item.external_data_tool).map((item: any) => { return (appParams?.user_input_form || []).filter((item: any) => !item.external_data_tool).map((item: any) => {
if (item.paragraph) { if (item.paragraph) {
let value = initInputs[item.paragraph.variable]
if (value && item.paragraph.max_length && value.length > item.paragraph.max_length)
value = value.slice(0, item.paragraph.max_length)
return { return {
...item.paragraph, ...item.paragraph,
default: value || item.default,
type: 'paragraph', type: 'paragraph',
} }
} }
if (item.number) { if (item.number) {
const convertedNumber = Number(initInputs[item.number.variable]) ?? undefined
return { return {
...item.number, ...item.number,
default: convertedNumber || item.default,
type: 'number', type: 'number',
} }
} }
if (item.select) { if (item.select) {
const isInputInOptions = item.select.options.includes(initInputs[item.select.variable])
return { return {
...item.select, ...item.select,
default: (isInputInOptions ? initInputs[item.select.variable] : undefined) || item.default,
type: 'select', type: 'select',
} }
} }
@ -234,17 +238,30 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
} }
} }
let value = initInputs[item['text-input'].variable]
if (value && item['text-input'].max_length && value.length > item['text-input'].max_length)
value = value.slice(0, item['text-input'].max_length)
return { return {
...item['text-input'], ...item['text-input'],
default: value || item.default,
type: 'text-input', type: 'text-input',
} }
}) })
}, [appParams]) }, [initInputs, appParams])
const allInputsHidden = useMemo(() => { const allInputsHidden = useMemo(() => {
return inputsForms.length > 0 && inputsForms.every(item => item.hide === true) return inputsForms.length > 0 && inputsForms.every(item => item.hide === true)
}, [inputsForms]) }, [inputsForms])
useEffect(() => {
// init inputs from url params
(async () => {
const inputs = await getRawInputsFromUrlParams()
setInitInputs(inputs)
})()
}, [])
useEffect(() => { useEffect(() => {
const conversationInputs: Record<string, any> = {} const conversationInputs: Record<string, any> = {}
@ -362,11 +379,11 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
if (conversationId) if (conversationId)
setClearChatList(false) setClearChatList(false)
}, [handleConversationIdInfoChange, setClearChatList]) }, [handleConversationIdInfoChange, setClearChatList])
const handleNewConversation = useCallback(() => { const handleNewConversation = useCallback(async () => {
currentChatInstanceRef.current.handleStop() currentChatInstanceRef.current.handleStop()
setShowNewConversationItemInList(true) setShowNewConversationItemInList(true)
handleChangeConversation('') handleChangeConversation('')
handleNewConversationInputsChange({}) handleNewConversationInputsChange(await getRawInputsFromUrlParams())
setClearChatList(true) setClearChatList(true)
}, [handleChangeConversation, setShowNewConversationItemInList, handleNewConversationInputsChange, setClearChatList]) }, [handleChangeConversation, setShowNewConversationItemInList, handleNewConversationInputsChange, setClearChatList])
const handleUpdateConversationList = useCallback(() => { const handleUpdateConversationList = useCallback(() => {
@ -469,8 +486,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
return { return {
appInfoError, appInfoError,
appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission)), appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission),
accessMode: systemFeatures.webapp_auth.enabled ? appAccessMode?.accessMode : AccessMode.PUBLIC,
userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true, userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true,
isInstalledApp, isInstalledApp,
appId, appId,

@ -124,7 +124,6 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
const { const {
appInfoError, appInfoError,
appInfoLoading, appInfoLoading,
accessMode,
userCanAccess, userCanAccess,
appData, appData,
appParams, appParams,
@ -169,7 +168,6 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
appInfoError, appInfoError,
appInfoLoading, appInfoLoading,
appData, appData,
accessMode,
userCanAccess, userCanAccess,
appParams, appParams,
appMeta, appMeta,

@ -19,7 +19,6 @@ import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/re
import DifyLogo from '@/app/components/base/logo/dify-logo' import DifyLogo from '@/app/components/base/logo/dify-logo'
import type { ConversationItem } from '@/models/share' import type { ConversationItem } from '@/models/share'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { AccessMode } from '@/models/access-control'
import { useGlobalPublicStore } from '@/context/global-public-context' import { useGlobalPublicStore } from '@/context/global-public-context'
type Props = { type Props = {
@ -30,7 +29,6 @@ const Sidebar = ({ isPanel }: Props) => {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
isInstalledApp, isInstalledApp,
accessMode,
appData, appData,
handleNewConversation, handleNewConversation,
pinnedConversationList, pinnedConversationList,
@ -140,7 +138,7 @@ const Sidebar = ({ isPanel }: Props) => {
)} )}
</div> </div>
<div className='flex shrink-0 items-center justify-between p-3'> <div className='flex shrink-0 items-center justify-between p-3'>
<MenuDropdown hideLogout={isInstalledApp || accessMode === AccessMode.PUBLIC} placement='top-start' data={appData?.site} /> <MenuDropdown hideLogout={isInstalledApp} placement='top-start' data={appData?.site} />
{/* powered by */} {/* powered by */}
<div className='shrink-0'> <div className='shrink-0'>
{!appData?.custom_config?.remove_webapp_brand && ( {!appData?.custom_config?.remove_webapp_brand && (

@ -3,7 +3,7 @@ export const markdownContentSVG = `
<svg width="400" height="600" xmlns="http://www.w3.org/2000/svg"> <svg width="400" height="600" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#F0F8FF"/> <rect width="100%" height="100%" fill="#F0F8FF"/>
<text x="50%" y="60" font-family="楷体" font-size="32" fill="#4682B4" text-anchor="middle">Logo</text> <text x="50%" y="60" font-family="楷体" font-size="32" fill="#4682B4" text-anchor="middle"> Logo </text>
<line x1="50" y1="80" x2="350" y2="80" stroke="#B0C4DE" stroke-width="2"/> <line x1="50" y1="80" x2="350" y2="80" stroke="#B0C4DE" stroke-width="2"/>

@ -15,10 +15,8 @@ import type {
ConversationItem, ConversationItem,
} from '@/models/share' } from '@/models/share'
import { noop } from 'lodash-es' import { noop } from 'lodash-es'
import { AccessMode } from '@/models/access-control'
export type EmbeddedChatbotContextValue = { export type EmbeddedChatbotContextValue = {
accessMode?: AccessMode
userCanAccess?: boolean userCanAccess?: boolean
appInfoError?: any appInfoError?: any
appInfoLoading?: boolean appInfoLoading?: boolean
@ -58,7 +56,6 @@ export type EmbeddedChatbotContextValue = {
export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({
userCanAccess: false, userCanAccess: false,
accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS,
currentConversationId: '', currentConversationId: '',
appPrevChatList: [], appPrevChatList: [],
pinnedConversationList: [], pinnedConversationList: [],

@ -36,9 +36,8 @@ import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app' import { TransferMethod } from '@/types/app'
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
import { noop } from 'lodash-es' import { noop } from 'lodash-es'
import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control' import { useGetUserCanAccessApp } from '@/service/access-control'
import { useGlobalPublicStore } from '@/context/global-public-context' import { useGlobalPublicStore } from '@/context/global-public-context'
import { AccessMode } from '@/models/access-control'
function getFormattedChatList(messages: any[]) { function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = [] const newChatList: ChatItem[] = []
@ -70,11 +69,6 @@ export const useEmbeddedChatbot = () => {
const isInstalledApp = false const isInstalledApp = false
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR('appInfo', fetchAppInfo) const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR('appInfo', fetchAppInfo)
const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({
appId: appInfo?.app_id,
isInstalledApp,
enabled: systemFeatures.webapp_auth.enabled,
})
const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({
appId: appInfo?.app_id, appId: appInfo?.app_id,
isInstalledApp, isInstalledApp,
@ -385,8 +379,7 @@ export const useEmbeddedChatbot = () => {
return { return {
appInfoError, appInfoError,
appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission)), appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission),
accessMode: systemFeatures.webapp_auth.enabled ? appAccessMode?.accessMode : AccessMode.PUBLIC,
userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true, userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true,
isInstalledApp, isInstalledApp,
allowResetChat, allowResetChat,

@ -15,6 +15,17 @@ async function decodeBase64AndDecompress(base64String: string) {
} }
} }
async function getRawInputsFromUrlParams(): Promise<Record<string, any>> {
const urlParams = new URLSearchParams(window.location.search)
const inputs: Record<string, any> = {}
const entriesArray = Array.from(urlParams.entries())
entriesArray.forEach(([key, value]) => {
if (!key.startsWith('sys.'))
inputs[key] = decodeURIComponent(value)
})
return inputs
}
async function getProcessedInputsFromUrlParams(): Promise<Record<string, any>> { async function getProcessedInputsFromUrlParams(): Promise<Record<string, any>> {
const urlParams = new URLSearchParams(window.location.search) const urlParams = new URLSearchParams(window.location.search)
const inputs: Record<string, any> = {} const inputs: Record<string, any> = {}
@ -184,6 +195,7 @@ function getThreadMessages(tree: ChatItemInTree[], targetMessageId?: string): Ch
} }
export { export {
getRawInputsFromUrlParams,
getProcessedInputsFromUrlParams, getProcessedInputsFromUrlParams,
getProcessedSystemVariablesFromUrlParams, getProcessedSystemVariablesFromUrlParams,
isValidGeneratedAnswer, isValidGeneratedAnswer,

@ -231,7 +231,7 @@ export const useFile = (fileConfig: FileUpload) => {
url: res.url, url: res.url,
} }
if (!isAllowedFileExtension(res.name, res.mime_type, fileConfig.allowed_file_types || [], fileConfig.allowed_file_extensions || [])) { if (!isAllowedFileExtension(res.name, res.mime_type, fileConfig.allowed_file_types || [], fileConfig.allowed_file_extensions || [])) {
notify({ type: 'error', message: t('common.fileUploader.fileExtensionNotSupport') }) notify({ type: 'error', message: `${t('common.fileUploader.fileExtensionNotSupport')} ${file.type}` })
handleRemoveFile(uploadingFile.id) handleRemoveFile(uploadingFile.id)
} }
if (!checkSizeLimit(newFile.supportFileType, newFile.size)) if (!checkSizeLimit(newFile.supportFileType, newFile.size))
@ -257,7 +257,7 @@ export const useFile = (fileConfig: FileUpload) => {
const handleLocalFileUpload = useCallback((file: File) => { const handleLocalFileUpload = useCallback((file: File) => {
if (!isAllowedFileExtension(file.name, file.type, fileConfig.allowed_file_types || [], fileConfig.allowed_file_extensions || [])) { if (!isAllowedFileExtension(file.name, file.type, fileConfig.allowed_file_types || [], fileConfig.allowed_file_extensions || [])) {
notify({ type: 'error', message: t('common.fileUploader.fileExtensionNotSupport') }) notify({ type: 'error', message: `${t('common.fileUploader.fileExtensionNotSupport')} ${file.type}` })
return return
} }
const allowedFileTypes = fileConfig.allowed_file_types const allowedFileTypes = fileConfig.allowed_file_types

@ -22,7 +22,7 @@ import { FILE_EXTS } from '../prompt-editor/constants'
jest.mock('mime', () => ({ jest.mock('mime', () => ({
__esModule: true, __esModule: true,
default: { default: {
getExtension: jest.fn(), getAllExtensions: jest.fn(),
}, },
})) }))
@ -58,12 +58,27 @@ describe('file-uploader utils', () => {
describe('getFileExtension', () => { describe('getFileExtension', () => {
it('should get extension from mimetype', () => { it('should get extension from mimetype', () => {
jest.mocked(mime.getExtension).mockReturnValue('pdf') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['pdf']))
expect(getFileExtension('file', 'application/pdf')).toBe('pdf') expect(getFileExtension('file', 'application/pdf')).toBe('pdf')
}) })
it('should get extension from mimetype and file name 1', () => {
jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['pdf']))
expect(getFileExtension('file.pdf', 'application/pdf')).toBe('pdf')
})
it('should get extension from mimetype with multiple ext candidates with filename hint', () => {
jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['der', 'crt', 'pem']))
expect(getFileExtension('file.pem', 'application/x-x509-ca-cert')).toBe('pem')
})
it('should get extension from mimetype with multiple ext candidates without filename hint', () => {
jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['der', 'crt', 'pem']))
expect(getFileExtension('file', 'application/x-x509-ca-cert')).toBe('der')
})
it('should get extension from filename if mimetype fails', () => { it('should get extension from filename if mimetype fails', () => {
jest.mocked(mime.getExtension).mockReturnValue(null) jest.mocked(mime.getAllExtensions).mockReturnValue(null)
expect(getFileExtension('file.txt', '')).toBe('txt') expect(getFileExtension('file.txt', '')).toBe('txt')
expect(getFileExtension('file.txt.docx', '')).toBe('docx') expect(getFileExtension('file.txt.docx', '')).toBe('docx')
expect(getFileExtension('file', '')).toBe('') expect(getFileExtension('file', '')).toBe('')
@ -76,157 +91,157 @@ describe('file-uploader utils', () => {
describe('getFileAppearanceType', () => { describe('getFileAppearanceType', () => {
it('should identify gif files', () => { it('should identify gif files', () => {
jest.mocked(mime.getExtension).mockReturnValue('gif') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['gif']))
expect(getFileAppearanceType('image.gif', 'image/gif')) expect(getFileAppearanceType('image.gif', 'image/gif'))
.toBe(FileAppearanceTypeEnum.gif) .toBe(FileAppearanceTypeEnum.gif)
}) })
it('should identify image files', () => { it('should identify image files', () => {
jest.mocked(mime.getExtension).mockReturnValue('jpg') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['jpg']))
expect(getFileAppearanceType('image.jpg', 'image/jpeg')) expect(getFileAppearanceType('image.jpg', 'image/jpeg'))
.toBe(FileAppearanceTypeEnum.image) .toBe(FileAppearanceTypeEnum.image)
jest.mocked(mime.getExtension).mockReturnValue('jpeg') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['jpeg']))
expect(getFileAppearanceType('image.jpeg', 'image/jpeg')) expect(getFileAppearanceType('image.jpeg', 'image/jpeg'))
.toBe(FileAppearanceTypeEnum.image) .toBe(FileAppearanceTypeEnum.image)
jest.mocked(mime.getExtension).mockReturnValue('png') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['png']))
expect(getFileAppearanceType('image.png', 'image/png')) expect(getFileAppearanceType('image.png', 'image/png'))
.toBe(FileAppearanceTypeEnum.image) .toBe(FileAppearanceTypeEnum.image)
jest.mocked(mime.getExtension).mockReturnValue('webp') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['webp']))
expect(getFileAppearanceType('image.webp', 'image/webp')) expect(getFileAppearanceType('image.webp', 'image/webp'))
.toBe(FileAppearanceTypeEnum.image) .toBe(FileAppearanceTypeEnum.image)
jest.mocked(mime.getExtension).mockReturnValue('svg') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['svg']))
expect(getFileAppearanceType('image.svg', 'image/svgxml')) expect(getFileAppearanceType('image.svg', 'image/svgxml'))
.toBe(FileAppearanceTypeEnum.image) .toBe(FileAppearanceTypeEnum.image)
}) })
it('should identify video files', () => { it('should identify video files', () => {
jest.mocked(mime.getExtension).mockReturnValue('mp4') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['mp4']))
expect(getFileAppearanceType('video.mp4', 'video/mp4')) expect(getFileAppearanceType('video.mp4', 'video/mp4'))
.toBe(FileAppearanceTypeEnum.video) .toBe(FileAppearanceTypeEnum.video)
jest.mocked(mime.getExtension).mockReturnValue('mov') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['mov']))
expect(getFileAppearanceType('video.mov', 'video/quicktime')) expect(getFileAppearanceType('video.mov', 'video/quicktime'))
.toBe(FileAppearanceTypeEnum.video) .toBe(FileAppearanceTypeEnum.video)
jest.mocked(mime.getExtension).mockReturnValue('mpeg') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['mpeg']))
expect(getFileAppearanceType('video.mpeg', 'video/mpeg')) expect(getFileAppearanceType('video.mpeg', 'video/mpeg'))
.toBe(FileAppearanceTypeEnum.video) .toBe(FileAppearanceTypeEnum.video)
jest.mocked(mime.getExtension).mockReturnValue('webm') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['webm']))
expect(getFileAppearanceType('video.web', 'video/webm')) expect(getFileAppearanceType('video.web', 'video/webm'))
.toBe(FileAppearanceTypeEnum.video) .toBe(FileAppearanceTypeEnum.video)
}) })
it('should identify audio files', () => { it('should identify audio files', () => {
jest.mocked(mime.getExtension).mockReturnValue('mp3') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['mp3']))
expect(getFileAppearanceType('audio.mp3', 'audio/mpeg')) expect(getFileAppearanceType('audio.mp3', 'audio/mpeg'))
.toBe(FileAppearanceTypeEnum.audio) .toBe(FileAppearanceTypeEnum.audio)
jest.mocked(mime.getExtension).mockReturnValue('m4a') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['m4a']))
expect(getFileAppearanceType('audio.m4a', 'audio/mp4')) expect(getFileAppearanceType('audio.m4a', 'audio/mp4'))
.toBe(FileAppearanceTypeEnum.audio) .toBe(FileAppearanceTypeEnum.audio)
jest.mocked(mime.getExtension).mockReturnValue('wav') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['wav']))
expect(getFileAppearanceType('audio.wav', 'audio/vnd.wav')) expect(getFileAppearanceType('audio.wav', 'audio/vnd.wav'))
.toBe(FileAppearanceTypeEnum.audio) .toBe(FileAppearanceTypeEnum.audio)
jest.mocked(mime.getExtension).mockReturnValue('amr') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['amr']))
expect(getFileAppearanceType('audio.amr', 'audio/AMR')) expect(getFileAppearanceType('audio.amr', 'audio/AMR'))
.toBe(FileAppearanceTypeEnum.audio) .toBe(FileAppearanceTypeEnum.audio)
jest.mocked(mime.getExtension).mockReturnValue('mpga') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['mpga']))
expect(getFileAppearanceType('audio.mpga', 'audio/mpeg')) expect(getFileAppearanceType('audio.mpga', 'audio/mpeg'))
.toBe(FileAppearanceTypeEnum.audio) .toBe(FileAppearanceTypeEnum.audio)
}) })
it('should identify code files', () => { it('should identify code files', () => {
jest.mocked(mime.getExtension).mockReturnValue('html') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['html']))
expect(getFileAppearanceType('index.html', 'text/html')) expect(getFileAppearanceType('index.html', 'text/html'))
.toBe(FileAppearanceTypeEnum.code) .toBe(FileAppearanceTypeEnum.code)
}) })
it('should identify PDF files', () => { it('should identify PDF files', () => {
jest.mocked(mime.getExtension).mockReturnValue('pdf') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['pdf']))
expect(getFileAppearanceType('doc.pdf', 'application/pdf')) expect(getFileAppearanceType('doc.pdf', 'application/pdf'))
.toBe(FileAppearanceTypeEnum.pdf) .toBe(FileAppearanceTypeEnum.pdf)
}) })
it('should identify markdown files', () => { it('should identify markdown files', () => {
jest.mocked(mime.getExtension).mockReturnValue('md') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['md']))
expect(getFileAppearanceType('file.md', 'text/markdown')) expect(getFileAppearanceType('file.md', 'text/markdown'))
.toBe(FileAppearanceTypeEnum.markdown) .toBe(FileAppearanceTypeEnum.markdown)
jest.mocked(mime.getExtension).mockReturnValue('markdown') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['markdown']))
expect(getFileAppearanceType('file.markdown', 'text/markdown')) expect(getFileAppearanceType('file.markdown', 'text/markdown'))
.toBe(FileAppearanceTypeEnum.markdown) .toBe(FileAppearanceTypeEnum.markdown)
jest.mocked(mime.getExtension).mockReturnValue('mdx') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['mdx']))
expect(getFileAppearanceType('file.mdx', 'text/mdx')) expect(getFileAppearanceType('file.mdx', 'text/mdx'))
.toBe(FileAppearanceTypeEnum.markdown) .toBe(FileAppearanceTypeEnum.markdown)
}) })
it('should identify excel files', () => { it('should identify excel files', () => {
jest.mocked(mime.getExtension).mockReturnValue('xlsx') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['xlsx']))
expect(getFileAppearanceType('doc.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')) expect(getFileAppearanceType('doc.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'))
.toBe(FileAppearanceTypeEnum.excel) .toBe(FileAppearanceTypeEnum.excel)
jest.mocked(mime.getExtension).mockReturnValue('xls') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['xls']))
expect(getFileAppearanceType('doc.xls', 'application/vnd.ms-excel')) expect(getFileAppearanceType('doc.xls', 'application/vnd.ms-excel'))
.toBe(FileAppearanceTypeEnum.excel) .toBe(FileAppearanceTypeEnum.excel)
}) })
it('should identify word files', () => { it('should identify word files', () => {
jest.mocked(mime.getExtension).mockReturnValue('doc') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['doc']))
expect(getFileAppearanceType('doc.doc', 'application/msword')) expect(getFileAppearanceType('doc.doc', 'application/msword'))
.toBe(FileAppearanceTypeEnum.word) .toBe(FileAppearanceTypeEnum.word)
jest.mocked(mime.getExtension).mockReturnValue('docx') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['docx']))
expect(getFileAppearanceType('doc.docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')) expect(getFileAppearanceType('doc.docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'))
.toBe(FileAppearanceTypeEnum.word) .toBe(FileAppearanceTypeEnum.word)
}) })
it('should identify word files', () => { it('should identify word files', () => {
jest.mocked(mime.getExtension).mockReturnValue('ppt') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['ppt']))
expect(getFileAppearanceType('doc.ppt', 'application/vnd.ms-powerpoint')) expect(getFileAppearanceType('doc.ppt', 'application/vnd.ms-powerpoint'))
.toBe(FileAppearanceTypeEnum.ppt) .toBe(FileAppearanceTypeEnum.ppt)
jest.mocked(mime.getExtension).mockReturnValue('pptx') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['pptx']))
expect(getFileAppearanceType('doc.pptx', 'application/vnd.openxmlformats-officedocument.presentationml.presentation')) expect(getFileAppearanceType('doc.pptx', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'))
.toBe(FileAppearanceTypeEnum.ppt) .toBe(FileAppearanceTypeEnum.ppt)
}) })
it('should identify document files', () => { it('should identify document files', () => {
jest.mocked(mime.getExtension).mockReturnValue('txt') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['txt']))
expect(getFileAppearanceType('file.txt', 'text/plain')) expect(getFileAppearanceType('file.txt', 'text/plain'))
.toBe(FileAppearanceTypeEnum.document) .toBe(FileAppearanceTypeEnum.document)
jest.mocked(mime.getExtension).mockReturnValue('csv') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['csv']))
expect(getFileAppearanceType('file.csv', 'text/csv')) expect(getFileAppearanceType('file.csv', 'text/csv'))
.toBe(FileAppearanceTypeEnum.document) .toBe(FileAppearanceTypeEnum.document)
jest.mocked(mime.getExtension).mockReturnValue('msg') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['msg']))
expect(getFileAppearanceType('file.msg', 'application/vnd.ms-outlook')) expect(getFileAppearanceType('file.msg', 'application/vnd.ms-outlook'))
.toBe(FileAppearanceTypeEnum.document) .toBe(FileAppearanceTypeEnum.document)
jest.mocked(mime.getExtension).mockReturnValue('eml') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['eml']))
expect(getFileAppearanceType('file.eml', 'message/rfc822')) expect(getFileAppearanceType('file.eml', 'message/rfc822'))
.toBe(FileAppearanceTypeEnum.document) .toBe(FileAppearanceTypeEnum.document)
jest.mocked(mime.getExtension).mockReturnValue('xml') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['xml']))
expect(getFileAppearanceType('file.xml', 'application/rssxml')) expect(getFileAppearanceType('file.xml', 'application/rssxml'))
.toBe(FileAppearanceTypeEnum.document) .toBe(FileAppearanceTypeEnum.document)
jest.mocked(mime.getExtension).mockReturnValue('epub') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['epub']))
expect(getFileAppearanceType('file.epub', 'application/epubzip')) expect(getFileAppearanceType('file.epub', 'application/epubzip'))
.toBe(FileAppearanceTypeEnum.document) .toBe(FileAppearanceTypeEnum.document)
}) })
it('should handle null mime extension', () => { it('should handle null mime extension', () => {
jest.mocked(mime.getExtension).mockReturnValue(null) jest.mocked(mime.getAllExtensions).mockReturnValue(null)
expect(getFileAppearanceType('file.txt', 'text/plain')) expect(getFileAppearanceType('file.txt', 'text/plain'))
.toBe(FileAppearanceTypeEnum.document) .toBe(FileAppearanceTypeEnum.document)
}) })
@ -360,7 +375,7 @@ describe('file-uploader utils', () => {
describe('isAllowedFileExtension', () => { describe('isAllowedFileExtension', () => {
it('should validate allowed file extensions', () => { it('should validate allowed file extensions', () => {
jest.mocked(mime.getExtension).mockReturnValue('pdf') jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['pdf']))
expect(isAllowedFileExtension( expect(isAllowedFileExtension(
'test.pdf', 'test.pdf',
'application/pdf', 'application/pdf',

@ -42,19 +42,38 @@ export const fileUpload: FileUpload = ({
}) })
} }
const additionalExtensionMap = new Map<string, string[]>([
['text/x-markdown', ['md']],
])
export const getFileExtension = (fileName: string, fileMimetype: string, isRemote?: boolean) => { export const getFileExtension = (fileName: string, fileMimetype: string, isRemote?: boolean) => {
let extension = '' let extension = ''
if (fileMimetype) let extensions = new Set<string>()
extension = mime.getExtension(fileMimetype) || '' if (fileMimetype) {
const extensionsFromMimeType = mime.getAllExtensions(fileMimetype) || new Set<string>()
const additionalExtensions = additionalExtensionMap.get(fileMimetype) || []
extensions = new Set<string>([
...extensionsFromMimeType,
...additionalExtensions,
])
}
if (fileName && !extension) { let extensionInFileName = ''
if (fileName) {
const fileNamePair = fileName.split('.') const fileNamePair = fileName.split('.')
const fileNamePairLength = fileNamePair.length const fileNamePairLength = fileNamePair.length
if (fileNamePairLength > 1) if (fileNamePairLength > 1) {
extension = fileNamePair[fileNamePairLength - 1] extensionInFileName = fileNamePair[fileNamePairLength - 1].toLowerCase()
if (extensions.has(extensionInFileName))
extension = extensionInFileName
}
}
if (!extension) {
if (extensions.size > 0)
extension = extensions.values().next().value.toLowerCase()
else else
extension = '' extension = extensionInFileName
} }
if (isRemote) if (isRemote)

@ -1,7 +1,7 @@
import { useChatContext } from '@/app/components/base/chat/chat/context' import { useChatContext } from '@/app/components/base/chat/chat/context'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { isValidUrl } from './utils'
const MarkdownButton = ({ node }: any) => { const MarkdownButton = ({ node }: any) => {
const { onSend } = useChatContext() const { onSend } = useChatContext()
const variant = node.properties.dataVariant const variant = node.properties.dataVariant
@ -9,25 +9,17 @@ const MarkdownButton = ({ node }: any) => {
const link = node.properties.dataLink const link = node.properties.dataLink
const size = node.properties.dataSize const size = node.properties.dataSize
function is_valid_url(url: string): boolean {
try {
const parsed_url = new URL(url)
return ['http:', 'https:'].includes(parsed_url.protocol)
}
catch {
return false
}
}
return <Button return <Button
variant={variant} variant={variant}
size={size} size={size}
className={cn('!h-auto min-h-8 select-none whitespace-normal !px-3')} className={cn('!h-auto min-h-8 select-none whitespace-normal !px-3')}
onClick={() => { onClick={() => {
if (is_valid_url(link)) { if (isValidUrl(link)) {
window.open(link, '_blank') window.open(link, '_blank')
return return
} }
if(!message)
return
onSend?.(message) onSend?.(message)
}} }}
> >

@ -5,6 +5,7 @@
*/ */
import React from 'react' import React from 'react'
import { useChatContext } from '@/app/components/base/chat/chat/context' import { useChatContext } from '@/app/components/base/chat/chat/context'
import { isValidUrl } from './utils'
const Link = ({ node, children, ...props }: any) => { const Link = ({ node, children, ...props }: any) => {
const { onSend } = useChatContext() const { onSend } = useChatContext()
@ -14,7 +15,11 @@ const Link = ({ node, children, ...props }: any) => {
return <abbr className="cursor-pointer underline !decoration-primary-700 decoration-dashed" onClick={() => onSend?.(hidden_text)} title={node.children[0]?.value || ''}>{node.children[0]?.value || ''}</abbr> return <abbr className="cursor-pointer underline !decoration-primary-700 decoration-dashed" onClick={() => onSend?.(hidden_text)} title={node.children[0]?.value || ''}>{node.children[0]?.value || ''}</abbr>
} }
else { else {
return <a {...props} target="_blank" className="cursor-pointer underline !decoration-primary-700 decoration-dashed">{children || 'Download'}</a> const href = props.href || node.properties?.href
if(!isValidUrl(href))
return <span>{children}</span>
return <a href={href} target="_blank" className="cursor-pointer underline !decoration-primary-700 decoration-dashed">{children || 'Download'}</a>
} }
} }

@ -0,0 +1,3 @@
export const isValidUrl = (url: string): boolean => {
return ['http:', 'https:', '//', 'mailto:'].some(prefix => url.startsWith(prefix))
}

@ -7,7 +7,7 @@ import RemarkGfm from 'remark-gfm'
import RehypeRaw from 'rehype-raw' import RehypeRaw from 'rehype-raw'
import { flow } from 'lodash-es' import { flow } from 'lodash-es'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { preprocessLaTeX, preprocessThinkTag } from './markdown-utils' import { customUrlTransform, preprocessLaTeX, preprocessThinkTag } from './markdown-utils'
import { import {
AudioBlock, AudioBlock,
CodeBlock, CodeBlock,
@ -65,6 +65,7 @@ export function Markdown(props: { content: string; className?: string; customDis
} }
}, },
]} ]}
urlTransform={customUrlTransform}
disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body', ...(props.customDisallowedElements || [])]} disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body', ...(props.customDisallowedElements || [])]}
components={{ components={{
code: CodeBlock, code: CodeBlock,

@ -36,3 +36,52 @@ export const preprocessThinkTag = (content: string) => {
(str: string) => str.replace(/(<\/details>)(?![^\S\r\n]*[\r\n])(?![^\S\r\n]*$)/g, '$1\n'), (str: string) => str.replace(/(<\/details>)(?![^\S\r\n]*[\r\n])(?![^\S\r\n]*$)/g, '$1\n'),
])(content) ])(content)
} }
/**
* Transforms a URI for use in react-markdown, ensuring security and compatibility.
* This function is designed to work with react-markdown v9+ which has stricter
* default URL handling.
*
* Behavior:
* 1. Always allows the custom 'abbr:' protocol.
* 2. Always allows page-local fragments (e.g., "#some-id").
* 3. Always allows protocol-relative URLs (e.g., "//example.com/path").
* 4. Always allows purely relative paths (e.g., "path/to/file", "/abs/path").
* 5. Allows absolute URLs if their scheme is in a permitted list (case-insensitive):
* 'http:', 'https:', 'mailto:', 'xmpp:', 'irc:', 'ircs:'.
* 6. Intelligently distinguishes colons used for schemes from colons within
* paths, query parameters, or fragments of relative-like URLs.
* 7. Returns the original URI if allowed, otherwise returns `undefined` to
* signal that the URI should be removed/disallowed by react-markdown.
*/
export const customUrlTransform = (uri: string): string | undefined => {
const PERMITTED_SCHEME_REGEX = /^(https?|ircs?|mailto|xmpp|abbr):$/i
if (uri.startsWith('#'))
return uri
if (uri.startsWith('//'))
return uri
const colonIndex = uri.indexOf(':')
if (colonIndex === -1)
return uri
const slashIndex = uri.indexOf('/')
const questionMarkIndex = uri.indexOf('?')
const hashIndex = uri.indexOf('#')
if (
(slashIndex !== -1 && colonIndex > slashIndex)
|| (questionMarkIndex !== -1 && colonIndex > questionMarkIndex)
|| (hashIndex !== -1 && colonIndex > hashIndex)
)
return uri
const scheme = uri.substring(0, colonIndex + 1).toLowerCase()
if (PERMITTED_SCHEME_REGEX.test(scheme))
return uri
return undefined
}

@ -487,15 +487,15 @@ const Flowchart = React.forwardRef((props: {
'bg-white': currentTheme === Theme.light, 'bg-white': currentTheme === Theme.light,
'bg-slate-900': currentTheme === Theme.dark, 'bg-slate-900': currentTheme === Theme.dark,
}), }),
mermaidDiv: cn('mermaid cursor-pointer h-auto w-full relative', { mermaidDiv: cn('mermaid relative h-auto w-full cursor-pointer', {
'bg-white': currentTheme === Theme.light, 'bg-white': currentTheme === Theme.light,
'bg-slate-900': currentTheme === Theme.dark, 'bg-slate-900': currentTheme === Theme.dark,
}), }),
errorMessage: cn('py-4 px-[26px]', { errorMessage: cn('px-[26px] py-4', {
'text-red-500': currentTheme === Theme.light, 'text-red-500': currentTheme === Theme.light,
'text-red-400': currentTheme === Theme.dark, 'text-red-400': currentTheme === Theme.dark,
}), }),
errorIcon: cn('w-6 h-6', { errorIcon: cn('h-6 w-6', {
'text-red-500': currentTheme === Theme.light, 'text-red-500': currentTheme === Theme.light,
'text-red-400': currentTheme === Theme.dark, 'text-red-400': currentTheme === Theme.dark,
}), }),
@ -503,7 +503,7 @@ const Flowchart = React.forwardRef((props: {
'text-gray-700': currentTheme === Theme.light, 'text-gray-700': currentTheme === Theme.light,
'text-gray-300': currentTheme === Theme.dark, 'text-gray-300': currentTheme === Theme.dark,
}), }),
themeToggle: cn('flex items-center justify-center w-10 h-10 rounded-full transition-all duration-300 shadow-md backdrop-blur-sm', { themeToggle: cn('flex h-10 w-10 items-center justify-center rounded-full shadow-md backdrop-blur-sm transition-all duration-300', {
'bg-white/80 hover:bg-white hover:shadow-lg text-gray-700 border border-gray-200': currentTheme === Theme.light, 'bg-white/80 hover:bg-white hover:shadow-lg text-gray-700 border border-gray-200': currentTheme === Theme.light,
'bg-slate-800/80 hover:bg-slate-700 hover:shadow-lg text-yellow-300 border border-slate-600': currentTheme === Theme.dark, 'bg-slate-800/80 hover:bg-slate-700 hover:shadow-lg text-yellow-300 border border-slate-600': currentTheme === Theme.dark,
}), }),
@ -512,7 +512,7 @@ const Flowchart = React.forwardRef((props: {
// Style classes for look options // Style classes for look options
const getLookButtonClass = (lookType: 'classic' | 'handDrawn') => { const getLookButtonClass = (lookType: 'classic' | 'handDrawn') => {
return cn( return cn(
'flex items-center justify-center mb-4 w-[calc((100%-8px)/2)] h-8 rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg cursor-pointer system-sm-medium text-text-secondary', 'system-sm-medium mb-4 flex h-8 w-[calc((100%-8px)/2)] cursor-pointer items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg text-text-secondary',
look === lookType && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary', look === lookType && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary',
currentTheme === Theme.dark && 'border-slate-600 bg-slate-800 text-slate-300', currentTheme === Theme.dark && 'border-slate-600 bg-slate-800 text-slate-300',
look === lookType && currentTheme === Theme.dark && 'border-blue-500 bg-slate-700 text-white', look === lookType && currentTheme === Theme.dark && 'border-blue-500 bg-slate-700 text-white',
@ -523,7 +523,7 @@ const Flowchart = React.forwardRef((props: {
<div ref={ref as React.RefObject<HTMLDivElement>} className={themeClasses.container}> <div ref={ref as React.RefObject<HTMLDivElement>} className={themeClasses.container}>
<div className={themeClasses.segmented}> <div className={themeClasses.segmented}>
<div className="msh-segmented-group"> <div className="msh-segmented-group">
<label className="msh-segmented-item flex items-center space-x-1 m-2 w-[200px]"> <label className="msh-segmented-item m-2 flex w-[200px] items-center space-x-1">
<div <div
key='classic' key='classic'
className={getLookButtonClass('classic')} className={getLookButtonClass('classic')}
@ -545,7 +545,7 @@ const Flowchart = React.forwardRef((props: {
<div ref={containerRef} style={{ position: 'absolute', visibility: 'hidden', height: 0, overflow: 'hidden' }} /> <div ref={containerRef} style={{ position: 'absolute', visibility: 'hidden', height: 0, overflow: 'hidden' }} />
{isLoading && !svgCode && ( {isLoading && !svgCode && (
<div className='py-4 px-[26px]'> <div className='px-[26px] py-4'>
<LoadingAnim type='text'/> <LoadingAnim type='text'/>
{!isCodeComplete && ( {!isCodeComplete && (
<div className="mt-2 text-sm text-gray-500"> <div className="mt-2 text-sm text-gray-500">
@ -557,7 +557,7 @@ const Flowchart = React.forwardRef((props: {
{svgCode && ( {svgCode && (
<div className={themeClasses.mermaidDiv} style={{ objectFit: 'cover' }} onClick={() => setImagePreviewUrl(svgCode)}> <div className={themeClasses.mermaidDiv} style={{ objectFit: 'cover' }} onClick={() => setImagePreviewUrl(svgCode)}>
<div className="absolute left-2 bottom-2 z-[100]"> <div className="absolute bottom-2 left-2 z-[100]">
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()

@ -3,10 +3,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
# Completion アプリ API # Completion アプリ API
テキスト生成アプリケーションはセッションレスをサポートし、翻訳、記事作成、要約AI等に最適です。 テキスト生成アプリケーションはセッションレスをサポートし、翻訳、記事作成、要約 AI 等に最適です。
<div> <div>
### ベースURL ### ベース URL
<CodeGroup title="Code" targetCode={props.appDetail.api_base_url}> <CodeGroup title="Code" targetCode={props.appDetail.api_base_url}>
```javascript ```javascript
``` ```
@ -14,10 +14,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
### 認証 ### 認証
サービスAPIは`API-Key`認証を使用します。 サービス API `API-Key` 認証を使用します。
<i>**APIキーの漏洩による重大な結果を避けるため、APIキーはサーバーサイドに保存し、クライアントサイドでは共有や保存しないことを強く推奨します。**</i> <i>**API キーの漏洩による重大な結果を避けるため、API キーはサーバーサイドに保存し、クライアントサイドでは共有や保存しないことを強く推奨します。**</i>
すべてのAPIリクエストで、以下のように`Authorization` HTTPヘッダーにAPIキーを含めてください すべての API リクエストで、以下のように `Authorization` HTTP ヘッダーに API キーを含めてください:
<CodeGroup title="Code"> <CodeGroup title="Code">
```javascript ```javascript
@ -212,7 +212,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<Row> <Row>
<Col> <Col>
メッセージ送信時に使用するファイル(現在は画像のみ対応)をアップロードし、画像とテキストのマルチモーダルな理解を可能にします。 メッセージ送信時に使用するファイル(現在は画像のみ対応)をアップロードし、画像とテキストのマルチモーダルな理解を可能にします。
png、jpg、jpeg、webp、gif形式に対応しています。 png、jpg、jpeg、webp、gif 形式に対応しています。
<i>アップロードされたファイルは、現在のエンドユーザーのみが使用できます。</i> <i>アップロードされたファイルは、現在のエンドユーザーのみが使用できます。</i>
### リクエストボディ ### リクエストボディ
@ -223,25 +223,25 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
開発者のルールで定義されたユーザー識別子。アプリケーション内で一意である必要があります。 開発者のルールで定義されたユーザー識別子。アプリケーション内で一意である必要があります。
### レスポンス ### レスポンス
アップロードが成功すると、サーバーはファイルのIDと関連情報を返します。 アップロードが成功すると、サーバーはファイルの ID と関連情報を返します。
- `id` (uuid) ID - `id` (uuid) ID
- `name` (string) ファイル名 - `name` (string) ファイル名
- `size` (int) ファイルサイズ(バイト) - `size` (int) ファイルサイズ(バイト)
- `extension` (string) ファイル拡張子 - `extension` (string) ファイル拡張子
- `mime_type` (string) ファイルのMIMEタイプ - `mime_type` (string) ファイルの MIME タイプ
- `created_by` (uuid) エンドユーザーID - `created_by` (uuid) エンドユーザーID
- `created_at` (timestamp) 作成タイムスタンプ、例1705395332 - `created_at` (timestamp) 作成タイムスタンプ、例1705395332
### エラー ### エラー
- 400, `no_file_uploaded`, ファイルを提供する必要があります - 400, `no_file_uploaded`, ファイルを提供する必要があります
- 400, `too_many_files`, 現在は1つのファイルのみ受け付けています - 400, `too_many_files`, 現在は 1 つのファイルのみ受け付けています
- 400, `unsupported_preview`, ファイルがプレビューに対応していません - 400, `unsupported_preview`, ファイルがプレビューに対応していません
- 400, `unsupported_estimate`, ファイルが推定に対応していません - 400, `unsupported_estimate`, ファイルが推定に対応していません
- 413, `file_too_large`, ファイルが大きすぎます - 413, `file_too_large`, ファイルが大きすぎます
- 415, `unsupported_file_type`, サポートされていない拡張子です。現在はドキュメントファイルのみ受け付けています - 415, `unsupported_file_type`, サポートされていない拡張子です。現在はドキュメントファイルのみ受け付けています
- 503, `s3_connection_failed`, S3サービスに接続できません - 503, `s3_connection_failed`, S3 サービスに接続できません
- 503, `s3_permission_denied`, S3へのファイルアップロード権限がありません - 503, `s3_permission_denied`, S3 へのファイルアップロード権限がありません
- 503, `s3_file_too_large`, ファイルがS3のサイズ制限を超えています - 503, `s3_file_too_large`, ファイルが S3 のサイズ制限を超えています
- 500, 内部サーバーエラー - 500, 内部サーバーエラー
</Col> </Col>
@ -286,7 +286,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<Col> <Col>
ストリーミングモードでのみサポートされています。 ストリーミングモードでのみサポートされています。
### パス ### パス
- `task_id` (string) タスクID、ストリーミングチャンクの返信から取得可能 - `task_id` (string) タスク ID、ストリーミングチャンクの返信から取得可能
リクエストボディ リクエストボディ
- `user` (string) 必須 - `user` (string) 必須
ユーザー識別子。エンドユーザーの身元を定義するために使用され、メッセージ送信インターフェースで渡されたユーザーと一致する必要があります。 ユーザー識別子。エンドユーザーの身元を定義するために使用され、メッセージ送信インターフェースで渡されたユーザーと一致する必要があります。
@ -655,22 +655,22 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
/> />
<Row> <Row>
<Col> <Col>
アプリのWebApp設定を取得するために使用します。 アプリの WebApp 設定を取得するために使用します。
### レスポンス ### レスポンス
- `title` (string) WebApp名 - `title` (string) WebApp
- `chat_color_theme` (string) チャットの色テーマ、16進数形式 - `chat_color_theme` (string) チャットの色テーマ、16 進数形式
- `chat_color_theme_inverted` (bool) チャットの色テーマを反転するかどうか - `chat_color_theme_inverted` (bool) チャットの色テーマを反転するかどうか
- `icon_type` (string) アイコンタイプ、`emoji`-絵文字、`image`-画像 - `icon_type` (string) アイコンタイプ、`emoji`-絵文字、`image`-画像
- `icon` (string) アイコン。`emoji`タイプの場合は絵文字、`image`タイプの場合は画像URL - `icon` (string) アイコン。`emoji`タイプの場合は絵文字、`image`タイプの場合は画像 URL
- `icon_background` (string) 16進数形式の背景色 - `icon_background` (string) 16 進数形式の背景色
- `icon_url` (string) アイコンのURL - `icon_url` (string) アイコンの URL
- `description` (string) 説明 - `description` (string) 説明
- `copyright` (string) 著作権情報 - `copyright` (string) 著作権情報
- `privacy_policy` (string) プライバシーポリシーのリンク - `privacy_policy` (string) プライバシーポリシーのリンク
- `custom_disclaimer` (string) カスタム免責事項 - `custom_disclaimer` (string) カスタム免責事項
- `default_language` (string) デフォルト言語 - `default_language` (string) デフォルト言語
- `show_workflow_steps` (bool) ワークフローの詳細を表示するかどうか - `show_workflow_steps` (bool) ワークフローの詳細を表示するかどうか
- `use_icon_as_answer_icon` (bool) WebAppのアイコンをチャット内の🤖に置き換えるかどうか - `use_icon_as_answer_icon` (bool) WebApp のアイコンをチャット内の🤖に置き換えるかどうか
</Col> </Col>
<Col> <Col>
<CodeGroup title="Request" tag="POST" label="/meta" targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\\n-H 'Authorization: Bearer {api_key}'`}> <CodeGroup title="Request" tag="POST" label="/meta" targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\\n-H 'Authorization: Bearer {api_key}'`}>

@ -60,7 +60,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
<Property name='files' type='array[object]' key='files'> <Property name='files' type='array[object]' key='files'>
上传的文件。 上传的文件。
- `type` (string) 支持类型:图片 `image`(目前仅支持图片格式) 。 - `type` (string) 支持类型:图片 `image`(目前仅支持图片格式) 。
- `transfer_method` (string) 传递方式: - `transfer_method` (string) 传递方式
- `remote_url`: 图片地址。 - `remote_url`: 图片地址。
- `local_file`: 上传文件。 - `local_file`: 上传文件。
- `url` 图片地址。(仅当传递方式为 `remote_url` 时)。 - `url` 图片地址。(仅当传递方式为 `remote_url` 时)。
@ -622,10 +622,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
用于获取应用的 WebApp 设置 用于获取应用的 WebApp 设置
### Response ### Response
- `title` (string) WebApp 名称 - `title` (string) WebApp 名称
- `chat_color_theme` (string) 聊天颜色主题, hex 格式 - `chat_color_theme` (string) 聊天颜色主题hex 格式
- `chat_color_theme_inverted` (bool) 聊天颜色主题是否反转 - `chat_color_theme_inverted` (bool) 聊天颜色主题是否反转
- `icon_type` (string) 图标类型, `emoji`-表情, `image`-图片 - `icon_type` (string) 图标类型`emoji`-表情,`image`-图片
- `icon` (string) 图标, 如果是 `emoji` 类型, 则是 emoji 表情符号, 如果是 `image` 类型, 则是图片 URL - `icon` (string) 图标,如果是 `emoji` 类型,则是 emoji 表情符号,如果是 `image` 类型,则是图片 URL
- `icon_background` (string) hex 格式的背景色 - `icon_background` (string) hex 格式的背景色
- `icon_url` (string) 图标 URL - `icon_url` (string) 图标 URL
- `description` (string) 描述 - `description` (string) 描述
@ -879,10 +879,10 @@ ___
动作,只能是 'enable' 或 'disable' 动作,只能是 'enable' 或 'disable'
</Property> </Property>
<Property name='embedding_provider_name' type='string' key='embedding_provider_name'> <Property name='embedding_provider_name' type='string' key='embedding_provider_name'>
指定的嵌入模型提供商, 必须先在系统内设定好接入的模型对应的是provider字段 指定的嵌入模型提供商必须先在系统内设定好接入的模型,对应的是 provider 字段
</Property> </Property>
<Property name='embedding_model_name' type='string' key='embedding_model_name'> <Property name='embedding_model_name' type='string' key='embedding_model_name'>
指定的嵌入模型对应的是model字段 指定的嵌入模型,对应的是 model 字段
</Property> </Property>
<Property name='score_threshold' type='number' key='score_threshold'> <Property name='score_threshold' type='number' key='score_threshold'>
相似度阈值,当相似度大于该阈值时,系统会自动回复,否则不回复 相似度阈值,当相似度大于该阈值时,系统会自动回复,否则不回复
@ -890,8 +890,8 @@ ___
</Properties> </Properties>
</Col> </Col>
<Col sticky> <Col sticky>
嵌入模型的提供商和模型名称可以通过以下接口获取v1/workspaces/current/models/model-types/text-embedding 具体见:通过 API 维护知识库。 使用的Authorization是Dataset的API Token。 嵌入模型的提供商和模型名称可以通过以下接口获取v1/workspaces/current/models/model-types/text-embedding具体见通过 API 维护知识库。使用的 Authorization Dataset API Token。
该接口是异步执行所以会返回一个job_id通过查询job状态接口可以获取到最终的执行结果。 该接口是异步执行,所以会返回一个 job_id通过查询 job 状态接口可以获取到最终的执行结果。
<CodeGroup <CodeGroup
title="Request" title="Request"
tag="POST" tag="POST"

@ -1,12 +1,12 @@
import { CodeGroup } from '../code.tsx' import { CodeGroup } from '../code.tsx'
import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx' import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx'
# 高度なチャットアプリAPI # 高度なチャットアプリ API
チャットアプリケーションはセッションの持続性をサポートしており、以前のチャット履歴を応答のコンテキストとして使用できます。これは、チャットボットやカスタマーサービスAIなどに適用できます。 チャットアプリケーションはセッションの持続性をサポートしており、以前のチャット履歴を応答のコンテキストとして使用できます。これは、チャットボットやカスタマーサービス AI などに適用できます。
<div> <div>
### ベースURL ### ベース URL
<CodeGroup title="コード" targetCode={props.appDetail.api_base_url}> <CodeGroup title="コード" targetCode={props.appDetail.api_base_url}>
```javascript ```javascript
``` ```
@ -14,10 +14,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
### 認証 ### 認証
サービスAPIは`API-Key`認証を使用します。 サービス API `API-Key` 認証を使用します。
<i>**APIキーはサーバー側に保存し、クライアント側で共有または保存しないことを強くお勧めします。APIキーの漏洩は深刻な結果を招く可能性があります。**</i> <i>**API キーはサーバー側に保存し、クライアント側で共有または保存しないことを強くお勧めします。API キーの漏洩は深刻な結果を招く可能性があります。**</i>
すべてのAPIリクエストには、以下のように`Authorization`HTTPヘッダーにAPIキーを含めてください すべての API リクエストには、以下のように `Authorization`HTTP ヘッダーに API キーを含めてください:
<CodeGroup title="コード"> <CodeGroup title="コード">
```javascript ```javascript
@ -327,25 +327,25 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
ユーザー識別子、開発者のルールによって定義され、アプリケーション内で一意でなければなりません。 ユーザー識別子、開発者のルールによって定義され、アプリケーション内で一意でなければなりません。
### 応答 ### 応答
アップロードが成功すると、サーバーはファイルのIDと関連情報を返します。 アップロードが成功すると、サーバーはファイルの ID と関連情報を返します。
- `id` (uuid) ID - `id` (uuid) ID
- `name` (string) ファイル名 - `name` (string) ファイル名
- `size` (int) ファイルサイズ(バイト) - `size` (int) ファイルサイズ(バイト)
- `extension` (string) ファイル拡張子 - `extension` (string) ファイル拡張子
- `mime_type` (string) ファイルのMIMEタイプ - `mime_type` (string) ファイルの MIME タイプ
- `created_by` (uuid) エンドユーザーID - `created_by` (uuid) エンドユーザーID
- `created_at` (timestamp) 作成タイムスタンプ、例1705395332 - `created_at` (timestamp) 作成タイムスタンプ、例1705395332
### エラー ### エラー
- 400, `no_file_uploaded`, ファイルが提供されなければなりません - 400, `no_file_uploaded`, ファイルが提供されなければなりません
- 400, `too_many_files`, 現在は1つのファイルのみ受け付けます - 400, `too_many_files`, 現在は 1 つのファイルのみ受け付けます
- 400, `unsupported_preview`, ファイルはプレビューをサポートしていません - 400, `unsupported_preview`, ファイルはプレビューをサポートしていません
- 400, `unsupported_estimate`, ファイルは推定をサポートしていません - 400, `unsupported_estimate`, ファイルは推定をサポートしていません
- 413, `file_too_large`, ファイルが大きすぎます - 413, `file_too_large`, ファイルが大きすぎます
- 415, `unsupported_file_type`, サポートされていない拡張子、現在はドキュメントファイルのみ受け付けます - 415, `unsupported_file_type`, サポートされていない拡張子、現在はドキュメントファイルのみ受け付けます
- 503, `s3_connection_failed`, S3サービスに接続できません - 503, `s3_connection_failed`, S3 サービスに接続できません
- 503, `s3_permission_denied`, S3にファイルをアップロードする権限がありません - 503, `s3_permission_denied`, S3 にファイルをアップロードする権限がありません
- 503, `s3_file_too_large`, ファイルがS3のサイズ制限を超えています - 503, `s3_file_too_large`, ファイルが S3 のサイズ制限を超えています
- 500, 内部サーバーエラー - 500, 内部サーバーエラー
@ -391,7 +391,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<Col> <Col>
ストリーミングモードでのみサポートされています。 ストリーミングモードでのみサポートされています。
### パス ### パス
- `task_id` (string) タスクID、ストリーミングチャンクの返り値から取得できます - `task_id` (string) タスク ID、ストリーミングチャンクの返り値から取得できます
### リクエストボディ ### リクエストボディ
- `user` (string) 必須 - `user` (string) 必須
ユーザー識別子、エンドユーザーの身元を定義するために使用され、送信メッセージインターフェースで渡されたユーザーと一致している必要があります。 ユーザー識別子、エンドユーザーの身元を定義するために使用され、送信メッセージインターフェースで渡されたユーザーと一致している必要があります。
@ -712,7 +712,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
/> />
<Row> <Row>
<Col> <Col>
現在のユーザーの会話リストを取得し、デフォルトで最新の20件を返します。 現在のユーザーの会話リストを取得し、デフォルトで最新の 20 件を返します。
### クエリ ### クエリ
@ -943,7 +943,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `limit` (int) ページごとのアイテム数 - `limit` (int) ページごとのアイテム数
- `has_more` (bool) さらにアイテムがあるかどうか - `has_more` (bool) さらにアイテムがあるかどうか
- `data` (array[object]) 変数のリスト - `data` (array[object]) 変数のリスト
- `id` (string) 変数ID - `id` (string) 変数 ID
- `name` (string) 変数名 - `name` (string) 変数名
- `value_type` (string) 変数タイプ(文字列、数値、真偽値など) - `value_type` (string) 変数タイプ(文字列、数値、真偽値など)
- `value` (string) 変数値 - `value` (string) 変数値
@ -1014,7 +1014,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
/> />
<Row> <Row>
<Col> <Col>
このエンドポイントはmultipart/form-dataリクエストを必要とします。 このエンドポイントは multipart/form-data リクエストを必要とします。
### リクエストボディ ### リクエストボディ
@ -1288,9 +1288,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `tool_name` (string) - `tool_name` (string)
- `icon` (object|string) - `icon` (object|string)
- (object) アイコンオブジェクト - (object) アイコンオブジェクト
- `background` (string) 背景色16進数形式 - `background` (string) 背景色16 進数形式)
- `content`(string) 絵文字 - `content`(string) 絵文字
- (string) アイコンのURL - (string) アイコンの URL
</Col> </Col>
<Col> <Col>
<CodeGroup title="リクエスト" tag="GET" label="/meta" targetCode={`curl -X GET '${props.appDetail.api_base_url}/meta' \\\n-H 'Authorization: Bearer {api_key}'`}> <CodeGroup title="リクエスト" tag="GET" label="/meta" targetCode={`curl -X GET '${props.appDetail.api_base_url}/meta' \\\n-H 'Authorization: Bearer {api_key}'`}>
@ -1327,22 +1327,22 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
/> />
<Row> <Row>
<Col> <Col>
アプリのWebApp設定を取得するために使用します。 アプリの WebApp 設定を取得するために使用します。
### 応答 ### 応答
- `title` (string) WebApp名 - `title` (string) WebApp
- `chat_color_theme` (string) チャットの色テーマ、16進数形式 - `chat_color_theme` (string) チャットの色テーマ、16 進数形式
- `chat_color_theme_inverted` (bool) チャットの色テーマを反転するかどうか - `chat_color_theme_inverted` (bool) チャットの色テーマを反転するかどうか
- `icon_type` (string) アイコンタイプ、`emoji`-絵文字、`image`-画像 - `icon_type` (string) アイコンタイプ、`emoji`-絵文字、`image`-画像
- `icon` (string) アイコン。`emoji`タイプの場合は絵文字、`image`タイプの場合は画像URL - `icon` (string) アイコン。`emoji`タイプの場合は絵文字、`image`タイプの場合は画像 URL
- `icon_background` (string) 16進数形式の背景色 - `icon_background` (string) 16 進数形式の背景色
- `icon_url` (string) アイコンのURL - `icon_url` (string) アイコンの URL
- `description` (string) 説明 - `description` (string) 説明
- `copyright` (string) 著作権情報 - `copyright` (string) 著作権情報
- `privacy_policy` (string) プライバシーポリシーのリンク - `privacy_policy` (string) プライバシーポリシーのリンク
- `custom_disclaimer` (string) カスタム免責事項 - `custom_disclaimer` (string) カスタム免責事項
- `default_language` (string) デフォルト言語 - `default_language` (string) デフォルト言語
- `show_workflow_steps` (bool) ワークフローの詳細を表示するかどうか - `show_workflow_steps` (bool) ワークフローの詳細を表示するかどうか
- `use_icon_as_answer_icon` (bool) WebAppのアイコンをチャット内の🤖に置き換えるかどうか - `use_icon_as_answer_icon` (bool) WebApp のアイコンをチャット内の🤖に置き換えるかどうか
</Col> </Col>
<Col> <Col>
<CodeGroup title="Request" tag="POST" label="/meta" targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\\n-H 'Authorization: Bearer {api_key}'`}> <CodeGroup title="Request" tag="POST" label="/meta" targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\\n-H 'Authorization: Bearer {api_key}'`}>

@ -981,7 +981,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
- `limit` (int) 每页项目数 - `limit` (int) 每页项目数
- `has_more` (bool) 是否有更多项目 - `has_more` (bool) 是否有更多项目
- `data` (array[object]) 变量列表 - `data` (array[object]) 变量列表
- `id` (string) 变量ID - `id` (string) 变量 ID
- `name` (string) 变量名称 - `name` (string) 变量名称
- `value_type` (string) 变量类型(字符串、数字、布尔等) - `value_type` (string) 变量类型(字符串、数字、布尔等)
- `value` (string) 变量值 - `value` (string) 变量值
@ -1300,15 +1300,15 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
/> />
<Row> <Row>
<Col> <Col>
用于获取工具icon 用于获取工具 icon
### Response ### Response
- `tool_icons`(object[string]) 工具图标 - `tool_icons`(object[string]) 工具图标
- `工具名称` (string) - `工具名称` (string)
- `icon` (object|string) - `icon` (object|string)
- (object) 图标 - (object) 图标
- `background` (string) hex格式的背景色 - `background` (string) hex 格式的背景色
- `content`(string) emoji - `content`(string) emoji
- (string) 图标URL - (string) 图标 URL
</Col> </Col>
<Col> <Col>
<CodeGroup title="Request" tag="POST" label="/meta" targetCode={`curl -X GET '${props.appDetail.api_base_url}/meta' \\\n-H 'Authorization: Bearer {api_key}'`}> <CodeGroup title="Request" tag="POST" label="/meta" targetCode={`curl -X GET '${props.appDetail.api_base_url}/meta' \\\n-H 'Authorization: Bearer {api_key}'`}>
@ -1347,10 +1347,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
用于获取应用的 WebApp 设置 用于获取应用的 WebApp 设置
### Response ### Response
- `title` (string) WebApp 名称 - `title` (string) WebApp 名称
- `chat_color_theme` (string) 聊天颜色主题, hex 格式 - `chat_color_theme` (string) 聊天颜色主题hex 格式
- `chat_color_theme_inverted` (bool) 聊天颜色主题是否反转 - `chat_color_theme_inverted` (bool) 聊天颜色主题是否反转
- `icon_type` (string) 图标类型, `emoji`-表情, `image`-图片 - `icon_type` (string) 图标类型`emoji`-表情,`image`-图片
- `icon` (string) 图标, 如果是 `emoji` 类型, 则是 emoji 表情符号, 如果是 `image` 类型, 则是图片 URL - `icon` (string) 图标,如果是 `emoji` 类型,则是 emoji 表情符号,如果是 `image` 类型,则是图片 URL
- `icon_background` (string) hex 格式的背景色 - `icon_background` (string) hex 格式的背景色
- `icon_url` (string) 图标 URL - `icon_url` (string) 图标 URL
- `description` (string) 描述 - `description` (string) 描述
@ -1604,10 +1604,10 @@ ___
动作,只能是 'enable' 或 'disable' 动作,只能是 'enable' 或 'disable'
</Property> </Property>
<Property name='embedding_provider_name' type='string' key='embedding_provider_name'> <Property name='embedding_provider_name' type='string' key='embedding_provider_name'>
指定的嵌入模型提供商, 必须先在系统内设定好接入的模型对应的是provider字段 指定的嵌入模型提供商必须先在系统内设定好接入的模型,对应的是 provider 字段
</Property> </Property>
<Property name='embedding_model_name' type='string' key='embedding_model_name'> <Property name='embedding_model_name' type='string' key='embedding_model_name'>
指定的嵌入模型对应的是model字段 指定的嵌入模型,对应的是 model 字段
</Property> </Property>
<Property name='score_threshold' type='number' key='score_threshold'> <Property name='score_threshold' type='number' key='score_threshold'>
相似度阈值,当相似度大于该阈值时,系统会自动回复,否则不回复 相似度阈值,当相似度大于该阈值时,系统会自动回复,否则不回复
@ -1615,7 +1615,7 @@ ___
</Properties> </Properties>
</Col> </Col>
<Col sticky> <Col sticky>
嵌入模型的提供商和模型名称可以通过以下接口获取v1/workspaces/current/models/model-types/text-embedding 具体见:通过 API 维护知识库。 使用的Authorization是Dataset的API Token。 嵌入模型的提供商和模型名称可以通过以下接口获取v1/workspaces/current/models/model-types/text-embedding具体见通过 API 维护知识库。使用的 Authorization Dataset API Token。
<CodeGroup <CodeGroup
title="Request" title="Request"
tag="POST" tag="POST"

@ -1,12 +1,12 @@
import { CodeGroup } from '../code.tsx' import { CodeGroup } from '../code.tsx'
import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx' import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx'
# チャットアプリAPI # チャットアプリ API
チャットアプリケーションはセッションの持続性をサポートしており、以前のチャット履歴を応答のコンテキストとして使用できます。これは、チャットボットやカスタマーサービスAIなどに適用できます。 チャットアプリケーションはセッションの持続性をサポートしており、以前のチャット履歴を応答のコンテキストとして使用できます。これは、チャットボットやカスタマーサービス AI などに適用できます。
<div> <div>
### ベースURL ### ベース URL
<CodeGroup title="コード" targetCode={props.appDetail.api_base_url}> <CodeGroup title="コード" targetCode={props.appDetail.api_base_url}>
```javascript ```javascript
``` ```
@ -14,10 +14,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
### 認証 ### 認証
サービスAPIは`API-Key`認証を使用します。 サービス API `API-Key` 認証を使用します。
<i>**APIキーの漏洩を防ぐため、APIキーはクライアント側で共有または保存せず、サーバー側で保存することを強くお勧めします。**</i> <i>**API キーの漏洩を防ぐため、API キーはクライアント側で共有または保存せず、サーバー側で保存することを強くお勧めします。**</i>
すべてのAPIリクエストにおいて、以下のように`Authorization`HTTPヘッダーにAPIキーを含めてください すべての API リクエストにおいて、以下のように `Authorization`HTTP ヘッダーに API キーを含めてください:
<CodeGroup title="コード"> <CodeGroup title="コード">
```javascript ```javascript
@ -279,7 +279,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<Row> <Row>
<Col> <Col>
メッセージ送信時に使用するためのファイルをアップロードします(現在は画像のみサポート)。画像とテキストのマルチモーダル理解を可能にします。 メッセージ送信時に使用するためのファイルをアップロードします(現在は画像のみサポート)。画像とテキストのマルチモーダル理解を可能にします。
png、jpg、jpeg、webp、gif形式をサポートしています。 png、jpg、jpeg、webp、gif 形式をサポートしています。
アップロードされたファイルは現在のエンドユーザーのみが使用できます。 アップロードされたファイルは現在のエンドユーザーのみが使用できます。
### リクエストボディ ### リクエストボディ
@ -290,25 +290,25 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。 ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。
### 応答 ### 応答
アップロードが成功すると、サーバーはファイルのIDと関連情報を返します。 アップロードが成功すると、サーバーはファイルの ID と関連情報を返します。
- `id` (uuid) ID - `id` (uuid) ID
- `name` (string) ファイル名 - `name` (string) ファイル名
- `size` (int) ファイルサイズ(バイト) - `size` (int) ファイルサイズ(バイト)
- `extension` (string) ファイル拡張子 - `extension` (string) ファイル拡張子
- `mime_type` (string) ファイルのMIMEタイプ - `mime_type` (string) ファイルの MIME タイプ
- `created_by` (uuid) エンドユーザーID - `created_by` (uuid) エンドユーザーID
- `created_at` (timestamp) 作成タイムスタンプ、例1705395332 - `created_at` (timestamp) 作成タイムスタンプ、例1705395332
### エラー ### エラー
- 400, `no_file_uploaded`, ファイルが提供されなければなりません - 400, `no_file_uploaded`, ファイルが提供されなければなりません
- 400, `too_many_files`, 現在は1つのファイルのみ受け付けます - 400, `too_many_files`, 現在は 1 つのファイルのみ受け付けます
- 400, `unsupported_preview`, ファイルはプレビューをサポートしていません - 400, `unsupported_preview`, ファイルはプレビューをサポートしていません
- 400, `unsupported_estimate`, ファイルは推定をサポートしていません - 400, `unsupported_estimate`, ファイルは推定をサポートしていません
- 413, `file_too_large`, ファイルが大きすぎます - 413, `file_too_large`, ファイルが大きすぎます
- 415, `unsupported_file_type`, サポートされていない拡張子、現在はドキュメントファイルのみ受け付けます - 415, `unsupported_file_type`, サポートされていない拡張子、現在はドキュメントファイルのみ受け付けます
- 503, `s3_connection_failed`, S3サービスに接続できません - 503, `s3_connection_failed`, S3 サービスに接続できません
- 503, `s3_permission_denied`, S3にファイルをアップロードする権限がありません - 503, `s3_permission_denied`, S3 にファイルをアップロードする権限がありません
- 503, `s3_file_too_large`, ファイルがS3のサイズ制限を超えています - 503, `s3_file_too_large`, ファイルが S3 のサイズ制限を超えています
- 500, 内部サーバーエラー - 500, 内部サーバーエラー
@ -354,7 +354,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<Col> <Col>
ストリーミングモードでのみサポートされています。 ストリーミングモードでのみサポートされています。
### パス ### パス
- `task_id` (string) タスクID、ストリーミングチャンクの返り値から取得できます - `task_id` (string) タスク ID、ストリーミングチャンクの返り値から取得できます
### リクエストボディ ### リクエストボディ
- `user` (string) 必須 - `user` (string) 必須
ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用され、メッセージ送信インターフェースで渡されたユーザーと一致している必要があります。 ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用され、メッセージ送信インターフェースで渡されたユーザーと一致している必要があります。
@ -745,7 +745,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
/> />
<Row> <Row>
<Col> <Col>
現在のユーザーの会話リストを取得し、デフォルトで最新の20件を返します。 現在のユーザーの会話リストを取得し、デフォルトで最新の 20 件を返します。
### クエリ ### クエリ
@ -975,7 +975,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `limit` (int) ページごとのアイテム数 - `limit` (int) ページごとのアイテム数
- `has_more` (bool) さらにアイテムがあるかどうか - `has_more` (bool) さらにアイテムがあるかどうか
- `data` (array[object]) 変数のリスト - `data` (array[object]) 変数のリスト
- `id` (string) 変数ID - `id` (string) 変数 ID
- `name` (string) 変数名 - `name` (string) 変数名
- `value_type` (string) 変数タイプ(文字列、数値、真偽値など) - `value_type` (string) 変数タイプ(文字列、数値、真偽値など)
- `value` (string) 変数値 - `value` (string) 変数値
@ -1046,7 +1046,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
/> />
<Row> <Row>
<Col> <Col>
このエンドポイントはmultipart/form-dataリクエストを必要とします。 このエンドポイントは multipart/form-data リクエストを必要とします。
### リクエストボディ ### リクエストボディ
@ -1315,9 +1315,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `tool_name` (string) - `tool_name` (string)
- `icon` (object|string) - `icon` (object|string)
- (object) アイコンオブジェクト - (object) アイコンオブジェクト
- `background` (string) 背景色16進数形式 - `background` (string) 背景色16 進数形式)
- `content`(string) 絵文字 - `content`(string) 絵文字
- (string) アイコンのURL - (string) アイコンの URL
</Col> </Col>
<Col> <Col>
<CodeGroup title="リクエスト" tag="GET" label="/meta" targetCode={`curl -X GET '${props.appDetail.api_base_url}/meta' \\\n-H 'Authorization: Bearer {api_key}'`}> <CodeGroup title="リクエスト" tag="GET" label="/meta" targetCode={`curl -X GET '${props.appDetail.api_base_url}/meta' \\\n-H 'Authorization: Bearer {api_key}'`}>
@ -1354,22 +1354,22 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
/> />
<Row> <Row>
<Col> <Col>
アプリのWebApp設定を取得するために使用します。 アプリの WebApp 設定を取得するために使用します。
### 応答 ### 応答
- `title` (string) WebApp名 - `title` (string) WebApp
- `chat_color_theme` (string) チャットの色テーマ、16進数形式 - `chat_color_theme` (string) チャットの色テーマ、16 進数形式
- `chat_color_theme_inverted` (bool) チャットの色テーマを反転するかどうか - `chat_color_theme_inverted` (bool) チャットの色テーマを反転するかどうか
- `icon_type` (string) アイコンタイプ、`emoji`-絵文字、`image`-画像 - `icon_type` (string) アイコンタイプ、`emoji`-絵文字、`image`-画像
- `icon` (string) アイコン。`emoji`タイプの場合は絵文字、`image`タイプの場合は画像URL - `icon` (string) アイコン。`emoji`タイプの場合は絵文字、`image`タイプの場合は画像 URL
- `icon_background` (string) 16進数形式の背景色 - `icon_background` (string) 16 進数形式の背景色
- `icon_url` (string) アイコンのURL - `icon_url` (string) アイコンの URL
- `description` (string) 説明 - `description` (string) 説明
- `copyright` (string) 著作権情報 - `copyright` (string) 著作権情報
- `privacy_policy` (string) プライバシーポリシーのリンク - `privacy_policy` (string) プライバシーポリシーのリンク
- `custom_disclaimer` (string) カスタム免責事項 - `custom_disclaimer` (string) カスタム免責事項
- `default_language` (string) デフォルト言語 - `default_language` (string) デフォルト言語
- `show_workflow_steps` (bool) ワークフローの詳細を表示するかどうか - `show_workflow_steps` (bool) ワークフローの詳細を表示するかどうか
- `use_icon_as_answer_icon` (bool) WebAppのアイコンをチャット内の🤖に置き換えるかどうか - `use_icon_as_answer_icon` (bool) WebApp のアイコンをチャット内の🤖に置き換えるかどうか
</Col> </Col>
<Col> <Col>
<CodeGroup title="Request" tag="POST" label="/meta" targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\\n-H 'Authorization: Bearer {api_key}'`}> <CodeGroup title="Request" tag="POST" label="/meta" targetCode={`curl -X GET '${props.appDetail.api_base_url}/site' \\\n-H 'Authorization: Bearer {api_key}'`}>

@ -991,7 +991,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
- `limit` (int) 每页项目数 - `limit` (int) 每页项目数
- `has_more` (bool) 是否有更多项目 - `has_more` (bool) 是否有更多项目
- `data` (array[object]) 变量列表 - `data` (array[object]) 变量列表
- `id` (string) 变量ID - `id` (string) 变量 ID
- `name` (string) 变量名称 - `name` (string) 变量名称
- `value_type` (string) 变量类型(字符串、数字、布尔等) - `value_type` (string) 变量类型(字符串、数字、布尔等)
- `value` (string) 变量值 - `value` (string) 变量值
@ -1305,15 +1305,15 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
/> />
<Row> <Row>
<Col> <Col>
用于获取工具icon 用于获取工具 icon
### Response ### Response
- `tool_icons`(object[string]) 工具图标 - `tool_icons`(object[string]) 工具图标
- `工具名称` (string) - `工具名称` (string)
- `icon` (object|string) - `icon` (object|string)
- (object) 图标 - (object) 图标
- `background` (string) hex格式的背景色 - `background` (string) hex 格式的背景色
- `content`(string) emoji - `content`(string) emoji
- (string) 图标URL - (string) 图标 URL
</Col> </Col>
<Col> <Col>
<CodeGroup title="Request" tag="POST" label="/meta" targetCode={`curl -X GET '${props.appDetail.api_base_url}/meta' \\\n-H 'Authorization: Bearer {api_key}'`}> <CodeGroup title="Request" tag="POST" label="/meta" targetCode={`curl -X GET '${props.appDetail.api_base_url}/meta' \\\n-H 'Authorization: Bearer {api_key}'`}>
@ -1353,10 +1353,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
用于获取应用的 WebApp 设置 用于获取应用的 WebApp 设置
### Response ### Response
- `title` (string) WebApp 名称 - `title` (string) WebApp 名称
- `chat_color_theme` (string) 聊天颜色主题, hex 格式 - `chat_color_theme` (string) 聊天颜色主题hex 格式
- `chat_color_theme_inverted` (bool) 聊天颜色主题是否反转 - `chat_color_theme_inverted` (bool) 聊天颜色主题是否反转
- `icon_type` (string) 图标类型, `emoji`-表情, `image`-图片 - `icon_type` (string) 图标类型`emoji`-表情,`image`-图片
- `icon` (string) 图标, 如果是 `emoji` 类型, 则是 emoji 表情符号, 如果是 `image` 类型, 则是图片 URL - `icon` (string) 图标,如果是 `emoji` 类型,则是 emoji 表情符号,如果是 `image` 类型,则是图片 URL
- `icon_background` (string) hex 格式的背景色 - `icon_background` (string) hex 格式的背景色
- `icon_url` (string) 图标 URL - `icon_url` (string) 图标 URL
- `description` (string) 描述 - `description` (string) 描述

@ -1,12 +1,12 @@
import { CodeGroup } from '../code.tsx' import { CodeGroup } from '../code.tsx'
import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx' import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx'
# ワークフローアプリAPI # ワークフローアプリ API
ワークフローアプリケーションは、セッションをサポートせず、翻訳、記事作成、要約AIなどに最適です。 ワークフローアプリケーションは、セッションをサポートせず、翻訳、記事作成、要約 AI などに最適です。
<div> <div>
### ベースURL ### ベース URL
<CodeGroup title="コード" targetCode={props.appDetail.api_base_url}> <CodeGroup title="コード" targetCode={props.appDetail.api_base_url}>
```javascript ```javascript
``` ```
@ -14,10 +14,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
### 認証 ### 認証
サービスAPIは`API-Key`認証を使用します。 サービス API `API-Key` 認証を使用します。
<i>**APIキーの漏洩を防ぐため、APIキーはクライアント側で共有または保存せず、サーバー側で保存することを強くお勧めします。**</i> <i>**API キーの漏洩を防ぐため、API キーはクライアント側で共有または保存せず、サーバー側で保存することを強くお勧めします。**</i>
すべてのAPIリクエストにおいて、以下のように`Authorization`HTTPヘッダーにAPIキーを含めてください すべての API リクエストにおいて、以下のように `Authorization`HTTP ヘッダーに API キーを含めてください:
<CodeGroup title="コード"> <CodeGroup title="コード">
```javascript ```javascript
@ -61,7 +61,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
応答の返却モードを指定します。サポートされているモード: 応答の返却モードを指定します。サポートされているモード:
- `streaming` ストリーミングモード推奨、SSE[Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events))を通じてタイプライターのような出力を実装します。 - `streaming` ストリーミングモード推奨、SSE[Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events))を通じてタイプライターのような出力を実装します。
- `blocking` ブロッキングモード、実行完了後に結果を返します。(プロセスが長い場合、リクエストが中断される可能性があります) - `blocking` ブロッキングモード、実行完了後に結果を返します。(プロセスが長い場合、リクエストが中断される可能性があります)
<i>Cloudflareの制限により、100秒後に応答がない場合、リクエストは中断されます。</i> <i>Cloudflare の制限により、100 秒後に応答がない場合、リクエストは中断されます。</i>
- `user` (string) 必須 - `user` (string) 必須
ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用されます。 ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用されます。
アプリケーション内で開発者によって一意に定義される必要があります。 アプリケーション内で開発者によって一意に定義される必要があります。
@ -69,28 +69,28 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
### 応答 ### 応答
`response_mode`が`blocking`の場合、CompletionResponseオブジェクトを返します。 `response_mode`が`blocking`の場合、CompletionResponse オブジェクトを返します。
`response_mode`が`streaming`の場合、ChunkCompletionResponseストリームを返します。 `response_mode`が`streaming`の場合、ChunkCompletionResponse ストリームを返します。
### CompletionResponse ### CompletionResponse
アプリの結果を返します。`Content-Type`は`application/json`です。 アプリの結果を返します。`Content-Type`は`application/json`です。
- `workflow_run_id` (string) ワークフロー実行の一意のID - `workflow_run_id` (string) ワークフロー実行の一意の ID
- `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 - `task_id` (string) タスク ID、リクエスト追跡と以下の Stop Generate API に使用
- `data` (object) 結果の詳細 - `data` (object) 結果の詳細
- `id` (string) ワークフロー実行のID - `id` (string) ワークフロー実行の ID
- `workflow_id` (string) 関連するワークフローのID - `workflow_id` (string) 関連するワークフローの ID
- `status` (string) 実行のステータス、`running` / `succeeded` / `failed` / `stopped` - `status` (string) 実行のステータス、`running` / `succeeded` / `failed` / `stopped`
- `outputs` (json) オプションの出力内容 - `outputs` (json) オプションの出力内容
- `error` (string) オプションのエラー理由 - `error` (string) オプションのエラー理由
- `elapsed_time` (float) オプションの使用時間(秒) - `elapsed_time` (float) オプションの使用時間(秒)
- `total_tokens` (int) オプションの使用トークン数 - `total_tokens` (int) オプションの使用トークン数
- `total_steps` (int) デフォルト0 - `total_steps` (int) デフォルト 0
- `created_at` (timestamp) 開始時間 - `created_at` (timestamp) 開始時間
- `finished_at` (timestamp) 終了時間 - `finished_at` (timestamp) 終了時間
### ChunkCompletionResponse ### ChunkCompletionResponse
アプリによって出力されたストリームチャンクを返します。`Content-Type`は`text/event-stream`です。 アプリによって出力されたストリームチャンクを返します。`Content-Type`は`text/event-stream`です。
各ストリーミングチャンクは`data:`で始まり、2つの改行文字`\n\n`で区切られます。以下のように表示されます: 各ストリーミングチャンクは`data:`で始まり、2 つの改行文字`\n\n`で区切られます。以下のように表示されます:
<CodeGroup> <CodeGroup>
```streaming {{ title: '応答' }} ```streaming {{ title: '応答' }}
data: {"event": "text_chunk", "workflow_run_id": "b85e5fc5-751b-454d-b14e-dc5f240b0a31", "task_id": "bd029338-b068-4d34-a331-fc85478922c2", "data": {"text": "\u4e3a\u4e86", "from_variable_selector": ["1745912968134", "text"]}}\n\n data: {"event": "text_chunk", "workflow_run_id": "b85e5fc5-751b-454d-b14e-dc5f240b0a31", "task_id": "bd029338-b068-4d34-a331-fc85478922c2", "data": {"text": "\u4e3a\u4e86", "from_variable_selector": ["1745912968134", "text"]}}\n\n
@ -98,45 +98,45 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</CodeGroup> </CodeGroup>
ストリーミングチャンクの構造は`event`に応じて異なります: ストリーミングチャンクの構造は`event`に応じて異なります:
- `event: workflow_started` ワークフローが実行を開始 - `event: workflow_started` ワークフローが実行を開始
- `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 - `task_id` (string) タスク ID、リクエスト追跡と以下の Stop Generate API に使用
- `workflow_run_id` (string) ワークフロー実行の一意のID - `workflow_run_id` (string) ワークフロー実行の一意の ID
- `event` (string) `workflow_started`に固定 - `event` (string) `workflow_started`に固定
- `data` (object) 詳細 - `data` (object) 詳細
- `id` (string) ワークフロー実行の一意のID - `id` (string) ワークフロー実行の一意の ID
- `workflow_id` (string) 関連するワークフローのID - `workflow_id` (string) 関連するワークフローの ID
- `sequence_number` (int) 自己増加シリアル番号、アプリ内で自己増加し、1から始まります - `sequence_number` (int) 自己増加シリアル番号、アプリ内で自己増加し、1 から始まります
- `created_at` (timestamp) 作成タイムスタンプ、例1705395332 - `created_at` (timestamp) 作成タイムスタンプ、例1705395332
- `event: node_started` ノード実行開始 - `event: node_started` ノード実行開始
- `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 - `task_id` (string) タスク ID、リクエスト追跡と以下の Stop Generate API に使用
- `workflow_run_id` (string) ワークフロー実行の一意のID - `workflow_run_id` (string) ワークフロー実行の一意の ID
- `event` (string) `node_started`に固定 - `event` (string) `node_started`に固定
- `data` (object) 詳細 - `data` (object) 詳細
- `id` (string) ワークフロー実行の一意のID - `id` (string) ワークフロー実行の一意の ID
- `node_id` (string) ードのID - `node_id` (string) ノードの ID
- `node_type` (string) ノードのタイプ - `node_type` (string) ノードのタイプ
- `title` (string) ノードの名前 - `title` (string) ノードの名前
- `index` (int) 実行シーケンス番号、トレースノードシーケンスを表示するために使用 - `index` (int) 実行シーケンス番号、トレースノードシーケンスを表示するために使用
- `predecessor_node_id` (string) オプションのプレフィックスードID、キャンバス表示実行パスに使用 - `predecessor_node_id` (string) オプションのプレフィックスノード ID、キャンバス表示実行パスに使用
- `inputs` (object) ノードで使用されるすべての前のノード変数の内容 - `inputs` (object) ノードで使用されるすべての前のノード変数の内容
- `created_at` (timestamp) 開始のタイムスタンプ、例1705395332 - `created_at` (timestamp) 開始のタイムスタンプ、例1705395332
- `event: text_chunk` テキストフラグメント - `event: text_chunk` テキストフラグメント
- `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 - `task_id` (string) タスク ID、リクエスト追跡と以下の Stop Generate API に使用
- `workflow_run_id` (string) ワークフロー実行の一意のID - `workflow_run_id` (string) ワークフロー実行の一意の ID
- `event` (string) `text_chunk`に固定 - `event` (string) `text_chunk`に固定
- `data` (object) 詳細 - `data` (object) 詳細
- `text` (string) テキスト内容 - `text` (string) テキスト内容
- `from_variable_selector` (array) テキスト生成元パス(開発者がどのノードのどの変数から生成されたかを理解するための情報) - `from_variable_selector` (array) テキスト生成元パス(開発者がどのノードのどの変数から生成されたかを理解するための情報)
- `event: node_finished` ノード実行終了、同じイベントで異なる状態で成功または失敗 - `event: node_finished` ノード実行終了、同じイベントで異なる状態で成功または失敗
- `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 - `task_id` (string) タスク ID、リクエスト追跡と以下の Stop Generate API に使用
- `workflow_run_id` (string) ワークフロー実行の一意のID - `workflow_run_id` (string) ワークフロー実行の一意の ID
- `event` (string) `node_finished`に固定 - `event` (string) `node_finished`に固定
- `data` (object) 詳細 - `data` (object) 詳細
- `id` (string) ワークフロー実行の一意のID - `id` (string) ワークフロー実行の一意の ID
- `node_id` (string) ードのID - `node_id` (string) ノードの ID
- `node_type` (string) ノードのタイプ - `node_type` (string) ノードのタイプ
- `title` (string) ノードの名前 - `title` (string) ノードの名前
- `index` (int) 実行シーケンス番号、トレースノードシーケンスを表示するために使用 - `index` (int) 実行シーケンス番号、トレースノードシーケンスを表示するために使用
- `predecessor_node_id` (string) オプションのプレフィックスードID、キャンバス表示実行パスに使用 - `predecessor_node_id` (string) オプションのプレフィックスノード ID、キャンバス表示実行パスに使用
- `inputs` (object) ノードで使用されるすべての前のノード変数の内容 - `inputs` (object) ノードで使用されるすべての前のノード変数の内容
- `process_data` (json) オプションのノードプロセスデータ - `process_data` (json) オプションのノードプロセスデータ
- `outputs` (json) オプションの出力内容 - `outputs` (json) オプションの出力内容
@ -149,31 +149,31 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `currency` (string) オプション 例:`USD` / `RMB` - `currency` (string) オプション 例:`USD` / `RMB`
- `created_at` (timestamp) 開始のタイムスタンプ、例1705395332 - `created_at` (timestamp) 開始のタイムスタンプ、例1705395332
- `event: workflow_finished` ワークフロー実行終了、同じイベントで異なる状態で成功または失敗 - `event: workflow_finished` ワークフロー実行終了、同じイベントで異なる状態で成功または失敗
- `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 - `task_id` (string) タスク ID、リクエスト追跡と以下の Stop Generate API に使用
- `workflow_run_id` (string) ワークフロー実行の一意のID - `workflow_run_id` (string) ワークフロー実行の一意の ID
- `event` (string) `workflow_finished`に固定 - `event` (string) `workflow_finished`に固定
- `data` (object) 詳細 - `data` (object) 詳細
- `id` (string) ワークフロー実行のID - `id` (string) ワークフロー実行の ID
- `workflow_id` (string) 関連するワークフローのID - `workflow_id` (string) 関連するワークフローの ID
- `status` (string) 実行のステータス、`running` / `succeeded` / `failed` / `stopped` - `status` (string) 実行のステータス、`running` / `succeeded` / `failed` / `stopped`
- `outputs` (json) オプションの出力内容 - `outputs` (json) オプションの出力内容
- `error` (string) オプションのエラー理由 - `error` (string) オプションのエラー理由
- `elapsed_time` (float) オプションの使用時間(秒) - `elapsed_time` (float) オプションの使用時間(秒)
- `total_tokens` (int) オプションの使用トークン数 - `total_tokens` (int) オプションの使用トークン数
- `total_steps` (int) デフォルト0 - `total_steps` (int) デフォルト 0
- `created_at` (timestamp) 開始時間 - `created_at` (timestamp) 開始時間
- `finished_at` (timestamp) 終了時間 - `finished_at` (timestamp) 終了時間
- `event: tts_message` TTSオーディオストリームイベント、つまり音声合成出力。内容はMp3形式のオーディオブロックで、base64文字列としてエンコードされています。再生時には、base64をデコードしてプレーヤーに入力するだけです。このメッセージは自動再生が有効な場合にのみ利用可能 - `event: tts_message` TTS オーディオストリームイベント、つまり音声合成出力。内容は Mp3 形式のオーディオブロックで、base64 文字列としてエンコードされています。再生時には、base64 をデコードしてプレーヤーに入力するだけです。(このメッセージは自動再生が有効な場合にのみ利用可能)
- `task_id` (string) タスクID、リクエスト追跡と以下の停止応答インターフェースに使用 - `task_id` (string) タスク ID、リクエスト追跡と以下の停止応答インターフェースに使用
- `message_id` (string) 一意のメッセージID - `message_id` (string) 一意のメッセージ ID
- `audio` (string) 音声合成後のオーディオ、base64テキストコンテンツとしてエンコードされており、再生時にはbase64をデコードしてプレーヤーに入力するだけです - `audio` (string) 音声合成後のオーディオ、base64 テキストコンテンツとしてエンコードされており、再生時には base64 をデコードしてプレーヤーに入力するだけです
- `created_at` (int) 作成タイムスタンプ、例1705395332 - `created_at` (int) 作成タイムスタンプ、例1705395332
- `event: tts_message_end` TTSオーディオストリーム終了イベント。このイベントを受信すると、オーディオストリームの終了を示します。 - `event: tts_message_end` TTS オーディオストリーム終了イベント。このイベントを受信すると、オーディオストリームの終了を示します。
- `task_id` (string) タスクID、リクエスト追跡と以下の停止応答インターフェースに使用 - `task_id` (string) タスク ID、リクエスト追跡と以下の停止応答インターフェースに使用
- `message_id` (string) 一意のメッセージID - `message_id` (string) 一意のメッセージ ID
- `audio` (string) 終了イベントにはオーディオがないため、これは空の文字列です - `audio` (string) 終了イベントにはオーディオがないため、これは空の文字列です
- `created_at` (int) 作成タイムスタンプ、例1705395332 - `created_at` (int) 作成タイムスタンプ、例1705395332
- `event: ping` 接続を維持するために10秒ごとに送信されるPingイベント。 - `event: ping` 接続を維持するために 10 秒ごとに送信される Ping イベント。
### エラー ### エラー
- 400, `invalid_param`, 異常なパラメータ入力 - 400, `invalid_param`, 異常なパラメータ入力
@ -342,12 +342,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
/> />
<Row> <Row>
<Col> <Col>
ワークフロー実行IDに基づいて、ワークフロータスクの現在の実行結果を取得します。 ワークフロー実行 ID に基づいて、ワークフロータスクの現在の実行結果を取得します。
### パス ### パス
- `workflow_id` (string) ワークフローID、ストリーミングチャンクの返り値から取得可能 - `workflow_id` (string) ワークフローID、ストリーミングチャンクの返り値から取得可能
### 応答 ### 応答
- `id` (string) ワークフロー実行のID - `id` (string) ワークフロー実行の ID
- `workflow_id` (string) 関連するワークフローのID - `workflow_id` (string) 関連するワークフローの ID
- `status` (string) 実行のステータス、`running` / `succeeded` / `failed` / `stopped` - `status` (string) 実行のステータス、`running` / `succeeded` / `failed` / `stopped`
- `inputs` (json) 入力内容 - `inputs` (json) 入力内容
- `outputs` (json) 出力内容 - `outputs` (json) 出力内容
@ -401,7 +401,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<Col> <Col>
ストリーミングモードでのみサポートされています。 ストリーミングモードでのみサポートされています。
### パス ### パス
- `task_id` (string) タスクID、ストリーミングチャンクの返り値から取得可能 - `task_id` (string) タスク ID、ストリーミングチャンクの返り値から取得可能
### リクエストボディ ### リクエストボディ
- `user` (string) 必須 - `user` (string) 必須
ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用され、送信メッセージインターフェースで渡されたユーザーと一致している必要があります。 ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用され、送信メッセージインターフェースで渡されたユーザーと一致している必要があります。
@ -454,25 +454,25 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。 ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。
### 応答 ### 応答
アップロードが成功すると、サーバーはファイルのIDと関連情報を返します。 アップロードが成功すると、サーバーはファイルの ID と関連情報を返します。
- `id` (uuid) ID - `id` (uuid) ID
- `name` (string) ファイル名 - `name` (string) ファイル名
- `size` (int) ファイルサイズ(バイト) - `size` (int) ファイルサイズ(バイト)
- `extension` (string) ファイル拡張子 - `extension` (string) ファイル拡張子
- `mime_type` (string) ファイルのMIMEタイプ - `mime_type` (string) ファイルの MIME タイプ
- `created_by` (uuid) エンドユーザーID - `created_by` (uuid) エンドユーザーID
- `created_at` (timestamp) 作成タイムスタンプ、例1705395332 - `created_at` (timestamp) 作成タイムスタンプ、例1705395332
### エラー ### エラー
- 400, `no_file_uploaded`, ファイルが提供されていません - 400, `no_file_uploaded`, ファイルが提供されていません
- 400, `too_many_files`, 現在は1つのファイルのみ受け付けています - 400, `too_many_files`, 現在は 1 つのファイルのみ受け付けています
- 400, `unsupported_preview`, ファイルはプレビューをサポートしていません - 400, `unsupported_preview`, ファイルはプレビューをサポートしていません
- 400, `unsupported_estimate`, ファイルは推定をサポートしていません - 400, `unsupported_estimate`, ファイルは推定をサポートしていません
- 413, `file_too_large`, ファイルが大きすぎます - 413, `file_too_large`, ファイルが大きすぎます
- 415, `unsupported_file_type`, サポートされていない拡張子、現在はドキュメントファイルのみ受け付けています - 415, `unsupported_file_type`, サポートされていない拡張子、現在はドキュメントファイルのみ受け付けています
- 503, `s3_connection_failed`, S3サービスに接続できません - 503, `s3_connection_failed`, S3 サービスに接続できません
- 503, `s3_permission_denied`, S3にファイルをアップロードする権限がありません - 503, `s3_permission_denied`, S3 にファイルをアップロードする権限がありません
- 503, `s3_file_too_large`, ファイルがS3のサイズ制限を超えています - 503, `s3_file_too_large`, ファイルが S3 のサイズ制限を超えています
- 500, 内部サーバーエラー - 500, 内部サーバーエラー
@ -550,7 +550,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `error` (string) オプションのエラー理由 - `error` (string) オプションのエラー理由
- `elapsed_time` (float) 使用される総秒数 - `elapsed_time` (float) 使用される総秒数
- `total_tokens` (int) 使用されるトークン数 - `total_tokens` (int) 使用されるトークン数
- `total_steps` (int) デフォルト0 - `total_steps` (int) デフォルト 0
- `created_at` (timestamp) 開始時間 - `created_at` (timestamp) 開始時間
- `finished_at` (timestamp) 終了時間 - `finished_at` (timestamp) 終了時間
- `created_from` (string) 作成元 - `created_from` (string) 作成元
@ -560,7 +560,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `id` (string) ID - `id` (string) ID
- `type` (string) タイプ - `type` (string) タイプ
- `is_anonymous` (bool) 匿名かどうか - `is_anonymous` (bool) 匿名かどうか
- `session_id` (string) セッションID - `session_id` (string) セッション ID
- `created_at` (timestamp) 作成時間 - `created_at` (timestamp) 作成時間
</Col> </Col>
<Col sticky> <Col sticky>
@ -750,13 +750,13 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
/> />
<Row> <Row>
<Col> <Col>
アプリのWebApp設定を取得するために使用します。 アプリの WebApp 設定を取得するために使用します。
### 応答 ### 応答
- `title` (string) WebApp名 - `title` (string) WebApp
- `icon_type` (string) アイコンタイプ、`emoji`-絵文字、`image`-画像 - `icon_type` (string) アイコンタイプ、`emoji`-絵文字、`image`-画像
- `icon` (string) アイコン。`emoji`タイプの場合は絵文字、`image`タイプの場合は画像URL - `icon` (string) アイコン。`emoji`タイプの場合は絵文字、`image`タイプの場合は画像 URL
- `icon_background` (string) 16進数形式の背景色 - `icon_background` (string) 16 進数形式の背景色
- `icon_url` (string) アイコンのURL - `icon_url` (string) アイコンの URL
- `description` (string) 説明 - `description` (string) 説明
- `copyright` (string) 著作権情報 - `copyright` (string) 著作権情報
- `privacy_policy` (string) プライバシーポリシーのリンク - `privacy_policy` (string) プライバシーポリシーのリンク

@ -346,7 +346,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
- `total_tokens` (int) 任务执行总 tokens - `total_tokens` (int) 任务执行总 tokens
- `created_at` (timestamp) 任务开始时间 - `created_at` (timestamp) 任务开始时间
- `finished_at` (timestamp) 任务结束时间 - `finished_at` (timestamp) 任务结束时间
- `elapsed_time` (float) 耗时(s) - `elapsed_time` (float) 耗时 (s)
</Col> </Col>
<Col sticky> <Col sticky>
### Request Example ### Request Example
@ -505,7 +505,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
/> />
<Row> <Row>
<Col> <Col>
倒序返回workflow日志 倒序返回 workflow 日志
### Query ### Query
@ -534,10 +534,10 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
- `workflow_run` (object) Workflow 执行日志 - `workflow_run` (object) Workflow 执行日志
- `id` (string) 标识 - `id` (string) 标识
- `version` (string) 版本 - `version` (string) 版本
- `status` (string) 执行状态, `running` / `succeeded` / `failed` / `stopped` - `status` (string) 执行状态`running` / `succeeded` / `failed` / `stopped`
- `error` (string) (可选) 错误 - `error` (string) (可选) 错误
- `elapsed_time` (float) 耗时,单位秒 - `elapsed_time` (float) 耗时,单位秒
- `total_tokens` (int) 消耗的token数量 - `total_tokens` (int) 消耗的 token 数量
- `total_steps` (int) 执行步骤长度 - `total_steps` (int) 执行步骤长度
- `created_at` (timestamp) 开始时间 - `created_at` (timestamp) 开始时间
- `finished_at` (timestamp) 结束时间 - `finished_at` (timestamp) 结束时间
@ -741,8 +741,8 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
用于获取应用的 WebApp 设置 用于获取应用的 WebApp 设置
### Response ### Response
- `title` (string) WebApp 名称 - `title` (string) WebApp 名称
- `icon_type` (string) 图标类型, `emoji`-表情, `image`-图片 - `icon_type` (string) 图标类型`emoji`-表情,`image`-图片
- `icon` (string) 图标, 如果是 `emoji` 类型, 则是 emoji 表情符号, 如果是 `image` 类型, 则是图片 URL - `icon` (string) 图标,如果是 `emoji` 类型,则是 emoji 表情符号,如果是 `image` 类型,则是图片 URL
- `icon_background` (string) hex 格式的背景色 - `icon_background` (string) hex 格式的背景色
- `icon_url` (string) 图标 URL - `icon_url` (string) 图标 URL
- `description` (string) 描述 - `description` (string) 描述

@ -1,11 +1,10 @@
import { useState } from 'react' import { useState } from 'react'
import { useContext } from 'use-context-selector'
import I18n from '@/context/i18n'
import { X } from '@/app/components/base/icons/src/vender/line/general' import { X } from '@/app/components/base/icons/src/vender/line/general'
import { NOTICE_I18N } from '@/i18n/language' import { NOTICE_I18N } from '@/i18n/language'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
const MaintenanceNotice = () => { const MaintenanceNotice = () => {
const { locale } = useContext(I18n) const locale = useLanguage()
const [showNotice, setShowNotice] = useState(localStorage.getItem('hide-maintenance-notice') !== '1') const [showNotice, setShowNotice] = useState(localStorage.getItem('hide-maintenance-notice') !== '1')
const handleJumpNotice = () => { const handleJumpNotice = () => {

@ -108,7 +108,7 @@ const PluginItem: FC<Props> = ({
}><RiErrorWarningLine color='red' className="ml-0.5 h-4 w-4 shrink-0 text-text-accent" /></Tooltip>} }><RiErrorWarningLine color='red' className="ml-0.5 h-4 w-4 shrink-0 text-text-accent" /></Tooltip>}
<Badge className='ml-1 shrink-0' <Badge className='ml-1 shrink-0'
text={source === PluginSource.github ? plugin.meta!.version : plugin.version} text={source === PluginSource.github ? plugin.meta!.version : plugin.version}
hasRedCornerMark={(source === PluginSource.marketplace) && !!plugin.latest_unique_identifier && plugin.latest_unique_identifier !== plugin_unique_identifier} hasRedCornerMark={(source === PluginSource.marketplace) && !!plugin.latest_version && plugin.latest_version !== plugin.version}
/> />
</div> </div>
<div className='flex items-center justify-between'> <div className='flex items-center justify-between'>

@ -6,9 +6,8 @@ import type { Placement } from '@floating-ui/react'
import { import {
RiEqualizer2Line, RiEqualizer2Line,
} from '@remixicon/react' } from '@remixicon/react'
import { useRouter } from 'next/navigation' import { usePathname, useRouter } from 'next/navigation'
import Divider from '../../base/divider' import Divider from '../../base/divider'
import { removeAccessToken } from '../utils'
import InfoModal from './info-modal' import InfoModal from './info-modal'
import ActionButton from '@/app/components/base/action-button' import ActionButton from '@/app/components/base/action-button'
import { import {
@ -19,6 +18,8 @@ import {
import ThemeSwitcher from '@/app/components/base/theme-switcher' import ThemeSwitcher from '@/app/components/base/theme-switcher'
import type { SiteInfo } from '@/models/share' import type { SiteInfo } from '@/models/share'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { AccessMode } from '@/models/access-control'
type Props = { type Props = {
data?: SiteInfo data?: SiteInfo
@ -31,7 +32,9 @@ const MenuDropdown: FC<Props> = ({
placement, placement,
hideLogout, hideLogout,
}) => { }) => {
const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode)
const router = useRouter() const router = useRouter()
const pathname = usePathname()
const { t } = useTranslation() const { t } = useTranslation()
const [open, doSetOpen] = useState(false) const [open, doSetOpen] = useState(false)
const openRef = useRef(open) const openRef = useRef(open)
@ -45,9 +48,10 @@ const MenuDropdown: FC<Props> = ({
}, [setOpen]) }, [setOpen])
const handleLogout = useCallback(() => { const handleLogout = useCallback(() => {
removeAccessToken() localStorage.removeItem('token')
router.replace(`/webapp-signin?redirect_url=${window.location.href}`) localStorage.removeItem('webapp_access_token')
}, [router]) router.replace(`/webapp-signin?redirect_url=${pathname}`)
}, [router, pathname])
const [show, setShow] = useState(false) const [show, setShow] = useState(false)
@ -92,6 +96,16 @@ const MenuDropdown: FC<Props> = ({
className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover' className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'
>{t('common.userProfile.about')}</div> >{t('common.userProfile.about')}</div>
</div> </div>
{!(hideLogout || webAppAccessMode === AccessMode.EXTERNAL_MEMBERS || webAppAccessMode === AccessMode.PUBLIC) && (
<div className='p-1'>
<div
onClick={handleLogout}
className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'
>
{t('common.userProfile.logout')}
</div>
</div>
)}
</div> </div>
</PortalToFollowElemContent> </PortalToFollowElemContent>
</PortalToFollowElem> </PortalToFollowElem>

@ -10,8 +10,8 @@ export const getInitialTokenV2 = (): Record<string, any> => ({
version: 2, version: 2,
}) })
export const checkOrSetAccessToken = async () => { export const checkOrSetAccessToken = async (appCode?: string) => {
const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0] const sharedToken = appCode || globalThis.location.pathname.split('/').slice(-1)[0]
const userId = (await getProcessedSystemVariablesFromUrlParams()).user_id const userId = (await getProcessedSystemVariablesFromUrlParams()).user_id
const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2()) const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2())
let accessTokenJson = getInitialTokenV2() let accessTokenJson = getInitialTokenV2()
@ -23,8 +23,10 @@ export const checkOrSetAccessToken = async () => {
catch { catch {
} }
if (!accessTokenJson[sharedToken]?.[userId || 'DEFAULT']) { if (!accessTokenJson[sharedToken]?.[userId || 'DEFAULT']) {
const res = await fetchAccessToken(sharedToken, userId) const webAppAccessToken = localStorage.getItem('webapp_access_token')
const res = await fetchAccessToken({ appCode: sharedToken, userId, webAppAccessToken })
accessTokenJson[sharedToken] = { accessTokenJson[sharedToken] = {
...accessTokenJson[sharedToken], ...accessTokenJson[sharedToken],
[userId || 'DEFAULT']: res.access_token, [userId || 'DEFAULT']: res.access_token,
@ -33,7 +35,7 @@ export const checkOrSetAccessToken = async () => {
} }
} }
export const setAccessToken = async (sharedToken: string, token: string, user_id?: string) => { export const setAccessToken = (sharedToken: string, token: string, user_id?: string) => {
const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2()) const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2())
let accessTokenJson = getInitialTokenV2() let accessTokenJson = getInitialTokenV2()
try { try {
@ -69,6 +71,7 @@ export const removeAccessToken = () => {
} }
localStorage.removeItem(CONVERSATION_ID_INFO) localStorage.removeItem(CONVERSATION_ID_INFO)
localStorage.removeItem('webapp_access_token')
delete accessTokenJson[sharedToken] delete accessTokenJson[sharedToken]
localStorage.setItem('token', JSON.stringify(accessTokenJson)) localStorage.setItem('token', JSON.stringify(accessTokenJson))

@ -57,7 +57,7 @@ describe('extractFunctionParams', () => {
}) })
}) })
// JavaScriptのテストケース // JavaScript のテストケース
describe('JavaScript', () => { describe('JavaScript', () => {
test('handles no parameters', () => { test('handles no parameters', () => {
const result = extractFunctionParams(SAMPLE_CODES.javascript.noParams, CodeLanguage.javascript) const result = extractFunctionParams(SAMPLE_CODES.javascript.noParams, CodeLanguage.javascript)
@ -180,7 +180,7 @@ function main(name, age, city) {
} }
describe('extractReturnType', () => { describe('extractReturnType', () => {
// Python3のテスト // Python3 のテスト
describe('Python3', () => { describe('Python3', () => {
test('extracts single return value', () => { test('extracts single return value', () => {
const result = extractReturnType(RETURN_TYPE_SAMPLES.python3.singleReturn, CodeLanguage.python3) const result = extractReturnType(RETURN_TYPE_SAMPLES.python3.singleReturn, CodeLanguage.python3)
@ -247,7 +247,7 @@ describe('extractReturnType', () => {
}) })
}) })
// JavaScriptのテスト // JavaScript のテスト
describe('JavaScript', () => { describe('JavaScript', () => {
test('extracts single return value', () => { test('extracts single return value', () => {
const result = extractReturnType(RETURN_TYPE_SAMPLES.javascript.singleReturn, CodeLanguage.javascript) const result = extractReturnType(RETURN_TYPE_SAMPLES.javascript.singleReturn, CodeLanguage.javascript)

@ -31,7 +31,7 @@ export const extractReturnType = (code: string, language: CodeLanguage): OutputV
if (returnIndex === -1) if (returnIndex === -1)
return {} return {}
// returnから始まる部分文字列を取得 // return から始まる部分文字列を取得
const codeAfterReturn = codeWithoutComments.slice(returnIndex) const codeAfterReturn = codeWithoutComments.slice(returnIndex)
let bracketCount = 0 let bracketCount = 0

@ -1,8 +1,8 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import classNames from '@/utils/classnames' import classNames from '@/utils/classnames'
import { useSelector } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context' import { useGlobalPublicStore } from '@/context/global-public-context'
import { useTheme } from 'next-themes'
type LoginLogoProps = { type LoginLogoProps = {
className?: string className?: string
@ -12,11 +12,7 @@ const LoginLogo: FC<LoginLogoProps> = ({
className, className,
}) => { }) => {
const { systemFeatures } = useGlobalPublicStore() const { systemFeatures } = useGlobalPublicStore()
const { theme } = useSelector((s) => { const { theme } = useTheme()
return {
theme: s.theme,
}
})
let src = theme === 'light' ? '/logo/logo-site.png' : `/logo/logo-site-${theme}.png` let src = theme === 'light' ? '/logo/logo-site.png' : `/logo/logo-site-${theme}.png`
if (systemFeatures.branding.enabled) if (systemFeatures.branding.enabled)

@ -7,19 +7,24 @@ import type { SystemFeatures } from '@/types/feature'
import { defaultSystemFeatures } from '@/types/feature' import { defaultSystemFeatures } from '@/types/feature'
import { getSystemFeatures } from '@/service/common' import { getSystemFeatures } from '@/service/common'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import { AccessMode } from '@/models/access-control'
type GlobalPublicStore = { type GlobalPublicStore = {
isPending: boolean isGlobalPending: boolean
setIsPending: (isPending: boolean) => void setIsGlobalPending: (isPending: boolean) => void
systemFeatures: SystemFeatures systemFeatures: SystemFeatures
setSystemFeatures: (systemFeatures: SystemFeatures) => void setSystemFeatures: (systemFeatures: SystemFeatures) => void
webAppAccessMode: AccessMode,
setWebAppAccessMode: (webAppAccessMode: AccessMode) => void
} }
export const useGlobalPublicStore = create<GlobalPublicStore>(set => ({ export const useGlobalPublicStore = create<GlobalPublicStore>(set => ({
isPending: true, isGlobalPending: true,
setIsPending: (isPending: boolean) => set(() => ({ isPending })), setIsGlobalPending: (isPending: boolean) => set(() => ({ isGlobalPending: isPending })),
systemFeatures: defaultSystemFeatures, systemFeatures: defaultSystemFeatures,
setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })), setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })),
webAppAccessMode: AccessMode.PUBLIC,
setWebAppAccessMode: (webAppAccessMode: AccessMode) => set(() => ({ webAppAccessMode })),
})) }))
const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({ const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({
@ -29,7 +34,7 @@ const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({
queryKey: ['systemFeatures'], queryKey: ['systemFeatures'],
queryFn: getSystemFeatures, queryFn: getSystemFeatures,
}) })
const { setSystemFeatures, setIsPending } = useGlobalPublicStore() const { setSystemFeatures, setIsGlobalPending: setIsPending } = useGlobalPublicStore()
useEffect(() => { useEffect(() => {
if (data) if (data)
setSystemFeatures({ ...defaultSystemFeatures, ...data }) setSystemFeatures({ ...defaultSystemFeatures, ...data })

@ -11,7 +11,7 @@ describe('title should be empty if systemFeatures is pending', () => {
act(() => { act(() => {
useGlobalPublicStore.setState({ useGlobalPublicStore.setState({
systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } }, systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } },
isPending: true, isGlobalPending: true,
}) })
}) })
it('document title should be empty if set title', () => { it('document title should be empty if set title', () => {
@ -28,7 +28,7 @@ describe('use default branding', () => {
beforeEach(() => { beforeEach(() => {
act(() => { act(() => {
useGlobalPublicStore.setState({ useGlobalPublicStore.setState({
isPending: false, isGlobalPending: false,
systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } }, systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } },
}) })
}) })
@ -48,7 +48,7 @@ describe('use specific branding', () => {
beforeEach(() => { beforeEach(() => {
act(() => { act(() => {
useGlobalPublicStore.setState({ useGlobalPublicStore.setState({
isPending: false, isGlobalPending: false,
systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: true, application_title: 'Test' } }, systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: true, application_title: 'Test' } },
}) })
}) })

@ -3,7 +3,7 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
import { useFavicon, useTitle } from 'ahooks' import { useFavicon, useTitle } from 'ahooks'
export default function useDocumentTitle(title: string) { export default function useDocumentTitle(title: string) {
const isPending = useGlobalPublicStore(s => s.isPending) const isPending = useGlobalPublicStore(s => s.isGlobalPending)
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const prefix = title ? `${title} - ` : '' const prefix = title ? `${title} - ` : ''
let titleStr = '' let titleStr = ''

@ -115,13 +115,13 @@ export const languages = [
}, },
{ {
value: 'ja-JP', value: 'ja-JP',
name: '日本語(日本)', name: '日本語 (日本)',
example: 'こんにちは、Dify!', example: 'こんにちは、Dify!',
supported: false, supported: false,
}, },
{ {
value: 'ko-KR', value: 'ko-KR',
name: '한국어(대한민국)', name: '한국어 (대한민국)',
example: '안녕, Dify!', example: '안녕, Dify!',
supported: true, supported: true,
}, },

@ -197,9 +197,10 @@ const translation = {
}, },
accessControl: 'Web App Access Control', accessControl: 'Web App Access Control',
accessItemsDescription: { accessItemsDescription: {
anyone: 'Anyone can access the web app', anyone: 'Anyone can access the web app (no login required)',
specific: 'Only specific groups or members can access the web app', specific: 'Only specific members within the platform can access the Web application',
organization: 'Anyone in the organization can access the web app', organization: 'All members within the platform can access the Web application',
external: 'Only authenticated external users can access the Web application',
}, },
accessControlDialog: { accessControlDialog: {
title: 'Web App Access Control', title: 'Web App Access Control',
@ -207,15 +208,16 @@ const translation = {
accessLabel: 'Who has access', accessLabel: 'Who has access',
accessItems: { accessItems: {
anyone: 'Anyone with the link', anyone: 'Anyone with the link',
specific: 'Specific groups or members', specific: 'Specific members within the platform',
organization: 'Only members within the enterprise', organization: 'All members within the platform',
external: 'Authenticated external users',
}, },
groups_one: '{{count}} GROUP', groups_one: '{{count}} GROUP',
groups_other: '{{count}} GROUPS', groups_other: '{{count}} GROUPS',
members_one: '{{count}} MEMBER', members_one: '{{count}} MEMBER',
members_other: '{{count}} MEMBERS', members_other: '{{count}} MEMBERS',
noGroupsOrMembers: 'No groups or members selected', noGroupsOrMembers: 'No groups or members selected',
webAppSSONotEnabledTip: 'Please contact enterprise administrator to configure the web app authentication method.', webAppSSONotEnabledTip: 'Please contact your organization administrator to configure external authentication for the Web application.',
operateGroupAndMember: { operateGroupAndMember: {
searchPlaceholder: 'Search groups and members', searchPlaceholder: 'Search groups and members',
allMembers: 'All members', allMembers: 'All members',

@ -24,7 +24,7 @@ const translation = {
desc: { desc: {
front: 'Your information and use of Education Verified status are subject to our', front: 'Your information and use of Education Verified status are subject to our',
and: 'and', and: 'and',
end: '. By submitting', end: '. By submitting:',
termsOfService: 'Terms of Service', termsOfService: 'Terms of Service',
privacyPolicy: 'Privacy Policy', privacyPolicy: 'Privacy Policy',
}, },

@ -77,6 +77,9 @@ const translation = {
atLeastOne: 'Please input at least one row in the uploaded file.', atLeastOne: 'Please input at least one row in the uploaded file.',
}, },
}, },
login: {
backToHome: 'Back to Home',
},
} }
export default translation export default translation

@ -280,6 +280,7 @@ const translation = {
'inputPlaceholder': 'Por favor ingresa', 'inputPlaceholder': 'Por favor ingresa',
'content': 'Contenido', 'content': 'Contenido',
'required': 'Requerido', 'required': 'Requerido',
'hide': 'Ocultar',
'errorMsg': { 'errorMsg': {
varNameRequired: 'Nombre de la variable es requerido', varNameRequired: 'Nombre de la variable es requerido',
labelNameRequired: 'Nombre de la etiqueta es requerido', labelNameRequired: 'Nombre de la etiqueta es requerido',

@ -315,6 +315,7 @@ const translation = {
'inputPlaceholder': 'لطفاً وارد کنید', 'inputPlaceholder': 'لطفاً وارد کنید',
'content': 'محتوا', 'content': 'محتوا',
'required': 'مورد نیاز', 'required': 'مورد نیاز',
'hide': 'مخفی کردن',
'errorMsg': { 'errorMsg': {
varNameRequired: 'نام متغیر مورد نیاز است', varNameRequired: 'نام متغیر مورد نیاز است',
labelNameRequired: 'نام برچسب مورد نیاز است', labelNameRequired: 'نام برچسب مورد نیاز است',

@ -268,6 +268,7 @@ const translation = {
'labelName': 'Label Name', 'labelName': 'Label Name',
'inputPlaceholder': 'Please input', 'inputPlaceholder': 'Please input',
'required': 'Required', 'required': 'Required',
'hide': 'Caché',
'errorMsg': { 'errorMsg': {
varNameRequired: 'Variable name is required', varNameRequired: 'Variable name is required',
labelNameRequired: 'Label name is required', labelNameRequired: 'Label name is required',

@ -312,6 +312,7 @@ const translation = {
'inputPlaceholder': 'कृपया इनपुट करें', 'inputPlaceholder': 'कृपया इनपुट करें',
'content': 'सामग्री', 'content': 'सामग्री',
'required': 'आवश्यक', 'required': 'आवश्यक',
'hide': 'छुपाएँ',
'errorMsg': { 'errorMsg': {
varNameRequired: 'वेरिएबल नाम आवश्यक है', varNameRequired: 'वेरिएबल नाम आवश्यक है',
labelNameRequired: 'लेबल नाम आवश्यक है', labelNameRequired: 'लेबल नाम आवश्यक है',

@ -1,6 +1,6 @@
const translation = { const translation = {
title: 'ज्ञान सेटिंग्ज', title: 'ज्ञान सेटिंग्ज',
desc: 'यहां आप ज्ञान की संपत्ति और कार्य प्रक्रियाओं को modify कर सकते हैं', desc: 'यहां आप ज्ञान की संपत्ति और कार्य प्रक्रियाओं को modify कर सकते हैं.',
form: { form: {
name: 'ज्ञान नाम', name: 'ज्ञान नाम',
namePlaceholder: 'कृपया ज्ञान नाम दर्ज करें', namePlaceholder: 'कृपया ज्ञान नाम दर्ज करें',

@ -314,6 +314,7 @@ const translation = {
'inputPlaceholder': 'Per favore inserisci', 'inputPlaceholder': 'Per favore inserisci',
'content': 'Contenuto', 'content': 'Contenuto',
'required': 'Richiesto', 'required': 'Richiesto',
'hide': 'Nascondi',
'errorMsg': { 'errorMsg': {
varNameRequired: 'Il nome della variabile è richiesto', varNameRequired: 'Il nome della variabile è richiesto',
labelNameRequired: 'Il nome dell\'etichetta è richiesto', labelNameRequired: 'Il nome dell\'etichetta è richiesto',

@ -42,9 +42,9 @@ const translation = {
}, },
batchModal: { batchModal: {
title: '一括インポート', title: '一括インポート',
csvUploadTitle: 'CSVファイルをここにドラッグ&ドロップするか、', csvUploadTitle: 'CSV ファイルをここにドラッグ&ドロップするか、',
browse: '参照', browse: '参照',
tip: 'CSVファイルは以下の構造に準拠する必要があります:', tip: 'CSV ファイルは以下の構造に準拠する必要があります',
question: '質問', question: '質問',
answer: '回答', answer: '回答',
contentTitle: 'チャンクの内容', contentTitle: 'チャンクの内容',

@ -1,6 +1,6 @@
const translation = { const translation = {
apiServer: 'APIサーバー', apiServer: 'API サーバー',
apiKey: 'APIキー', apiKey: 'API キー',
status: 'ステータス', status: 'ステータス',
disabled: '無効', disabled: '無効',
ok: '稼働中', ok: '稼働中',
@ -15,8 +15,8 @@ const translation = {
}, },
never: 'なし', never: 'なし',
apiKeyModal: { apiKeyModal: {
apiSecretKey: 'APIシークレットキー', apiSecretKey: 'API シークレットキー',
apiSecretKeyTips: 'APIの悪用を防ぐために、APIキーを保護してください。フロントエンドのコードで平文として使用しないでください。:)', apiSecretKeyTips: 'API の悪用を防ぐために、API キーを保護してください。フロントエンドのコードで平文として使用しないでください。:)',
createNewSecretKey: '新しいシークレットキーを作成', createNewSecretKey: '新しいシークレットキーを作成',
secretKey: 'シークレットキー', secretKey: 'シークレットキー',
created: '作成日時', created: '作成日時',
@ -29,44 +29,44 @@ const translation = {
ok: 'OK', ok: 'OK',
}, },
completionMode: { completionMode: {
title: '補完アプリAPI', title: '補完アプリ API',
info: '記事、要約、翻訳などの高品質なテキスト生成には、ユーザーの入力を使用した補完メッセージAPIを使用します。テキスト生成は、Dify Prompt Engineeringで設定されたモデルパラメータとプロンプトテンプレートに依存しています。', info: '記事、要約、翻訳などの高品質なテキスト生成には、ユーザーの入力を使用した補完メッセージ API を使用します。テキスト生成は、Dify Prompt Engineering で設定されたモデルパラメータとプロンプトテンプレートに依存しています。',
createCompletionApi: '補完メッセージの作成', createCompletionApi: '補完メッセージの作成',
createCompletionApiTip: '質疑応答モードをサポートするために、補完メッセージを作成します。', createCompletionApiTip: '質疑応答モードをサポートするために、補完メッセージを作成します。',
inputsTips: 'オプションPrompt Engの変数に対応するキーと値のペアとしてユーザー入力フィールドを提供します。キーは変数名で、値はパラメータの値です。フィールドのタイプがSelectの場合、送信される値は事前に設定された選択肢のいずれかである必要があります。', inputsTips: 'オプションPrompt Eng の変数に対応するキーと値のペアとしてユーザー入力フィールドを提供します。キーは変数名で、値はパラメータの値です。フィールドのタイプが Select の場合、送信される値は事前に設定された選択肢のいずれかである必要があります。',
queryTips: 'ユーザーの入力テキスト内容。', queryTips: 'ユーザーの入力テキスト内容。',
blocking: 'ブロッキングタイプで、実行が完了して結果が返されるまで待機します。(処理が長い場合、リクエストは中断される場合があります)', blocking: 'ブロッキングタイプで、実行が完了して結果が返されるまで待機します。(処理が長い場合、リクエストは中断される場合があります)',
streaming: 'ストリーミングの返却。SSEServer-Sent Eventsに基づいたストリーミングの返却の実装。', streaming: 'ストリーミングの返却。SSEServer-Sent Eventsに基づいたストリーミングの返却の実装。',
messageFeedbackApi: 'メッセージフィードバック(いいね)', messageFeedbackApi: 'メッセージフィードバック(いいね)',
messageFeedbackApiTip: 'エンドユーザーの代わりに受信したメッセージを「いいね」または「いいね」で評価します。このデータはログ&注釈ページで表示され、将来のモデルの微調整に使用されます。', messageFeedbackApiTip: 'エンドユーザーの代わりに受信したメッセージを「いいね」または「いいね」で評価します。このデータはログ&注釈ページで表示され、将来のモデルの微調整に使用されます。',
messageIDTip: 'メッセージID', messageIDTip: 'メッセージ ID',
ratingTip: 'いいねまたはいいね、nullは元に戻す', ratingTip: 'いいねまたはいいね、null は元に戻す',
parametersApi: 'アプリケーションパラメータ情報の取得', parametersApi: 'アプリケーションパラメータ情報の取得',
parametersApiTip: '変数名、フィールド名、タイプ、デフォルト値を含む設定済みの入力パラメータを取得します。通常、これらのフィールドをフォームに表示したり、クライアントの読み込み後にデフォルト値を入力したりするために使用されます。', parametersApiTip: '変数名、フィールド名、タイプ、デフォルト値を含む設定済みの入力パラメータを取得します。通常、これらのフィールドをフォームに表示したり、クライアントの読み込み後にデフォルト値を入力したりするために使用されます。',
}, },
chatMode: { chatMode: {
title: 'チャットアプリAPI', title: 'チャットアプリ API',
info: '質疑応答形式を使用した多目的の対話型アプリケーションには、チャットメッセージAPIを呼び出して対話を開始します。返されたconversation_idを渡すことで、継続的な会話を維持します。応答パラメータとテンプレートは、Dify Prompt Engの設定に依存します。', info: '質疑応答形式を使用した多目的の対話型アプリケーションには、チャットメッセージ API を呼び出して対話を開始します。返された conversation_id を渡すことで、継続的な会話を維持します。応答パラメータとテンプレートは、Dify Prompt Eng の設定に依存します。',
createChatApi: 'チャットメッセージの作成', createChatApi: 'チャットメッセージの作成',
createChatApiTip: '新しい会話メッセージを作成するか、既存の対話を継続します。', createChatApiTip: '新しい会話メッセージを作成するか、既存の対話を継続します。',
inputsTips: 'オプションPrompt Engの変数に対応するキーと値のペアとしてユーザー入力フィールドを提供します。キーは変数名で、値はパラメータの値です。フィールドのタイプがSelectの場合、送信される値は事前に設定された選択肢のいずれかである必要があります。', inputsTips: 'オプションPrompt Eng の変数に対応するキーと値のペアとしてユーザー入力フィールドを提供します。キーは変数名で、値はパラメータの値です。フィールドのタイプが Select の場合、送信される値は事前に設定された選択肢のいずれかである必要があります。',
queryTips: 'ユーザーの入力/質問内容', queryTips: 'ユーザーの入力/質問内容',
blocking: 'ブロッキングタイプで、実行が完了して結果が返されるまで待機します。(処理が長い場合、リクエストは中断される場合があります)', blocking: 'ブロッキングタイプで、実行が完了して結果が返されるまで待機します。(処理が長い場合、リクエストは中断される場合があります)',
streaming: 'ストリーミングの返却。SSEServer-Sent Eventsに基づいたストリーミングの返却の実装。', streaming: 'ストリーミングの返却。SSEServer-Sent Eventsに基づいたストリーミングの返却の実装。',
conversationIdTip: '(オプション)会話ID初回の会話の場合は空白のままにしておき、継続する場合はコンテキストからconversation_idを渡します。', conversationIdTip: '(オプション)会話 ID初回の会話の場合は空白のままにしておき、継続する場合はコンテキストから conversation_id を渡します。',
messageFeedbackApi: 'メッセージ端末ユーザーフィードバック、いいね', messageFeedbackApi: 'メッセージ端末ユーザーフィードバック、いいね',
messageFeedbackApiTip: 'エンドユーザーの代わりに受信したメッセージを「いいね」または「いいね」で評価します。このデータはログ&注釈ページで表示され、将来のモデルの微調整に使用されます。', messageFeedbackApiTip: 'エンドユーザーの代わりに受信したメッセージを「いいね」または「いいね」で評価します。このデータはログ&注釈ページで表示され、将来のモデルの微調整に使用されます。',
messageIDTip: 'メッセージID', messageIDTip: 'メッセージ ID',
ratingTip: 'いいねまたはいいね、nullは元に戻す', ratingTip: 'いいねまたはいいね、null は元に戻す',
chatMsgHistoryApi: 'チャット履歴メッセージの取得', chatMsgHistoryApi: 'チャット履歴メッセージの取得',
chatMsgHistoryApiTip: '最初のページは最新の「limit」バーを返します。逆順です。', chatMsgHistoryApiTip: '最初のページは最新の「limit」バーを返します。逆順です。',
chatMsgHistoryConversationIdTip: '会話ID', chatMsgHistoryConversationIdTip: '会話 ID',
chatMsgHistoryFirstId: '現在のページの最初のチャットレコードのID。デフォルトはなし。', chatMsgHistoryFirstId: '現在のページの最初のチャットレコードの ID。デフォルトはなし。',
chatMsgHistoryLimit: '1回のリクエストで返されるチャットの数', chatMsgHistoryLimit: '1 回のリクエストで返されるチャットの数',
conversationsListApi: '会話リストの取得', conversationsListApi: '会話リストの取得',
conversationsListApiTip: '現在のユーザーのセッションリストを取得します。デフォルトでは、最後の20のセッションが返されます。', conversationsListApiTip: '現在のユーザーのセッションリストを取得します。デフォルトでは、最後の 20 のセッションが返されます。',
conversationsListFirstIdTip: '現在のページの最後のレコードのID、デフォルトはなし。', conversationsListFirstIdTip: '現在のページの最後のレコードの ID、デフォルトはなし。',
conversationsListLimitTip: '1回のリクエストで返されるチャットの数', conversationsListLimitTip: '1 回のリクエストで返されるチャットの数',
conversationRenamingApi: '会話の名前変更', conversationRenamingApi: '会話の名前変更',
conversationRenamingApiTip: '会話の名前を変更します。名前はマルチセッションクライアントインターフェースに表示されます。', conversationRenamingApiTip: '会話の名前を変更します。名前はマルチセッションクライアントインターフェースに表示されます。',
conversationRenamingNameTip: '新しい名前', conversationRenamingNameTip: '新しい名前',

@ -5,12 +5,12 @@ const translation = {
}, },
orchestrate: 'オーケストレーション', orchestrate: 'オーケストレーション',
promptMode: { promptMode: {
simple: 'エキスパートモードに切り替えて、PROMPT全体を編集します', simple: 'エキスパートモードに切り替えて、PROMPT 全体を編集します',
advanced: 'エキスパートモード', advanced: 'エキスパートモード',
switchBack: '基本モードに戻る', switchBack: '基本モードに戻る',
advancedWarning: { advancedWarning: {
title: 'エキスパートモードに切り替えました。PROMPTを変更すると、基本モードに戻ることはできません。', title: 'エキスパートモードに切り替えました。PROMPT を変更すると、基本モードに戻ることはできません。',
description: 'エキスパートモードでは、PROMPT全体を編集できます。', description: 'エキスパートモードでは、PROMPT 全体を編集できます。',
learnMore: '詳細はこちら', learnMore: '詳細はこちら',
ok: 'OK', ok: 'OK',
}, },
@ -33,14 +33,14 @@ const translation = {
userAction: 'ユーザー', userAction: 'ユーザー',
}, },
notSetAPIKey: { notSetAPIKey: {
title: 'LLMプロバイダーキーが設定されていません', title: 'LLM プロバイダーキーが設定されていません',
trailFinished: 'トライアル終了', trailFinished: 'トライアル終了',
description: 'LLMプロバイダーキーが設定されていません。デバッグする前に設定する必要があります。', description: 'LLM プロバイダーキーが設定されていません。デバッグする前に設定する必要があります。',
settingBtn: '設定に移動', settingBtn: '設定に移動',
}, },
trailUseGPT4Info: { trailUseGPT4Info: {
title: '現在、gpt-4はサポートされていません', title: '現在、gpt-4 はサポートされていません',
description: 'gpt-4を使用するには、APIキーを設定してください。', description: 'gpt-4 を使用するには、API キーを設定してください。',
}, },
feature: { feature: {
groupChat: { groupChat: {
@ -52,12 +52,12 @@ const translation = {
}, },
conversationOpener: { conversationOpener: {
title: '会話の開始', title: '会話の開始',
description: 'チャットアプリでは、AIがユーザーに最初にアクティブに話しかける最初の文は、通常、歓迎メッセージとして使用されます。', description: 'チャットアプリでは、AI がユーザーに最初にアクティブに話しかける最初の文は、通常、歓迎メッセージとして使用されます。',
}, },
suggestedQuestionsAfterAnswer: { suggestedQuestionsAfterAnswer: {
title: 'フォローアップ', title: 'フォローアップ',
description: '次の質問の提案を設定すると、ユーザーにより良いチャットが提供されます。', description: '次の質問の提案を設定すると、ユーザーにより良いチャットが提供されます。',
resDes: 'ユーザーの次の質問に関する3つの提案。', resDes: 'ユーザーの次の質問に関する 3 つの提案。',
tryToAsk: '質問してみてください', tryToAsk: '質問してみてください',
}, },
moreLikeThis: { moreLikeThis: {
@ -128,7 +128,7 @@ const translation = {
}, },
tools: { tools: {
title: 'ツール', title: 'ツール',
tips: 'ツールは、ユーザー入力または変数をリクエストパラメーターとして使用して外部データをコンテキストとしてクエリするための標準的なAPI呼び出し方法を提供します。', tips: 'ツールは、ユーザー入力または変数をリクエストパラメーターとして使用して外部データをコンテキストとしてクエリするための標準的な API 呼び出し方法を提供します。',
toolsInUse: '{{count}} 個のツールが使用中', toolsInUse: '{{count}} 個のツールが使用中',
modal: { modal: {
title: 'ツール', title: 'ツール',
@ -162,7 +162,7 @@ const translation = {
}, },
moderation: { moderation: {
title: 'コンテンツのモデレーション', title: 'コンテンツのモデレーション',
description: 'モデレーションAPIを使用するか、機密語リストを維持することで、モデルの出力を安全にします。', description: 'モデレーション API を使用するか、機密語リストを維持することで、モデルの出力を安全にします。',
contentEnableLabel: 'モデレート・コンテンツを有効にする', contentEnableLabel: 'モデレート・コンテンツを有効にする',
allEnabled: '入力/出力コンテンツが有効になっています', allEnabled: '入力/出力コンテンツが有効になっています',
inputEnabled: '入力コンテンツが有効になっています', inputEnabled: '入力コンテンツが有効になっています',
@ -171,16 +171,16 @@ const translation = {
title: 'コンテンツのモデレーション設定', title: 'コンテンツのモデレーション設定',
provider: { provider: {
title: 'プロバイダ', title: 'プロバイダ',
openai: 'OpenAIモデレーション', openai: 'OpenAI モデレーション',
openaiTip: { openaiTip: {
prefix: 'OpenAIモデレーションには、', prefix: 'OpenAI モデレーションには、',
suffix: 'にOpenAI APIキーが設定されている必要があります。', suffix: 'に OpenAI API キーが設定されている必要があります。',
}, },
keywords: 'キーワード', keywords: 'キーワード',
}, },
keywords: { keywords: {
tip: '1行ごとに1つ、行区切りで入力してください。1行あたり最大100文字。', tip: '1 行ごとに 1 つ、行区切りで入力してください。1 行あたり最大 100 文字。',
placeholder: '1行ごとに、行区切りで入力してください', placeholder: '1 行ごとに、行区切りで入力してください',
line: '行', line: '行',
}, },
content: { content: {
@ -188,14 +188,14 @@ const translation = {
output: '出力コンテンツをモデレート', output: '出力コンテンツをモデレート',
preset: 'プリセット返信', preset: 'プリセット返信',
placeholder: 'ここにプリセット返信の内容を入力', placeholder: 'ここにプリセット返信の内容を入力',
condition: '少なくとも1つの入力および出力コンテンツをモデレートする', condition: '少なくとも 1 つの入力および出力コンテンツをモデレートする',
fromApi: 'プリセット返信はAPIによって返されます', fromApi: 'プリセット返信は API によって返されます',
errorMessage: 'プリセット返信は空にできません', errorMessage: 'プリセット返信は空にできません',
supportMarkdown: 'Markdownがサポートされています', supportMarkdown: 'Markdown がサポートされています',
}, },
openaiNotConfig: { openaiNotConfig: {
before: 'OpenAIモデレーションには、', before: 'OpenAI モデレーションには、',
after: 'にOpenAI APIキーが設定されている必要があります。', after: 'に OpenAI API キーが設定されている必要があります。',
}, },
}, },
}, },
@ -214,13 +214,13 @@ const translation = {
modalTitle: '画像アップロード設置', modalTitle: '画像アップロード設置',
}, },
bar: { bar: {
empty: 'Webアプリのユーザーエクスペリアンスを強化させる機能を有効にする', empty: 'Web アプリのユーザーエクスペリアンスを強化させる機能を有効にする',
enableText: '有効な機能', enableText: '有効な機能',
manage: '管理', manage: '管理',
}, },
documentUpload: { documentUpload: {
title: 'ドキュメント', title: 'ドキュメント',
description: 'ドキュメント機能を有効にすると、AIモデルがファイルを処理し、その内容に基づいて質問に回答できるようになります。', description: 'ドキュメント機能を有効にすると、AI モデルがファイルを処理し、その内容に基づいて質問に回答できるようになります。',
}, },
}, },
codegen: { codegen: {
@ -228,7 +228,7 @@ const translation = {
description: 'コードジェネレーターは、設定されたモデルを使用して指示に基づいて高品質なコードを生成します。明確で詳細な指示を提供してください。', description: 'コードジェネレーターは、設定されたモデルを使用して指示に基づいて高品質なコードを生成します。明確で詳細な指示を提供してください。',
instruction: '指示', instruction: '指示',
instructionPlaceholder: '生成したいコードの詳細な説明を入力してください。', instructionPlaceholder: '生成したいコードの詳細な説明を入力してください。',
noDataLine1: '左側に使用例を記入してください,', noDataLine1: '左側に使用例を記入してください',
noDataLine2: 'コードのプレビューがこちらに表示されます。', noDataLine2: 'コードのプレビューがこちらに表示されます。',
generate: '生成', generate: '生成',
generatedCodeTitle: '生成されたコード', generatedCodeTitle: '生成されたコード',
@ -247,7 +247,7 @@ const translation = {
instructionPlaceHolder: '具体的で明確な指示を入力してください。', instructionPlaceHolder: '具体的で明確な指示を入力してください。',
generate: '生成', generate: '生成',
resTitle: '生成されたプロンプト', resTitle: '生成されたプロンプト',
noDataLine1: '左側に使用例を記入してください,', noDataLine1: '左側に使用例を記入してください',
noDataLine2: 'オーケストレーションのプレビューがこちらに表示されます。', noDataLine2: 'オーケストレーションのプレビューがこちらに表示されます。',
apply: '適用', apply: '適用',
noData: '左側にユースケースを入力すると、こちらでプレビューができます。', noData: '左側にユースケースを入力すると、こちらでプレビューができます。',
@ -276,12 +276,12 @@ const translation = {
instruction: 'ユーザーが簡単に旅行計画を立てられるように設計されたツール', instruction: 'ユーザーが簡単に旅行計画を立てられるように設計されたツール',
}, },
SQLSorcerer: { SQLSorcerer: {
name: 'SQLソーサラー', name: 'SQL ソーサラー',
instruction: '日常言語をSQLクエリに変換する', instruction: '日常言語を SQL クエリに変換する',
}, },
GitGud: { GitGud: {
name: 'Git gud', name: 'Git gud',
instruction: 'ユーザーが記述したバージョン管理アクションに対応するGitコマンドを生成する', instruction: 'ユーザーが記述したバージョン管理アクションに対応する Git コマンドを生成する',
}, },
meetingTakeaways: { meetingTakeaways: {
name: '会議の要点', name: '会議の要点',
@ -298,7 +298,7 @@ const translation = {
message: '変更が破棄され、最後に公開された構成が復元されます。', message: '変更が破棄され、最後に公開された構成が復元されます。',
}, },
errorMessage: { errorMessage: {
nameOfKeyRequired: 'キーの名前: {{key}} が必要です', nameOfKeyRequired: 'キーの名前{{key}} が必要です',
valueOfVarRequired: '{{key}} の値は空にできません', valueOfVarRequired: '{{key}} の値は空にできません',
queryRequired: 'リクエストテキストが必要です。', queryRequired: 'リクエストテキストが必要です。',
waitForResponse: '前のメッセージへの応答が完了するまでお待ちください。', waitForResponse: '前のメッセージへの応答が完了するまでお待ちください。',
@ -309,7 +309,7 @@ const translation = {
}, },
chatSubTitle: 'プロンプト', chatSubTitle: 'プロンプト',
completionSubTitle: '接頭辞プロンプト', completionSubTitle: '接頭辞プロンプト',
promptTip: 'プロンプトは、AIの応答を指示と制約で誘導します。 {{input}} のような変数を挿入します。このプロンプトはユーザーには表示されません。', promptTip: 'プロンプトは、AI の応答を指示と制約で誘導します。 {{input}} のような変数を挿入します。このプロンプトはユーザーには表示されません。',
formattingChangedTitle: '書式が変更されました', formattingChangedTitle: '書式が変更されました',
formattingChangedText: '書式を変更すると、デバッグ領域がリセットされます。よろしいですか?', formattingChangedText: '書式を変更すると、デバッグ領域がリセットされます。よろしいですか?',
variableTitle: '変数', variableTitle: '変数',
@ -327,7 +327,7 @@ const translation = {
}, },
varKeyError: { varKeyError: {
canNoBeEmpty: '{{key}} は必須です', canNoBeEmpty: '{{key}} は必須です',
tooLong: '{{key}} が長すぎます。30文字を超えることはできません', tooLong: '{{key}} が長すぎます。30 文字を超えることはできません',
notValid: '{{key}} が無効です。文字、数字、アンダースコアのみを含めることができます', notValid: '{{key}} が無効です。文字、数字、アンダースコアのみを含めることができます',
notStartWithNumber: '{{key}} は数字で始めることはできません', notStartWithNumber: '{{key}} は数字で始めることはできません',
keyAlreadyExists: '{{key}} はすでに存在します', keyAlreadyExists: '{{key}} はすでに存在します',
@ -354,11 +354,12 @@ const translation = {
'maxLength': '最大長', 'maxLength': '最大長',
'options': 'オプション', 'options': 'オプション',
'addOption': 'オプションを追加', 'addOption': 'オプションを追加',
'apiBasedVar': 'APIベースの変数', 'apiBasedVar': 'API ベースの変数',
'varName': '変数名', 'varName': '変数名',
'labelName': 'ラベル名', 'labelName': 'ラベル名',
'inputPlaceholder': '入力してください', 'inputPlaceholder': '入力してください',
'required': '必須', 'required': '必須',
'hide': '非表示',
'file': { 'file': {
supportFileTypes: 'サポートされたファイルタイプ', supportFileTypes: 'サポートされたファイルタイプ',
image: { image: {
@ -376,7 +377,7 @@ const translation = {
custom: { custom: {
name: '他のファイルタイプ', name: '他のファイルタイプ',
description: '他のファイルタイプを指定する。', description: '他のファイルタイプを指定する。',
createPlaceholder: '+ 拡張子, 例:.doc', createPlaceholder: '+ 拡張子例:.doc',
}, },
}, },
'uploadFileTypes': 'アップロードされたファイルのタイプ', 'uploadFileTypes': 'アップロードされたファイルのタイプ',
@ -388,7 +389,7 @@ const translation = {
varNameRequired: '変数名は必須です', varNameRequired: '変数名は必須です',
labelNameRequired: 'ラベル名は必須です', labelNameRequired: 'ラベル名は必須です',
varNameCanBeRepeat: '変数名は繰り返すことができません', varNameCanBeRepeat: '変数名は繰り返すことができません',
atLeastOneOption: '少なくとも1つのオプションが必要です', atLeastOneOption: '少なくとも 1 つのオプションが必要です',
optionRepeat: '繰り返しオプションがあります', optionRepeat: '繰り返しオプションがあります',
}, },
}, },
@ -473,10 +474,10 @@ const translation = {
title: 'マルチパスリトリーバル', title: 'マルチパスリトリーバル',
description: 'ユーザーの意図に基づいて、すべてのナレッジをクエリし、複数のソースから関連するテキストを取得し、再順位付け後、ユーザークエリに最適な結果を選択します。再順位付けモデル API の構成が必要です。', description: 'ユーザーの意図に基づいて、すべてのナレッジをクエリし、複数のソースから関連するテキストを取得し、再順位付け後、ユーザークエリに最適な結果を選択します。再順位付けモデル API の構成が必要です。',
}, },
embeddingModelRequired: 'Embeddingモデルが設定されていない', embeddingModelRequired: 'Embedding モデルが設定されていない',
rerankModelRequired: '再順位付けモデルが必要です', rerankModelRequired: '再順位付けモデルが必要です',
params: 'パラメータ', params: 'パラメータ',
top_k: 'トップK', top_k: 'トップ K',
top_kTip: 'ユーザーの質問に最も類似したチャンクをフィルタリングするために使用されます。システムは、選択したモデルの max_tokens に応じて、動的に Top K の値を調整します。', top_kTip: 'ユーザーの質問に最も類似したチャンクをフィルタリングするために使用されます。システムは、選択したモデルの max_tokens に応じて、動的に Top K の値を調整します。',
score_threshold: 'スコア閾値', score_threshold: 'スコア閾値',
score_thresholdTip: 'チャンクフィルタリングの類似性閾値を設定するために使用されます。', score_thresholdTip: 'チャンクフィルタリングの類似性閾値を設定するために使用されます。',
@ -518,7 +519,7 @@ const translation = {
promptPlaceholder: 'ここにプロンプトを入力してください', promptPlaceholder: 'ここにプロンプトを入力してください',
tools: { tools: {
name: 'ツール', name: 'ツール',
description: 'ツールを使用すると、インターネットの検索や科学的計算など、LLMの機能を拡張できます', description: 'ツールを使用すると、インターネットの検索や科学的計算など、LLM の機能を拡張できます',
enabled: '有効', enabled: '有効',
}, },
}, },

@ -1,6 +1,6 @@
const translation = { const translation = {
title: 'ログ', title: 'ログ',
description: 'ログは、アプリケーションの実行状態を記録します。ユーザーの入力やAIの応答などが含まれます。', description: 'ログは、アプリケーションの実行状態を記録します。ユーザーの入力や AI の応答などが含まれます。',
dateTimeFormat: 'MM/DD/YYYY hh:mm A', dateTimeFormat: 'MM/DD/YYYY hh:mm A',
table: { table: {
header: { header: {
@ -29,13 +29,13 @@ const translation = {
noOutput: '出力がありません', noOutput: '出力がありません',
element: { element: {
title: '誰かいますか?', title: '誰かいますか?',
content: 'ここでは、エンドユーザーとAIアプリケーション間の相互作用を観察し、注釈を付けることで、AIの精度を継続的に向上させます。Webアプリを<shareLink>共有</shareLink>または<testLink>テスト</testLink>してみて、このページに戻ってください。', content: 'ここでは、エンドユーザーと AI アプリケーション間の相互作用を観察し、注釈を付けることで、AI の精度を継続的に向上させます。Web アプリを<shareLink>共有</shareLink>または<testLink>テスト</testLink>してみて、このページに戻ってください。',
}, },
}, },
}, },
detail: { detail: {
time: '時間', time: '時間',
conversationId: '会話ID', conversationId: '会話 ID',
promptTemplate: 'プロンプトテンプレート', promptTemplate: 'プロンプトテンプレート',
promptTemplateBeforeChat: 'チャット前のプロンプトテンプレート・システムメッセージとして', promptTemplateBeforeChat: 'チャット前のプロンプトテンプレート・システムメッセージとして',
annotationTip: '{{user}} によってマークされた改善', annotationTip: '{{user}} によってマークされた改善',
@ -48,7 +48,7 @@ const translation = {
dislike: 'いいね解除', dislike: 'いいね解除',
addAnnotation: '改善を追加', addAnnotation: '改善を追加',
editAnnotation: '改善を編集', editAnnotation: '改善を編集',
annotationPlaceholder: '将来のモデルの微調整やテキスト生成品質の継続的改善のためにAIが返信することを期待する答えを入力してください。', annotationPlaceholder: '将来のモデルの微調整やテキスト生成品質の継続的改善のために AI が返信することを期待する答えを入力してください。',
}, },
variables: '変数', variables: '変数',
uploadImages: 'アップロードされた画像', uploadImages: 'アップロードされた画像',
@ -57,10 +57,10 @@ const translation = {
filter: { filter: {
period: { period: {
today: '今日', today: '今日',
last7days: '過去7日間', last7days: '過去 7 日間',
last4weeks: '過去4週間', last4weeks: '過去 4 週間',
last3months: '過去3ヶ月', last3months: '過去 3 ヶ月',
last12months: '過去12ヶ月', last12months: '過去 12 ヶ月',
monthToDate: '月初から今日まで', monthToDate: '月初から今日まで',
quarterToDate: '四半期初から今日まで', quarterToDate: '四半期初から今日まで',
yearToDate: '年初から今日まで', yearToDate: '年初から今日まで',

@ -1,9 +1,9 @@
const translation = { const translation = {
welcome: { welcome: {
firstStepTip: 'はじめるには、', firstStepTip: 'はじめるには、',
enterKeyTip: '以下にOpenAI APIキーを入力してください', enterKeyTip: '以下に OpenAI API キーを入力してください',
getKeyTip: 'OpenAIダッシュボードからAPIキーを取得してください', getKeyTip: 'OpenAI ダッシュボードから API キーを取得してください',
placeholder: 'OpenAI APIキーsk-xxxx', placeholder: 'OpenAI API キーsk-xxxx',
}, },
apiKeyInfo: { apiKeyInfo: {
cloud: { cloud: {
@ -12,7 +12,7 @@ const translation = {
description: 'トライアルクォータはテスト用に提供されます。トライアルクォータのコールが使い切られる前に、独自のモデルプロバイダを設定するか、追加のクォータを購入してください。', description: 'トライアルクォータはテスト用に提供されます。トライアルクォータのコールが使い切られる前に、独自のモデルプロバイダを設定するか、追加のクォータを購入してください。',
}, },
exhausted: { exhausted: {
title: 'トライアルクォータが使い切れました。APIキーを設定してください。', title: 'トライアルクォータが使い切れました。API キーを設定してください。',
description: 'トライアルクォータが使い切れました。独自のモデルプロバイダを設定するか、追加のクォータを購入してください。', description: 'トライアルクォータが使い切れました。独自のモデルプロバイダを設定するか、追加のクォータを購入してください。',
}, },
}, },
@ -25,36 +25,36 @@ const translation = {
callTimes: 'コール回数', callTimes: 'コール回数',
usedToken: '使用済みトークン', usedToken: '使用済みトークン',
setAPIBtn: 'モデルプロバイダの設定へ', setAPIBtn: 'モデルプロバイダの設定へ',
tryCloud: 'またはDifyのクラウドバージョンを無料見積もりでお試しください', tryCloud: 'または Dify のクラウドバージョンを無料見積もりでお試しください',
}, },
overview: { overview: {
title: '概要', title: '概要',
appInfo: { appInfo: {
explanation: '使いやすいAI Webアプリ', explanation: '使いやすい AI Web アプリ',
accessibleAddress: '公開URL', accessibleAddress: '公開 URL',
preview: 'プレビュー', preview: 'プレビュー',
regenerate: '再生成', regenerate: '再生成',
regenerateNotice: '公開URLを再生成しますか', regenerateNotice: '公開 URL を再生成しますか?',
preUseReminder: '続行する前にWebアプリを有効にしてください。', preUseReminder: '続行する前に Web アプリを有効にしてください。',
settings: { settings: {
entry: '設定', entry: '設定',
title: 'Webアプリの設定', title: 'Web アプリの設定',
webName: 'Webアプリの名前', webName: 'Web アプリの名前',
webDesc: 'Webアプリの説明', webDesc: 'Web アプリの説明',
webDescTip: 'このテキストはクライアント側に表示され、アプリケーションの使用方法の基本的なガイダンスを提供します。', webDescTip: 'このテキストはクライアント側に表示され、アプリケーションの使用方法の基本的なガイダンスを提供します。',
webDescPlaceholder: 'Webアプリの説明を入力してください', webDescPlaceholder: 'Web アプリの説明を入力してください',
language: '言語', language: '言語',
workflow: { workflow: {
title: 'ワークフローステップ', title: 'ワークフローステップ',
show: '表示', show: '表示',
hide: '非表示', hide: '非表示',
subTitle: 'ワークフローの詳細', subTitle: 'ワークフローの詳細',
showDesc: 'Webアプリでワークフローの詳細を表示または非表示にする', showDesc: 'Web アプリでワークフローの詳細を表示または非表示にする',
}, },
chatColorTheme: 'チャットボットのカラーテーマ', chatColorTheme: 'チャットボットのカラーテーマ',
chatColorThemeDesc: 'チャットボットのカラーテーマを設定します', chatColorThemeDesc: 'チャットボットのカラーテーマを設定します',
chatColorThemeInverted: '反転', chatColorThemeInverted: '反転',
invalidHexMessage: '無効な16進数値', invalidHexMessage: '無効な 16 進数値',
invalidPrivacyPolicy: '無効なプライバシーポリシーのリンクです。http または https で始まる有効なリンクを使用してください', invalidPrivacyPolicy: '無効なプライバシーポリシーのリンクです。http または https で始まる有効なリンクを使用してください',
more: { more: {
entry: 'その他の設定を表示', entry: 'その他の設定を表示',
@ -62,18 +62,18 @@ const translation = {
copyRightPlaceholder: '著作者または組織名を入力してください', copyRightPlaceholder: '著作者または組織名を入力してください',
privacyPolicy: 'プライバシーポリシー', privacyPolicy: 'プライバシーポリシー',
privacyPolicyPlaceholder: 'プライバシーポリシーリンクを入力してください', privacyPolicyPlaceholder: 'プライバシーポリシーリンクを入力してください',
privacyPolicyTip: '訪問者がアプリケーションが収集するデータを理解し、Difyの<privacyPolicyLink>プライバシーポリシー</privacyPolicyLink>を参照できるようにします。', privacyPolicyTip: '訪問者がアプリケーションが収集するデータを理解し、Dify の<privacyPolicyLink>プライバシーポリシー</privacyPolicyLink>を参照できるようにします。',
customDisclaimer: 'カスタム免責事項', customDisclaimer: 'カスタム免責事項',
customDisclaimerPlaceholder: '免責事項を入力してください', customDisclaimerPlaceholder: '免責事項を入力してください',
customDisclaimerTip: 'アプリケーションの使用に関する免責事項を提供します。', customDisclaimerTip: 'アプリケーションの使用に関する免責事項を提供します。',
copyrightTooltip: 'プロフェッショナルプラン以上にアップグレードしてください', copyrightTooltip: 'プロフェッショナルプラン以上にアップグレードしてください',
copyrightTip: 'Webアプリに著作権情報を表示する', copyrightTip: 'Web アプリに著作権情報を表示する',
}, },
sso: { sso: {
title: 'WebアプリのSSO', title: 'Web アプリの SSO',
tooltip: '管理者に問い合わせて、WebアプリのSSOを有効にします', tooltip: '管理者に問い合わせて、Web アプリの SSO を有効にします',
label: 'SSO認証', label: 'SSO 認証',
description: 'すべてのユーザーは、Webアプリを使用する前にSSOでログインする必要があります', description: 'すべてのユーザーは、Web アプリを使用する前に SSO でログインする必要があります',
}, },
modalTip: 'クライアント側の Web アプリ設定。', modalTip: 'クライアント側の Web アプリ設定。',
}, },
@ -81,45 +81,45 @@ const translation = {
entry: '埋め込み', entry: '埋め込み',
title: 'ウェブサイトに埋め込む', title: 'ウェブサイトに埋め込む',
explanation: 'チャットアプリをウェブサイトに埋め込む方法を選択します。', explanation: 'チャットアプリをウェブサイトに埋め込む方法を選択します。',
iframe: 'ウェブサイトの任意の場所にチャットアプリを追加するには、このiframeをHTMLコードに追加してください。', iframe: 'ウェブサイトの任意の場所にチャットアプリを追加するには、この iframe HTML コードに追加してください。',
scripts: 'ウェブサイトの右下にチャットアプリを追加するには、このコードをHTMLに追加してください。', scripts: 'ウェブサイトの右下にチャットアプリを追加するには、このコードを HTML に追加してください。',
chromePlugin: 'Dify Chatbot Chrome拡張機能をインストール', chromePlugin: 'Dify Chatbot Chrome 拡張機能をインストール',
copied: 'コピーしました', copied: 'コピーしました',
copy: 'コピー', copy: 'コピー',
}, },
qrcode: { qrcode: {
title: '共有用QRコード', title: '共有用 QR コード',
scan: 'アプリケーションの共有をスキャン', scan: 'アプリケーションの共有をスキャン',
download: 'QRコードをダウンロード', download: 'QR コードをダウンロード',
}, },
customize: { customize: {
way: '方法', way: '方法',
entry: 'カスタマイズ', entry: 'カスタマイズ',
title: 'AI Webアプリのカスタマイズ', title: 'AI Web アプリのカスタマイズ',
explanation: 'シナリオとスタイルのニーズに合わせてWebアプリのフロントエンドをカスタマイズできます。', explanation: 'シナリオとスタイルのニーズに合わせて Web アプリのフロントエンドをカスタマイズできます。',
way1: { way1: {
name: 'クライアントコードをフォークして修正し、Vercelにデプロイします(推奨)', name: 'クライアントコードをフォークして修正し、Vercel にデプロイします(推奨)',
step1: 'クライアントコードをフォークして修正します', step1: 'クライアントコードをフォークして修正します',
step1Tip: 'ここをクリックしてソースコードをGitHubアカウントにフォークし、コードを修正します', step1Tip: 'ここをクリックしてソースコードを GitHub アカウントにフォークし、コードを修正します',
step1Operation: 'Dify-WebClient', step1Operation: 'Dify-WebClient',
step2: 'Vercelにデプロイします', step2: 'Vercel にデプロイします',
step2Tip: 'ここをクリックしてリポジトリをVercelにインポートし、デプロイします', step2Tip: 'ここをクリックしてリポジトリを Vercel にインポートし、デプロイします',
step2Operation: 'リポジトリをインポート', step2Operation: 'リポジトリをインポート',
step3: '環境変数を設定します', step3: '環境変数を設定します',
step3Tip: 'Vercelに次の環境変数を追加します', step3Tip: 'Vercel に次の環境変数を追加します',
}, },
way2: { way2: {
name: 'クライアントサイドのコードを記述してAPIを呼び出し、サーバーにデプロイします', name: 'クライアントサイドのコードを記述して API を呼び出し、サーバーにデプロイします',
operation: 'ドキュメント', operation: 'ドキュメント',
}, },
}, },
launch: '発射', launch: '発射',
}, },
apiInfo: { apiInfo: {
title: 'バックエンドサービスAPI', title: 'バックエンドサービス API',
explanation: 'あなたのアプリケーションに簡単に統合できます', explanation: 'あなたのアプリケーションに簡単に統合できます',
accessibleAddress: 'サービスAPIエンドポイント', accessibleAddress: 'サービス API エンドポイント',
doc: 'APIリファレンス', doc: 'API リファレンス',
}, },
status: { status: {
running: '稼働中', running: '稼働中',
@ -132,15 +132,15 @@ const translation = {
tokenPS: 'トークン/秒', tokenPS: 'トークン/秒',
totalMessages: { totalMessages: {
title: 'トータルメッセージ数', title: 'トータルメッセージ数',
explanation: '日次AIインタラクション数。', explanation: '日次 AI インタラクション数。',
}, },
totalConversations: { totalConversations: {
title: '総会話数', title: '総会話数',
explanation: '日次AI会話数プロンプトエンジニアリング/デバッグは除外。', explanation: '日次 AI 会話数;プロンプトエンジニアリング/デバッグは除外。',
}, },
activeUsers: { activeUsers: {
title: 'アクティブユーザー数', title: 'アクティブユーザー数',
explanation: 'AIとのQ&Aに参加しているユニークユーザー数工学的/デバッグ目的のプロンプトは除外されます。', explanation: 'AI との Q&A に参加しているユニークユーザー数;工学的/デバッグ目的のプロンプトは除外されます。',
}, },
tokenUsage: { tokenUsage: {
title: 'トークン使用量', title: 'トークン使用量',
@ -149,7 +149,7 @@ const translation = {
}, },
avgSessionInteractions: { avgSessionInteractions: {
title: '平均セッションインタラクション数', title: '平均セッションインタラクション数',
explanation: 'ユーザーとAIの連続的なコミュニケーション数対話型アプリケーション向け。', explanation: 'ユーザーと AI の連続的なコミュニケーション数;対話型アプリケーション向け。',
}, },
avgUserInteractions: { avgUserInteractions: {
title: '平均ユーザーインタラクション数', title: '平均ユーザーインタラクション数',
@ -157,15 +157,15 @@ const translation = {
}, },
userSatisfactionRate: { userSatisfactionRate: {
title: 'ユーザー満足度率', title: 'ユーザー満足度率',
explanation: '1,000件のメッセージあたりの「いいね」の数。これは、ユーザーが非常に満足している回答の割合を示します。', explanation: '1,000 件のメッセージあたりの「いいね」の数。これは、ユーザーが非常に満足している回答の割合を示します。',
}, },
avgResponseTime: { avgResponseTime: {
title: '平均応答時間', title: '平均応答時間',
explanation: 'AIが処理/応答する時間(ミリ秒);テキストベースのアプリケーション向け。', explanation: 'AI が処理/応答する時間(ミリ秒);テキストベースのアプリケーション向け。',
}, },
tps: { tps: {
title: 'トークン出力速度', title: 'トークン出力速度',
explanation: 'LLMのパフォーマンスを測定します。リクエストの開始から出力の完了までのLLMのトークン出力速度を数えます。', explanation: 'LLM のパフォーマンスを測定します。リクエストの開始から出力の完了までの LLM のトークン出力速度を数えます。',
}, },
}, },
} }

@ -19,10 +19,10 @@ const translation = {
exportFailed: 'DSL のエクスポートに失敗しました。', exportFailed: 'DSL のエクスポートに失敗しました。',
importDSL: 'DSL ファイルをインポート', importDSL: 'DSL ファイルをインポート',
createFromConfigFile: 'DSL ファイルから作成する', createFromConfigFile: 'DSL ファイルから作成する',
importFromDSL: 'DSLからインポート', importFromDSL: 'DSL からインポート',
importFromDSLFile: 'DSLファイルから', importFromDSLFile: 'DSL ファイルから',
importFromDSLUrl: 'URLから', importFromDSLUrl: 'URL から',
importFromDSLUrlPlaceholder: 'DSLリンクをここに貼り付けます', importFromDSLUrlPlaceholder: 'DSL リンクをここに貼り付けます',
deleteAppConfirmTitle: 'このアプリを削除しますか?', deleteAppConfirmTitle: 'このアプリを削除しますか?',
deleteAppConfirmContent: deleteAppConfirmContent:
'アプリを削除すると、元に戻すことはできません。他のユーザーはもはやこのアプリにアクセスできず、すべてのプロンプトの設定とログが永久に削除されます。', 'アプリを削除すると、元に戻すことはできません。他のユーザーはもはやこのアプリにアクセスできず、すべてのプロンプトの設定とログが永久に削除されます。',
@ -73,11 +73,11 @@ const translation = {
appCreateFailed: 'アプリの作成に失敗しました', appCreateFailed: 'アプリの作成に失敗しました',
Confirm: '確認する', Confirm: '確認する',
caution: '注意', caution: '注意',
appCreateDSLErrorPart2: '続行しますか?', appCreateDSLErrorPart2: '続行しますか',
appCreateDSLErrorPart4: 'システムがサポートするDSLバージョン:', appCreateDSLErrorPart4: 'システムがサポートする DSL バージョン:',
appCreateDSLErrorPart3: '現在のアプリケーションの DSL バージョン:', appCreateDSLErrorPart3: '現在のアプリケーションの DSL バージョン',
appCreateDSLErrorTitle: 'バージョンの非互換性', appCreateDSLErrorTitle: 'バージョンの非互換性',
appCreateDSLWarning: '注意:DSLのバージョンの違いは、特定の機能に影響を与える可能性があります', appCreateDSLWarning: '注意:DSL のバージョンの違いは、特定の機能に影響を与える可能性があります',
appCreateDSLErrorPart1: 'DSL バージョンに大きな違いが検出されました。インポートを強制すると、アプリケーションが誤動作する可能性があります。', appCreateDSLErrorPart1: 'DSL バージョンに大きな違いが検出されました。インポートを強制すると、アプリケーションが誤動作する可能性があります。',
optional: '随意', optional: '随意',
forBeginners: '初心者向けの基本的なアプリタイプ', forBeginners: '初心者向けの基本的なアプリタイプ',
@ -95,11 +95,11 @@ const translation = {
forAdvanced: '上級ユーザー向け', forAdvanced: '上級ユーザー向け',
chooseAppType: 'アプリタイプを選択', chooseAppType: 'アプリタイプを選択',
learnMore: '詳細情報', learnMore: '詳細情報',
noIdeaTip: 'アイデアがありませんか?テンプレートをご覧ください', noIdeaTip: 'アイデアがありませんかテンプレートをご覧ください',
chatbotShortDescription: '簡単なセットアップのLLMベースのチャットボット', chatbotShortDescription: '簡単なセットアップの LLM ベースのチャットボット',
chatbotUserDescription: '簡単な設定でLLMベースのチャットボットを迅速に構築します。Chatflowは後で切り替えることができます。', chatbotUserDescription: '簡単な設定で LLM ベースのチャットボットを迅速に構築します。Chatflow は後で切り替えることができます。',
workflowUserDescription: 'ドラッグ&ドロップの簡易性で自律型AIワークフローを視覚的に構築', workflowUserDescription: 'ドラッグ&ドロップの簡易性で自律型 AI ワークフローを視覚的に構築',
completionUserDescription: '簡単な構成でテキスト生成タスク用のAIアシスタントをすばやく構築します。', completionUserDescription: '簡単な構成でテキスト生成タスク用の AI アシスタントをすばやく構築します。',
}, },
editApp: '情報を編集する', editApp: '情報を編集する',
editAppTitle: 'アプリ情報を編集する', editAppTitle: 'アプリ情報を編集する',
@ -129,7 +129,7 @@ const translation = {
}, },
tracing: { tracing: {
title: 'アプリのパフォーマンスの追跡', title: 'アプリのパフォーマンスの追跡',
description: 'サードパーティのLLMOpsサービスとトレースアプリケーションのパフォーマンス設定を行います。', description: 'サードパーティの LLMOps サービスとトレースアプリケーションのパフォーマンス設定を行います。',
config: '設定', config: '設定',
view: '見る', view: '見る',
collapse: '折りたたむ', collapse: '折りたたむ',
@ -138,7 +138,7 @@ const translation = {
disabled: '無効しました', disabled: '無効しました',
disabledTip: 'まずはサービスの設定から始めましょう。', disabledTip: 'まずはサービスの設定から始めましょう。',
enabled: '有効しました', enabled: '有効しました',
tracingDescription: 'LLMの呼び出し、コンテキスト、プロンプト、HTTPリクエストなど、アプリケーション実行の全ての文脈をサードパーティのトレースプラットフォームで取り込みます。', tracingDescription: 'LLM の呼び出し、コンテキスト、プロンプト、HTTP リクエストなど、アプリケーション実行の全ての文脈をサードパーティのトレースプラットフォームで取り込みます。',
configProviderTitle: { configProviderTitle: {
configured: '設定しました', configured: '設定しました',
notConfigured: 'トレース機能を有効化するためには、サービスの設定が必要です。', notConfigured: 'トレース機能を有効化するためには、サービスの設定が必要です。',
@ -146,11 +146,11 @@ const translation = {
}, },
langsmith: { langsmith: {
title: 'LangSmith', title: 'LangSmith',
description: 'LLMを利用したアプリケーションのライフサイクル全段階を支援する、オールインワンの開発者向けプラットフォームです。', description: 'LLM を利用したアプリケーションのライフサイクル全段階を支援する、オールインワンの開発者向けプラットフォームです。',
}, },
langfuse: { langfuse: {
title: 'Langfuse', title: 'Langfuse',
description: 'トレース、評価、プロンプトの管理、そしてメトリクスを駆使して、LLMアプリケーションのデバッグや改善に役立てます。', description: 'トレース、評価、プロンプトの管理、そしてメトリクスを駆使して、LLM アプリケーションのデバッグや改善に役立てます。',
}, },
opik: { opik: {
title: 'オピック', title: 'オピック',
@ -169,13 +169,13 @@ const translation = {
}, },
weave: { weave: {
title: '織る', title: '織る',
description: 'Weaveは、LLMアプリケーションを評価、テスト、および監視するためのオープンソースプラットフォームです。', description: 'Weave は、LLM アプリケーションを評価、テスト、および監視するためのオープンソースプラットフォームです。',
}, },
}, },
answerIcon: { answerIcon: {
title: 'Webアプリアイコンを使用して🤖を置き換える', title: 'Web アプリアイコンを使用して🤖を置き換える',
description: '共有アプリケーションの中で Webアプリアイコンを使用して🤖を置き換えるかどうか', description: '共有アプリケーションの中で Web アプリアイコンを使用して🤖を置き換えるかどうか',
descriptionInExplore: 'ExploreでWebアプリアイコンを使用して🤖を置き換えるかどうか', descriptionInExplore: 'Explore Web アプリアイコンを使用して🤖を置き換えるかどうか',
}, },
newAppFromTemplate: { newAppFromTemplate: {
sidebar: { sidebar: {
@ -198,42 +198,39 @@ const translation = {
placeholder: 'アプリを選択...', placeholder: 'アプリを選択...',
}, },
structOutput: { structOutput: {
moreFillTip: '最大10レベルのネストを表示します', moreFillTip: '最大 10 レベルのネストを表示します',
required: '必須', required: '必須',
LLMResponse: 'LLMのレスポンス', LLMResponse: 'LLM のレスポンス',
configure: '設定', configure: '設定',
notConfiguredTip: '構造化出力が未設定です', notConfiguredTip: '構造化出力が未設定です',
structured: '構造化出力', structured: '構造化出力',
structuredTip: '構造化出力は、モデルが常に指定されたJSONスキーマに準拠した応答を生成することを保証する機能です。', structuredTip: '構造化出力は、モデルが常に指定された JSON スキーマに準拠した応答を生成することを保証する機能です。',
modelNotSupported: 'モデルが対応していません', modelNotSupported: 'モデルが対応していません',
modelNotSupportedTip: '現在のモデルはこの機能に対応しておらず、自動的にプロンプトインジェクションに切り替わります。', modelNotSupportedTip: '現在のモデルはこの機能に対応しておらず、自動的にプロンプトインジェクションに切り替わります。',
}, },
accessControl: 'Webアプリアクセス制御', accessControl: 'Web アプリアクセス制御',
accessItemsDescription: { accessItemsDescription: {
anyone: '誰でも Web アプリにアクセス可能', anyone: '誰でもこの web アプリにアクセスできます(ログイン不要)',
specific: '特定のグループまたはメンバーのみが Web アプリにアクセス可能', specific: '特定のプラットフォーム内メンバーのみがこの Web アプリにアクセスできます',
organization: '組織内の誰でも Web アプリにアクセス可能', organization: 'プラットフォーム内の全メンバーがこの Web アプリにアクセスできます',
external: '認証済みの外部ユーザーのみがこの Web アプリにアクセスできます',
}, },
accessControlDialog: { accessControlDialog: {
title: 'アクセス権限', title: 'アクセス権限',
description: 'Webアプリのアクセス権限を設定します', description: 'Web アプリのアクセス権限を設定します',
accessLabel: '誰がアクセスできますか', accessLabel: '誰がアクセスできますか',
accessItemsDescription: {
anyone: '誰でもWebアプリにアクセス可能です',
specific: '特定のグループやメンバーがWebアプリにアクセス可能です',
organization: '組織内の誰でもWebアプリにアクセス可能です',
},
accessItems: { accessItems: {
anyone: 'すべてのユーザー', anyone: 'リンクを知っているすべてのユーザー',
specific: '特定のグループメンバー', specific: '特定のプラットフォーム内メンバー',
organization: 'グループ内の全員', organization: 'プラットフォーム内の全メンバー',
external: '認証済みの外部ユーザー',
}, },
groups_one: '{{count}} グループ', groups_one: '{{count}} グループ',
groups_other: '{{count}} グループ', groups_other: '{{count}} グループ',
members_one: '{{count}} メンバー', members_one: '{{count}} メンバー',
members_other: '{{count}} メンバー', members_other: '{{count}} メンバー',
noGroupsOrMembers: 'グループまたはメンバーが選択されていません', noGroupsOrMembers: 'グループまたはメンバーが選択されていません',
webAppSSONotEnabledTip: 'Webアプリの認証方式設定については、企業管理者へご連絡ください。', webAppSSONotEnabledTip: 'Web アプリの外部認証方式を設定するには、組織の管理者にお問い合わせください。',
operateGroupAndMember: { operateGroupAndMember: {
searchPlaceholder: 'グループやメンバーを検索', searchPlaceholder: 'グループやメンバーを検索',
allMembers: 'すべてのメンバー', allMembers: 'すべてのメンバー',
@ -243,11 +240,11 @@ const translation = {
updateSuccess: '更新が成功しました', updateSuccess: '更新が成功しました',
}, },
publishApp: { publishApp: {
title: 'Webアプリへのアクセス権', title: 'Web アプリへのアクセス権',
notSet: '未設定', notSet: '未設定',
notSetDesc: '現在このWebアプリには誰もアクセスできません。権限を設定してください。', notSetDesc: '現在この Web アプリには誰もアクセスできません。権限を設定してください。',
}, },
noAccessPermission: 'Webアプリにアクセス権限がありません', noAccessPermission: 'Web アプリにアクセス権限がありません',
} }
export default translation export default translation

@ -16,11 +16,11 @@ const translation = {
viewBilling: '請求とサブスクリプションの管理', viewBilling: '請求とサブスクリプションの管理',
buyPermissionDeniedTip: 'サブスクリプションするには、エンタープライズ管理者に連絡してください', buyPermissionDeniedTip: 'サブスクリプションするには、エンタープライズ管理者に連絡してください',
plansCommon: { plansCommon: {
title: 'あなたのAIの旅を支える価格設定', title: 'あなたの AI の旅を支える価格設定',
freeTrialTipPrefix: 'サインアップ後、', freeTrialTipPrefix: 'サインアップ後、',
freeTrialTip: '200回のOpenAIコールの無料に受け取る', freeTrialTip: '200 回の OpenAI コールの無料に受け取る',
freeTrialTipSuffix: '。クレジットカード不要', freeTrialTipSuffix: '。クレジットカード不要',
yearlyTip: '10ヶ月分支払って、1年間楽しもう', yearlyTip: '10 ヶ月分支払って、1 年間楽しもう!',
mostPopular: '人気', mostPopular: '人気',
cloud: 'クラウドサービス', cloud: 'クラウドサービス',
self: 'セルフホストサービス', self: 'セルフホストサービス',
@ -53,11 +53,11 @@ const translation = {
vectorSpace: '{{size}}の知識データストレージ', vectorSpace: '{{size}}の知識データストレージ',
vectorSpaceTooltip: '高品質インデックスモードのドキュメントは、知識データストレージのリソースを消費します。知識データストレージの上限に達すると、新しいドキュメントはアップロードされません。', vectorSpaceTooltip: '高品質インデックスモードのドキュメントは、知識データストレージのリソースを消費します。知識データストレージの上限に達すると、新しいドキュメントはアップロードされません。',
documentsRequestQuota: '{{count,number}}/分のナレッジ リクエストのレート制限', documentsRequestQuota: '{{count,number}}/分のナレッジ リクエストのレート制限',
documentsRequestQuotaTooltip: 'ナレッジベース内でワークスペースが1分間に実行できる操作の総数を示します。これには、データセットの作成、削除、更新、ドキュメントのアップロード、修正、アーカイブ、およびナレッジベースクエリが含まれます。この指標は、ナレッジベースリクエストのパフォーマンスを評価するために使用されます。例えば、Sandbox ユーザーが1分間に10回連続でヒットテストを実行した場合、そのワークスペースは次の1分間、データセットの作成、削除、更新、ドキュメントのアップロードや修正などの操作を一時的に実行できなくなります。', documentsRequestQuotaTooltip: 'ナレッジベース内でワークスペースが 1 分間に実行できる操作の総数を示します。これには、データセットの作成、削除、更新、ドキュメントのアップロード、修正、アーカイブ、およびナレッジベースクエリが含まれます。この指標は、ナレッジベースリクエストのパフォーマンスを評価するために使用されます。例えば、Sandbox ユーザーが 1 分間に 10 回連続でヒットテストを実行した場合、そのワークスペースは次の 1 分間、データセットの作成、削除、更新、ドキュメントのアップロードや修正などの操作を一時的に実行できなくなります。',
apiRateLimit: 'APIレート制限', apiRateLimit: 'API レート制限',
apiRateLimitUnit: '{{count,number}}/日', apiRateLimitUnit: '{{count,number}}/日',
unlimitedApiRate: '無制限のAPIコール', unlimitedApiRate: '無制限の API コール',
apiRateLimitTooltip: 'APIレート制限は、テキスト生成、チャットボット、ワークフロー、ドキュメント処理など、Dify API経由のすべてのリクエストに適用されます。', apiRateLimitTooltip: 'API レート制限は、テキスト生成、チャットボット、ワークフロー、ドキュメント処理など、Dify API 経由のすべてのリクエストに適用されます。',
documentProcessingPriority: '文書処理', documentProcessingPriority: '文書処理',
documentProcessingPriorityUpgrade: 'より高い精度と高速な速度でデータを処理します。', documentProcessingPriorityUpgrade: 'より高い精度と高速な速度でデータを処理します。',
priority: { priority: {
@ -76,16 +76,16 @@ const translation = {
emailSupport: 'メールサポート', emailSupport: 'メールサポート',
priorityEmail: '優先メール&チャットサポート', priorityEmail: '優先メール&チャットサポート',
logoChange: 'ロゴ変更', logoChange: 'ロゴ変更',
SSOAuthentication: 'SSO認証', SSOAuthentication: 'SSO 認証',
personalizedSupport: '個別サポート', personalizedSupport: '個別サポート',
dedicatedAPISupport: '専用APIサポート', dedicatedAPISupport: '専用 API サポート',
customIntegration: 'カスタム統合とサポート', customIntegration: 'カスタム統合とサポート',
ragAPIRequest: 'RAG APIリクエスト', ragAPIRequest: 'RAG API リクエスト',
bulkUpload: 'ドキュメントの一括アップロード', bulkUpload: 'ドキュメントの一括アップロード',
agentMode: 'エージェントモード', agentMode: 'エージェントモード',
workflow: 'ワークフロー', workflow: 'ワークフロー',
llmLoadingBalancing: 'LLMロードバランシング', llmLoadingBalancing: 'LLM ロードバランシング',
llmLoadingBalancingTooltip: 'APIレート制限を効果的に回避するために、モデルに複数のAPIキーを追加する。', llmLoadingBalancingTooltip: 'API レート制限を効果的に回避するために、モデルに複数の API キーを追加する。',
}, },
comingSoon: '近日公開', comingSoon: '近日公開',
member: 'メンバー', member: 'メンバー',
@ -93,13 +93,13 @@ const translation = {
messageRequest: { messageRequest: {
title: '{{count,number}}メッセージクレジット', title: '{{count,number}}メッセージクレジット',
titlePerMonth: '{{count,number}}メッセージクレジット/月', titlePerMonth: '{{count,number}}メッセージクレジット/月',
tooltip: 'メッセージクレジットは、DifyでさまざまなOpenAIモデルを簡単にお試しいただくためのものです。モデルタイプに応じてクレジットが消費され、使い切った後はご自身のOpenAI APIキーに切り替えていただけます。', tooltip: 'メッセージクレジットは、Dify でさまざまな OpenAI モデルを簡単にお試しいただくためのものです。モデルタイプに応じてクレジットが消費され、使い切った後はご自身の OpenAI API キーに切り替えていただけます。',
}, },
annotatedResponse: { annotatedResponse: {
title: '{{count,number}}の注釈クォータ制限', title: '{{count,number}}の注釈クォータ制限',
tooltip: '手動での回答の編集と注釈により、カスタマイズ可能な高品質の質問応答機能をアプリに提供します。(チャットアプリのみに適用)', tooltip: '手動での回答の編集と注釈により、カスタマイズ可能な高品質の質問応答機能をアプリに提供します。(チャットアプリのみに適用)',
}, },
ragAPIRequestTooltip: 'Difyのナレッジベース処理機能のみを呼び出すAPI呼び出しの数を指します。', ragAPIRequestTooltip: 'Dify のナレッジベース処理機能のみを呼び出す API 呼び出しの数を指します。',
receiptInfo: 'チームオーナーとチーム管理者のみが購読および請求情報を表示できます', receiptInfo: 'チームオーナーとチーム管理者のみが購読および請求情報を表示できます',
}, },
plans: { plans: {
@ -124,11 +124,11 @@ const translation = {
description: 'オープンソース版の無料プラン', description: 'オープンソース版の無料プラン',
price: '無料', price: '無料',
btnText: 'コミュニティ版を始めましょう', btnText: 'コミュニティ版を始めましょう',
includesTitle: '無料機能:', includesTitle: '無料機能',
features: [ features: [
'パブリックリポジトリの全コア機能', 'パブリックリポジトリの全コア機能',
'シングルワークスペース', 'シングルワークスペース',
'Difyオープンソースライセンス準拠', 'Dify オープンソースライセンス準拠',
], ],
}, },
premium: { premium: {
@ -138,12 +138,12 @@ const translation = {
price: '従量制', price: '従量制',
priceTip: 'クラウドマーケットプレイス基準', priceTip: 'クラウドマーケットプレイス基準',
btnText: 'プレミアム版を取得', btnText: 'プレミアム版を取得',
includesTitle: 'コミュニティ版機能に加えて:', includesTitle: 'コミュニティ版機能に加えて',
comingSoon: 'Microsoft Azure & Google Cloud 近日対応', comingSoon: 'Microsoft Azure & Google Cloud 近日対応',
features: [ features: [
'クラウドプロバイダーによる自己管理', 'クラウドプロバイダーによる自己管理',
'シングルワークスペース', 'シングルワークスペース',
'Webアプリのロゴ&ブランドカスタマイズ', 'Web アプリのロゴ&ブランドカスタマイズ',
'優先メール/チャットサポート', '優先メール/チャットサポート',
], ],
}, },
@ -154,14 +154,14 @@ const translation = {
price: 'カスタム', price: 'カスタム',
priceTip: '年間契約専用', priceTip: '年間契約専用',
btnText: '営業に相談', btnText: '営業に相談',
includesTitle: 'プレミアム版機能に加えて:', includesTitle: 'プレミアム版機能に加えて',
features: [ features: [
'エンタープライズ向け拡張ソリューション', 'エンタープライズ向け拡張ソリューション',
'商用ライセンス認可', '商用ライセンス認可',
'企業専用機能', '企業専用機能',
'マルチワークスペース管理', 'マルチワークスペース管理',
'シングルサインオンSSO', 'シングルサインオンSSO',
'DifyパートナーによるSLA保証', 'Dify パートナーによる SLA 保証',
'高度なセキュリティ管理', '高度なセキュリティ管理',
'公式メンテナンス&アップデート', '公式メンテナンス&アップデート',
'プロフェッショナル技術支援', 'プロフェッショナル技術支援',

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

Loading…
Cancel
Save