diff --git a/.github/actions/setup-uv/action.yml b/.github/actions/setup-uv/action.yml index a596be63f7..0499b44dba 100644 --- a/.github/actions/setup-uv/action.yml +++ b/.github/actions/setup-uv/action.yml @@ -8,7 +8,7 @@ inputs: uv-version: description: UV version to set up required: true - default: '0.6.14' + default: '~=0.7.11' uv-lockfile: description: Path to the UV lockfile to restore cache from required: true diff --git a/.gitignore b/.gitignore index 8818ab6f65..74a9ef63ef 100644 --- a/.gitignore +++ b/.gitignore @@ -192,12 +192,12 @@ sdks/python-client/dist sdks/python-client/dify_client.egg-info .vscode/* -!.vscode/launch.json +!.vscode/launch.json.template +!.vscode/README.md pyrightconfig.json api/.vscode .idea/ -.vscode # pnpm /.pnpm-store @@ -207,3 +207,6 @@ plugins.jsonl # mise mise.toml + +# Next.js build output +.next/ diff --git a/.vscode/README.md b/.vscode/README.md new file mode 100644 index 0000000000..26516f0540 --- /dev/null +++ b/.vscode/README.md @@ -0,0 +1,14 @@ +# Debugging with VS Code + +This `launch.json.template` file provides various debug configurations for the Dify project within VS Code / Cursor. To use these configurations, you should copy the contents of this file into a new file named `launch.json` in the same `.vscode` directory. + +## How to Use + +1. **Create `launch.json`**: If you don't have one, create a file named `launch.json` inside the `.vscode` directory. +2. **Copy Content**: Copy the entire content from `launch.json.template` into your newly created `launch.json` file. +3. **Select Debug Configuration**: Go to the Run and Debug view in VS Code / Cursor (Ctrl+Shift+D or Cmd+Shift+D). +4. **Start Debugging**: Select the desired configuration from the dropdown menu and click the green play button. + +## Tips + +- If you need to debug with Edge browser instead of Chrome, modify the `serverReadyAction` configuration in the "Next.js: debug full stack" section, change `"debugWithChrome"` to `"debugWithEdge"` to use Microsoft Edge for debugging. diff --git a/.vscode/launch.json.template b/.vscode/launch.json.template new file mode 100644 index 0000000000..f5a7f0893b --- /dev/null +++ b/.vscode/launch.json.template @@ -0,0 +1,68 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Flask API", + "type": "debugpy", + "request": "launch", + "module": "flask", + "env": { + "FLASK_APP": "app.py", + "FLASK_ENV": "development", + "GEVENT_SUPPORT": "True" + }, + "args": [ + "run", + "--host=0.0.0.0", + "--port=5001", + "--no-debugger", + "--no-reload" + ], + "jinja": true, + "justMyCode": true, + "cwd": "${workspaceFolder}/api", + "python": "${workspaceFolder}/api/.venv/bin/python" + }, + { + "name": "Python: Celery Worker (Solo)", + "type": "debugpy", + "request": "launch", + "module": "celery", + "env": { + "GEVENT_SUPPORT": "True" + }, + "args": [ + "-A", + "app.celery", + "worker", + "-P", + "solo", + "-c", + "1", + "-Q", + "dataset,generation,mail,ops_trace", + "--loglevel", + "INFO" + ], + "justMyCode": false, + "cwd": "${workspaceFolder}/api", + "python": "${workspaceFolder}/api/.venv/bin/python" + }, + { + "name": "Next.js: debug full stack", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/web/node_modules/next/dist/bin/next", + "runtimeArgs": ["--inspect"], + "skipFiles": ["/**"], + "serverReadyAction": { + "action": "debugWithChrome", + "killOnServerStop": true, + "pattern": "- Local:.+(https?://.+)", + "uriFormat": "%s", + "webRoot": "${workspaceFolder}/web" + }, + "cwd": "${workspaceFolder}/web" + } + ] +} diff --git a/api/.env.example b/api/.env.example index ae7e82c779..7878308588 100644 --- a/api/.env.example +++ b/api/.env.example @@ -491,3 +491,10 @@ OTEL_METRIC_EXPORT_TIMEOUT=30000 # Prevent Clickjacking 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 diff --git a/api/.ruff.toml b/api/.ruff.toml index 41a24abad9..facb0d5419 100644 --- a/api/.ruff.toml +++ b/api/.ruff.toml @@ -43,6 +43,7 @@ select = [ "S307", # suspicious-eval-usage, disallow use of `eval` and `ast.literal_eval` "S301", # suspicious-pickle-usage, disallow use of `pickle` and its wrappers. "S302", # suspicious-marshal-usage, disallow use of `marshal` module + "S311", # suspicious-non-cryptographic-random-usage ] ignore = [ diff --git a/api/Dockerfile b/api/Dockerfile index cff696ff56..7e4997507f 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -4,7 +4,7 @@ FROM python:3.12-slim-bookworm AS base WORKDIR /app/api # Install uv -ENV UV_VERSION=0.6.14 +ENV UV_VERSION=0.7.11 RUN pip install --no-cache-dir uv==${UV_VERSION} diff --git a/api/configs/middleware/__init__.py b/api/configs/middleware/__init__.py index 1b015b3267..2dcf1710b0 100644 --- a/api/configs/middleware/__init__.py +++ b/api/configs/middleware/__init__.py @@ -2,7 +2,7 @@ import os from typing import Any, Literal, Optional 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 .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( # place the configs in alphabet order CeleryConfig, @@ -303,5 +322,6 @@ class MiddlewareConfig( BaiduVectorDBConfig, OpenGaussConfig, TableStoreConfig, + DatasetQueueMonitorConfig, ): pass diff --git a/api/controllers/inner_api/plugin/wraps.py b/api/controllers/inner_api/plugin/wraps.py index b2849a7962..50408e0929 100644 --- a/api/controllers/inner_api/plugin/wraps.py +++ b/api/controllers/inner_api/plugin/wraps.py @@ -32,6 +32,7 @@ def get_user(tenant_id: str, user_id: str | None) -> Account | EndUser: ) session.add(user_model) session.commit() + session.refresh(user_model) else: user_model = AccountService.load_user(user_id) if not user_model: diff --git a/api/controllers/service_api/dataset/dataset.py b/api/controllers/service_api/dataset/dataset.py index c100f53078..27e8dd3fa6 100644 --- a/api/controllers/service_api/dataset/dataset.py +++ b/api/controllers/service_api/dataset/dataset.py @@ -369,6 +369,7 @@ class DatasetTagsApi(DatasetApiResource): ) parser.add_argument("tag_id", nullable=False, required=True, help="Id of a tag.", type=str) args = parser.parse_args() + args["type"] = "knowledge" tag = TagService.update_tags(args, args.get("tag_id")) binding_count = TagService.get_tag_binding_count(args.get("tag_id")) diff --git a/api/controllers/service_api/dataset/document.py b/api/controllers/service_api/dataset/document.py index 418363ffbb..ab7ab4dcf0 100644 --- a/api/controllers/service_api/dataset/document.py +++ b/api/controllers/service_api/dataset/document.py @@ -175,8 +175,11 @@ class DocumentAddByFileApi(DatasetApiResource): if not dataset: 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.") + args["indexing_technique"] = indexing_technique # save file info file = request.files["file"] @@ -206,12 +209,16 @@ class DocumentAddByFileApi(DatasetApiResource): knowledge_config = KnowledgeConfig(**args) 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: documents, batch = DocumentService.save_document_with_dataset_id( dataset=dataset, knowledge_config=knowledge_config, 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", ) except ProviderTokenNotInitError as ex: diff --git a/api/core/entities/model_entities.py b/api/core/entities/model_entities.py index 5017835565..e1c021a44a 100644 --- a/api/core/entities/model_entities.py +++ b/api/core/entities/model_entities.py @@ -55,6 +55,25 @@ class ProviderModelWithStatusEntity(ProviderModel): status: ModelStatus 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): """ diff --git a/api/core/extension/extensible.py b/api/core/extension/extensible.py index 231743bf2a..06fdb089d4 100644 --- a/api/core/extension/extensible.py +++ b/api/core/extension/extensible.py @@ -41,45 +41,53 @@ class Extensible: extensions = [] position_map: dict[str, int] = {} - # get the path of the current class - current_path = os.path.abspath(cls.__module__.replace(".", os.path.sep) + ".py") - current_dir_path = os.path.dirname(current_path) + # Get the package name from the module path + package_name = ".".join(cls.__module__.split(".")[:-1]) - # traverse subdirectories - for subdir_name in os.listdir(current_dir_path): - if subdir_name.startswith("__"): - continue + try: + # Get package directory path + package_spec = importlib.util.find_spec(package_name) + if not package_spec or not package_spec.origin: + raise ImportError(f"Could not find package {package_name}") - subdir_path = os.path.join(current_dir_path, subdir_name) - extension_name = subdir_name - if os.path.isdir(subdir_path): + package_dir = os.path.dirname(package_spec.origin) + + # 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) - # is builtin extension, builtin extension - # in the front-end page and business logic, there are special treatments. + # Check for extension module file + 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 - # default position is 0 can not be None for sort_to_dict_by_position_map position = 0 if "__builtin__" in file_names: builtin = True - builtin_file_path = os.path.join(subdir_path, "__builtin__") if os.path.exists(builtin_file_path): position = int(Path(builtin_file_path).read_text(encoding="utf-8").strip()) position_map[extension_name] = position - if (extension_name + ".py") not in file_names: - logging.warning(f"Missing {extension_name}.py file in {subdir_path}, Skip.") - continue - - # 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) + # Import the extension module + module_name = f"{package_name}.{extension_name}.{extension_name}" + spec = importlib.util.find_spec(module_name) 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) spec.loader.exec_module(mod) + # Find extension class extension_class = None for name, obj in vars(mod).items(): if isinstance(obj, type) and issubclass(obj, cls) and obj != cls: @@ -87,21 +95,21 @@ class Extensible: break 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 + # Load schema if not builtin json_data: dict[str, Any] = {} 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.") continue - json_path = os.path.join(subdir_path, "schema.json") - json_data = {} - if os.path.exists(json_path): - with open(json_path, encoding="utf-8") as f: - json_data = json.load(f) + with open(json_path, encoding="utf-8") as f: + json_data = json.load(f) + # Create extension extensions.append( ModuleExtension( 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( position_map=position_map, data=extensions, name_func=lambda x: x.name ) diff --git a/api/core/helper/moderation.py b/api/core/helper/moderation.py index 6a5982eca4..a324ac2767 100644 --- a/api/core/helper/moderation.py +++ b/api/core/helper/moderation.py @@ -1,5 +1,5 @@ import logging -import random +import secrets from typing import cast from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity @@ -38,7 +38,7 @@ def check_moderation(tenant_id: str, model_config: ModelConfigWithCredentialsEnt if len(text_chunks) == 0: return True - text_chunk = random.choice(text_chunks) + text_chunk = secrets.choice(text_chunks) try: model_provider_factory = ModelProviderFactory(tenant_id) diff --git a/api/core/model_runtime/entities/model_entities.py b/api/core/model_runtime/entities/model_entities.py index 373ef2bbe2..568149cc37 100644 --- a/api/core/model_runtime/entities/model_entities.py +++ b/api/core/model_runtime/entities/model_entities.py @@ -160,6 +160,10 @@ class ProviderModel(BaseModel): deprecated: bool = False 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): """ diff --git a/api/core/ops/entities/config_entity.py b/api/core/ops/entities/config_entity.py index f2d1bd305a..c988bf48d1 100644 --- a/api/core/ops/entities/config_entity.py +++ b/api/core/ops/entities/config_entity.py @@ -98,6 +98,7 @@ class WeaveConfig(BaseTracingConfig): entity: str | None = None project: str endpoint: str = "https://trace.wandb.ai" + host: str | None = None @field_validator("endpoint") @classmethod @@ -109,6 +110,14 @@ class WeaveConfig(BaseTracingConfig): return v + @field_validator("host") + @classmethod + def validate_host(cls, v, info: ValidationInfo): + if v is not None and v != "": + if not v.startswith(("https://", "http://")): + raise ValueError("host must start with https:// or http://") + return v + OPS_FILE_PATH = "ops_trace/" OPS_TRACE_FAILED_KEY = "FAILED_OPS_TRACE" diff --git a/api/core/ops/ops_trace_manager.py b/api/core/ops/ops_trace_manager.py index dc4cfc48db..e0dfe0c312 100644 --- a/api/core/ops/ops_trace_manager.py +++ b/api/core/ops/ops_trace_manager.py @@ -81,7 +81,7 @@ class OpsTraceProviderConfigMap(dict[str, dict[str, Any]]): return { "config_class": WeaveConfig, "secret_keys": ["api_key"], - "other_keys": ["project", "entity", "endpoint"], + "other_keys": ["project", "entity", "endpoint", "host"], "trace_instance": WeaveDataTrace, } diff --git a/api/core/ops/weave_trace/weave_trace.py b/api/core/ops/weave_trace/weave_trace.py index cfc8a505bb..3917348a91 100644 --- a/api/core/ops/weave_trace/weave_trace.py +++ b/api/core/ops/weave_trace/weave_trace.py @@ -40,9 +40,14 @@ class WeaveDataTrace(BaseTraceInstance): self.weave_api_key = weave_config.api_key self.project_name = weave_config.project self.entity = weave_config.entity + self.host = weave_config.host + + # Login with API key first, including host if provided + if self.host: + login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True, host=self.host) + else: + login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True) - # Login with API key first - login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True) if not login_status: logger.error("Failed to login to Weights & Biases with the provided API key") raise ValueError("Weave login failed") @@ -386,7 +391,11 @@ class WeaveDataTrace(BaseTraceInstance): def api_check(self): try: - login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True) + if self.host: + login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True, host=self.host) + else: + login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True) + if not login_status: raise ValueError("Weave login failed") else: diff --git a/api/core/provider_manager.py b/api/core/provider_manager.py index 7570200175..488a394679 100644 --- a/api/core/provider_manager.py +++ b/api/core/provider_manager.py @@ -3,7 +3,9 @@ from collections import defaultdict from json import JSONDecodeError from typing import Any, Optional, cast +from sqlalchemy import select from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import Session from configs import dify_config from core.entities.model_entities import DefaultModelEntity, DefaultModelProviderEntity @@ -393,19 +395,13 @@ class ProviderManager: @staticmethod 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) - for provider in providers: - # TODO: Use provider name with prefix after the data migration - provider_name_to_provider_records_dict[str(ModelProviderID(provider.provider_name))].append(provider) - + with Session(db.engine, expire_on_commit=False) as session: + stmt = select(Provider).where(Provider.tenant_id == tenant_id, Provider.is_valid == True) + 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 @staticmethod @@ -416,17 +412,12 @@ class ProviderManager: :param tenant_id: workspace id :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) - for provider_model in provider_models: - provider_name_to_provider_model_records_dict[provider_model.provider_name].append(provider_model) - + with Session(db.engine, expire_on_commit=False) as session: + 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 @staticmethod @@ -437,17 +428,14 @@ class ProviderManager: :param tenant_id: workspace id :return: """ - preferred_provider_types = ( - db.session.query(TenantPreferredModelProvider) - .filter(TenantPreferredModelProvider.tenant_id == tenant_id) - .all() - ) - - provider_name_to_preferred_provider_type_records_dict = { - preferred_provider_type.provider_name: preferred_provider_type - for preferred_provider_type in preferred_provider_types - } - + provider_name_to_preferred_provider_type_records_dict = {} + with Session(db.engine, expire_on_commit=False) as session: + stmt = select(TenantPreferredModelProvider).where(TenantPreferredModelProvider.tenant_id == tenant_id) + preferred_provider_types = session.scalars(stmt) + provider_name_to_preferred_provider_type_records_dict = { + 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 @staticmethod @@ -458,18 +446,14 @@ class ProviderManager: :param tenant_id: workspace id :return: """ - provider_model_settings = ( - db.session.query(ProviderModelSetting).filter(ProviderModelSetting.tenant_id == tenant_id).all() - ) - 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_model_setting ) - ) - return provider_name_to_provider_model_settings_dict @staticmethod @@ -492,15 +476,14 @@ class ProviderManager: if not model_load_balancing_enabled: 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) - 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) + with Session(db.engine, expire_on_commit=False) as session: + stmt = select(LoadBalancingModelConfig).where(LoadBalancingModelConfig.tenant_id == tenant_id) + provider_load_balancing_configs = session.scalars(stmt) + 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 @@ -626,10 +609,9 @@ class ProviderManager: if not cached_provider_credentials: try: # fix origin data - if ( - custom_provider_record.encrypted_config - and not custom_provider_record.encrypted_config.startswith("{") - ): + if custom_provider_record.encrypted_config is None: + raise ValueError("No credentials found") + if not custom_provider_record.encrypted_config.startswith("{"): provider_credentials = {"openai_api_key": custom_provider_record.encrypted_config} else: provider_credentials = json.loads(custom_provider_record.encrypted_config) @@ -733,7 +715,7 @@ class ProviderManager: return SystemConfiguration(enabled=False) # 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: if provider_record.provider_type != ProviderType.SYSTEM.value: continue @@ -758,6 +740,11 @@ class ProviderManager: else: 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_type=provider_quota.quota_type, quota_unit=provider_hosting_configuration.quota_unit or QuotaUnit.TOKENS, @@ -791,10 +778,9 @@ class ProviderManager: cached_provider_credentials = provider_credentials_cache.get() if not cached_provider_credentials: - try: - provider_credentials: dict[str, Any] = json.loads(provider_record.encrypted_config) - except JSONDecodeError: - provider_credentials = {} + provider_credentials: dict[str, Any] = {} + if provider_records and provider_records[0].encrypted_config: + provider_credentials = json.loads(provider_records[0].encrypted_config) # Get provider credential secret variables provider_credential_secret_variables = self._extract_secret_variables( diff --git a/api/core/rag/datasource/keyword/jieba/stopwords.py b/api/core/rag/datasource/keyword/jieba/stopwords.py index 9abe78d6ef..54b65d9a2d 100644 --- a/api/core/rag/datasource/keyword/jieba/stopwords.py +++ b/api/core/rag/datasource/keyword/jieba/stopwords.py @@ -720,7 +720,7 @@ STOPWORDS = { "〉", "〈", "…", - " ", + " ", "0", "1", "2", @@ -731,16 +731,6 @@ STOPWORDS = { "7", "8", "9", - "0", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", "二", "三", "四", diff --git a/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py b/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py index 6991598ce6..0abb3c0077 100644 --- a/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py +++ b/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py @@ -184,7 +184,16 @@ class OpenSearchVector(BaseVector): } document_ids_filter = kwargs.get("document_ids_filter") if document_ids_filter: - query["query"] = {"terms": {"metadata.document_id": document_ids_filter}} + query["query"] = { + "script_score": { + "query": {"bool": {"filter": [{"terms": {Field.DOCUMENT_ID.value: document_ids_filter}}]}}, + "script": { + "source": "knn_score", + "lang": "knn", + "params": {"field": Field.VECTOR.value, "query_value": query_vector, "space_type": "l2"}, + }, + } + } try: response = self._client.search(index=self._collection_name.lower(), body=query) @@ -209,10 +218,10 @@ class OpenSearchVector(BaseVector): return docs def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]: - full_text_query = {"query": {"match": {Field.CONTENT_KEY.value: query}}} + full_text_query = {"query": {"bool": {"must": [{"match": {Field.CONTENT_KEY.value: query}}]}}} document_ids_filter = kwargs.get("document_ids_filter") if document_ids_filter: - full_text_query["query"]["terms"] = {"metadata.document_id": document_ids_filter} + full_text_query["query"]["bool"]["filter"] = [{"terms": {"metadata.document_id": document_ids_filter}}] response = self._client.search(index=self._collection_name.lower(), body=full_text_query) @@ -255,7 +264,8 @@ class OpenSearchVector(BaseVector): Field.METADATA_KEY.value: { "type": "object", "properties": { - "doc_id": {"type": "keyword"} # Map doc_id to keyword type + "doc_id": {"type": "keyword"}, # Map doc_id to keyword type + "document_id": {"type": "keyword"}, }, }, } diff --git a/api/core/rag/datasource/vdb/oracle/oraclevector.py b/api/core/rag/datasource/vdb/oracle/oraclevector.py index 0a3738ac93..d1c8142b3d 100644 --- a/api/core/rag/datasource/vdb/oracle/oraclevector.py +++ b/api/core/rag/datasource/vdb/oracle/oraclevector.py @@ -261,7 +261,7 @@ class OracleVector(BaseVector): words = pseg.cut(query) current_entity = "" 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 else: if current_entity: @@ -303,7 +303,6 @@ class OracleVector(BaseVector): return docs else: return [Document(page_content="", metadata={})] - return [] def delete(self) -> None: with self._get_connection() as conn: diff --git a/api/core/tools/builtin_tool/_position.yaml b/api/core/tools/builtin_tool/_position.yaml index b5875e2075..0e811de311 100644 --- a/api/core/tools/builtin_tool/_position.yaml +++ b/api/core/tools/builtin_tool/_position.yaml @@ -1,3 +1,4 @@ +- audio - code - time -- qrcode +- webscraper diff --git a/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py b/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py index 93d3fcc49d..2cbc4b9821 100644 --- a/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py +++ b/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py @@ -153,8 +153,6 @@ class DatasetMultiRetrieverTool(DatasetRetrieverBaseTool): return str("\n".join(document_context_list)) return "" - raise RuntimeError("not segments found") - def _retriever( self, flask_app: Flask, diff --git a/api/core/workflow/nodes/document_extractor/node.py b/api/core/workflow/nodes/document_extractor/node.py index d39eb9c932..429fed2d04 100644 --- a/api/core/workflow/nodes/document_extractor/node.py +++ b/api/core/workflow/nodes/document_extractor/node.py @@ -397,19 +397,44 @@ def _extract_text_from_csv(file_content: bytes) -> str: if not rows: return "" - # Create Markdown table - markdown_table = "| " + " | ".join(rows[0]) + " |\n" - markdown_table += "| " + " | ".join(["---"] * len(rows[0])) + " |\n" - for row in rows[1:]: - markdown_table += "| " + " | ".join(row) + " |\n" + # Combine multi-line text in the header row + header_row = [cell.replace("\n", " ").replace("\r", "") for cell in rows[0]] - return markdown_table.strip() + # Create Markdown table + markdown_table = "| " + " | ".join(header_row) + " |\n" + markdown_table += "| " + " | ".join(["-" * len(col) for col in rows[0]]) + " |\n" + + # Process each data row and combine multi-line text in each cell + for row in rows[1:]: + processed_row = [cell.replace("\n", " ").replace("\r", "") for cell in row] + markdown_table += "| " + " | ".join(processed_row) + " |\n" + + return markdown_table except Exception as e: raise TextExtractionError(f"Failed to extract text from CSV: {str(e)}") from e def _extract_text_from_excel(file_content: bytes) -> str: """Extract text from an Excel file using pandas.""" + + def _construct_markdown_table(df: pd.DataFrame) -> str: + """Manually construct a Markdown table from a DataFrame.""" + # Construct the header row + header_row = "| " + " | ".join(df.columns) + " |" + + # Construct the separator row + separator_row = "| " + " | ".join(["-" * len(col) for col in df.columns]) + " |" + + # Construct the data rows + data_rows = [] + for _, row in df.iterrows(): + data_row = "| " + " | ".join(map(str, row)) + " |" + data_rows.append(data_row) + + # Combine all rows into a single string + markdown_table = "\n".join([header_row, separator_row] + data_rows) + return markdown_table + try: excel_file = pd.ExcelFile(io.BytesIO(file_content)) markdown_table = "" @@ -417,8 +442,15 @@ def _extract_text_from_excel(file_content: bytes) -> str: try: df = excel_file.parse(sheet_name=sheet_name) df.dropna(how="all", inplace=True) - # Create Markdown table two times to separate tables with a newline - markdown_table += df.to_markdown(index=False, floatfmt="") + "\n\n" + + # Combine multi-line text in each cell into a single line + df = df.applymap(lambda x: " ".join(str(x).splitlines()) if isinstance(x, str) else x) # type: ignore + + # Combine multi-line text in column names into a single line + df.columns = pd.Index([" ".join(col.splitlines()) for col in df.columns]) + + # Manually construct the Markdown table + markdown_table += _construct_markdown_table(df) + "\n\n" except Exception as e: continue return markdown_table diff --git a/api/core/workflow/nodes/http_request/executor.py b/api/core/workflow/nodes/http_request/executor.py index e28ac6343b..2c83b00d4a 100644 --- a/api/core/workflow/nodes/http_request/executor.py +++ b/api/core/workflow/nodes/http_request/executor.py @@ -1,8 +1,9 @@ import base64 import json +import secrets +import string from collections.abc import Mapping from copy import deepcopy -from random import randint from typing import Any, Literal from urllib.parse import urlencode, urlparse @@ -434,4 +435,4 @@ def _generate_random_string(n: int) -> str: >>> _generate_random_string(5) 'abcde' """ - return "".join([chr(randint(97, 122)) for _ in range(n)]) + return "".join(secrets.choice(string.ascii_lowercase) for _ in range(n)) diff --git a/api/core/workflow/nodes/llm/entities.py b/api/core/workflow/nodes/llm/entities.py index 486b4b01af..36d0688807 100644 --- a/api/core/workflow/nodes/llm/entities.py +++ b/api/core/workflow/nodes/llm/entities.py @@ -66,7 +66,8 @@ class LLMNodeData(BaseNodeData): context: ContextConfig vision: VisionConfig = Field(default_factory=VisionConfig) 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") @classmethod @@ -74,3 +75,7 @@ class LLMNodeData(BaseNodeData): if v is None: return PromptConfig() return v + + @property + def structured_output_enabled(self) -> bool: + return self.structured_output_switch_on and self.structured_output is not None diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index df8f614db3..ee181cf3bf 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -12,9 +12,7 @@ from sqlalchemy.orm import Session from configs import dify_config 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.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from core.file import FileType, file_manager from core.helper.code_executor import CodeExecutor, CodeLanguage 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 ( ResponseFormat, SpecialModelType, - SupportStructuredOutputStatus, ) from core.workflow.utils.structured_output.prompt import STRUCTURED_OUTPUT_PROMPT from core.workflow.utils.variable_template_parser import VariableTemplateParser @@ -277,7 +274,7 @@ class LLMNode(BaseNode[LLMNodeData]): llm_usage=usage, ) ) - except LLMNodeError as e: + except ValueError as e: yield RunCompletedEvent( run_result=NodeRunResult( status=WorkflowNodeExecutionStatus.FAILED, @@ -527,65 +524,53 @@ class LLMNode(BaseNode[LLMNodeData]): def _fetch_model_config( self, node_data_model: ModelConfig ) -> tuple[ModelInstance, ModelConfigWithCredentialsEntity]: - model_name = node_data_model.name - provider_name = node_data_model.provider + if not node_data_model.mode: + raise LLMModeRequiredError("LLM mode is required.") - model_manager = ModelManager() - model_instance = model_manager.get_model_instance( - tenant_id=self.tenant_id, model_type=ModelType.LLM, provider=provider_name, model=model_name + model = ModelManager().get_model_instance( + tenant_id=self.tenant_id, + model_type=ModelType.LLM, + provider=node_data_model.provider, + model=node_data_model.name, ) - provider_model_bundle = model_instance.provider_model_bundle - model_type_instance = model_instance.model_type_instance - model_type_instance = cast(LargeLanguageModel, model_type_instance) - - model_credentials = model_instance.credentials + model.model_type_instance = cast(LargeLanguageModel, model.model_type_instance) # check model - provider_model = provider_model_bundle.configuration.get_provider_model( - model=model_name, model_type=ModelType.LLM + provider_model = model.provider_model_bundle.configuration.get_provider_model( + model=node_data_model.name, model_type=ModelType.LLM ) if provider_model is None: - raise ModelNotExistError(f"Model {model_name} not exist.") - - 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.") + raise ModelNotExistError(f"Model {node_data_model.name} not exist.") + provider_model.raise_for_status() # model config - completion_params = node_data_model.completion_params - stop = [] - if "stop" in completion_params: - 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) + stop: list[str] = [] + if "stop" in node_data_model.completion_params: + stop = node_data_model.completion_params.pop("stop") + model_schema = model.model_type_instance.get_model_schema(node_data_model.name, model.credentials) if not model_schema: - raise ModelNotExistError(f"Model {model_name} not exist.") - support_structured_output = self._check_model_structured_output_support() - if support_structured_output == SupportStructuredOutputStatus.SUPPORTED: - completion_params = self._handle_native_json_schema(completion_params, model_schema.parameter_rules) - elif support_structured_output == SupportStructuredOutputStatus.UNSUPPORTED: - # Set appropriate response format based on model capabilities - self._set_response_format(completion_params, model_schema.parameter_rules) - return model_instance, ModelConfigWithCredentialsEntity( - provider=provider_name, - model=model_name, + raise ModelNotExistError(f"Model {node_data_model.name} not exist.") + + if self.node_data.structured_output_enabled: + if model_schema.support_structure_output: + node_data_model.completion_params = self._handle_native_json_schema( + node_data_model.completion_params, model_schema.parameter_rules + ) + else: + # Set appropriate response format based on model capabilities + 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, - mode=model_mode, - provider_model_bundle=provider_model_bundle, - credentials=model_credentials, - parameters=completion_params, + mode=node_data_model.mode, + provider_model_bundle=model.provider_model_bundle, + credentials=model.credentials, + parameters=node_data_model.completion_params, stop=stop, ) @@ -786,13 +771,25 @@ class LLMNode(BaseNode[LLMNodeData]): "No prompt found in the LLM configuration. " "Please ensure a prompt is properly configured before proceeding." ) - support_structured_output = self._check_model_structured_output_support() - if support_structured_output == SupportStructuredOutputStatus.UNSUPPORTED: - filtered_prompt_messages = self._handle_prompt_based_schema( - prompt_messages=filtered_prompt_messages, - ) - stop = model_config.stop - return filtered_prompt_messages, stop + + model = ModelManager().get_model_instance( + tenant_id=self.tenant_id, + model_type=ModelType.LLM, + provider=self.node_data.model.provider, + model=self.node_data.model.name, + ) + 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]: structured_output: dict[str, Any] = {} @@ -903,7 +900,7 @@ class LLMNode(BaseNode[LLMNodeData]): variable_mapping["#context#"] = node_data.context.variable_selector 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: variable_mapping["#sys.query#"] = ["sys", SystemVariableKey.QUERY.value] @@ -1185,32 +1182,6 @@ class LLMNode(BaseNode[LLMNodeData]): except json.JSONDecodeError: 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( self, contents: str | list[PromptMessageContentUnionTypes] | None, diff --git a/api/core/workflow/utils/structured_output/entities.py b/api/core/workflow/utils/structured_output/entities.py index 7954acbaee..6491042bfe 100644 --- a/api/core/workflow/utils/structured_output/entities.py +++ b/api/core/workflow/utils/structured_output/entities.py @@ -14,11 +14,3 @@ class SpecialModelType(StrEnum): GEMINI = "gemini" OLLAMA = "ollama" - - -class SupportStructuredOutputStatus(StrEnum): - """Constants for structured output support status""" - - SUPPORTED = "supported" - UNSUPPORTED = "unsupported" - DISABLED = "disabled" diff --git a/api/extensions/ext_celery.py b/api/extensions/ext_celery.py index 26bd6b3577..a837552007 100644 --- a/api/extensions/ext_celery.py +++ b/api/extensions/ext_celery.py @@ -70,6 +70,7 @@ def init_app(app: DifyApp) -> Celery: "schedule.update_tidb_serverless_status_task", "schedule.clean_messages", "schedule.mail_clean_document_notify_task", + "schedule.queue_monitor_task", ] day = dify_config.CELERY_BEAT_SCHEDULER_TIME beat_schedule = { @@ -98,6 +99,12 @@ def init_app(app: DifyApp) -> Celery: "task": "schedule.mail_clean_document_notify_task.mail_clean_document_notify_task", "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) diff --git a/api/libs/helper.py b/api/libs/helper.py index afc8f31681..e78a782fbe 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -1,7 +1,7 @@ import json import logging -import random import re +import secrets import string import subprocess import time @@ -18,6 +18,7 @@ from flask_restful import fields from configs import dify_config from core.app.features.rate_limiting.rate_limit import RateLimitGenerator from core.file import helpers as file_helpers +from core.model_runtime.utils.encoders import jsonable_encoder from extensions.ext_redis import redis_client if TYPE_CHECKING: @@ -175,7 +176,7 @@ def generate_string(n): letters_digits = string.ascii_letters + string.digits result = "" for i in range(n): - result += random.choice(letters_digits) + result += secrets.choice(letters_digits) return result @@ -196,7 +197,7 @@ def generate_text_hash(text: str) -> str: def compact_generate_response(response: Union[Mapping, Generator, RateLimitGenerator]) -> Response: 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: def generate() -> Generator: diff --git a/api/migrations/versions/2025_06_06_1424-4474872b0ee6_workflow_draft_varaibles_add_node_execution_id.py b/api/migrations/versions/2025_06_06_1424-4474872b0ee6_workflow_draft_varaibles_add_node_execution_id.py new file mode 100644 index 0000000000..d7a5d116c9 --- /dev/null +++ b/api/migrations/versions/2025_06_06_1424-4474872b0ee6_workflow_draft_varaibles_add_node_execution_id.py @@ -0,0 +1,60 @@ +"""`workflow_draft_varaibles` add `node_execution_id` column, add an index for `workflow_node_executions`. + +Revision ID: 4474872b0ee6 +Revises: 2adcbe1f5dfb +Create Date: 2025-06-06 14:24:44.213018 + +""" +from alembic import op +import models as models +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '4474872b0ee6' +down_revision = '2adcbe1f5dfb' +branch_labels = None +depends_on = None + + +def upgrade(): + # `CREATE INDEX CONCURRENTLY` cannot run within a transaction, so use the `autocommit_block` + # context manager to wrap the index creation statement. + # Reference: + # + # - https://www.postgresql.org/docs/current/sql-createindex.html#:~:text=Another%20difference%20is,CREATE%20INDEX%20CONCURRENTLY%20cannot. + # - https://alembic.sqlalchemy.org/en/latest/api/runtime.html#alembic.runtime.migration.MigrationContext.autocommit_block + with op.get_context().autocommit_block(): + op.create_index( + op.f('workflow_node_executions_tenant_id_idx'), + "workflow_node_executions", + ['tenant_id', 'workflow_id', 'node_id', sa.literal_column('created_at DESC')], + unique=False, + postgresql_concurrently=True, + ) + + with op.batch_alter_table('workflow_draft_variables', schema=None) as batch_op: + batch_op.add_column(sa.Column('node_execution_id', models.types.StringUUID(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + + # `DROP INDEX CONCURRENTLY` cannot run within a transaction, so use the `autocommit_block` + # context manager to wrap the index creation statement. + # Reference: + # + # - https://www.postgresql.org/docs/current/sql-createindex.html#:~:text=Another%20difference%20is,CREATE%20INDEX%20CONCURRENTLY%20cannot. + # - https://alembic.sqlalchemy.org/en/latest/api/runtime.html#alembic.runtime.migration.MigrationContext.autocommit_block + # `DROP INDEX CONCURRENTLY` cannot run within a transaction, so commit existing transactions first. + # Reference: + # + # https://www.postgresql.org/docs/current/sql-createindex.html#:~:text=Another%20difference%20is,CREATE%20INDEX%20CONCURRENTLY%20cannot. + with op.get_context().autocommit_block(): + op.drop_index(op.f('workflow_node_executions_tenant_id_idx'), postgresql_concurrently=True) + + with op.batch_alter_table('workflow_draft_variables', schema=None) as batch_op: + batch_op.drop_column('node_execution_id') + + # ### end Alembic commands ### diff --git a/api/models/provider.py b/api/models/provider.py index 497cbefc61..1e25f0c90f 100644 --- a/api/models/provider.py +++ b/api/models/provider.py @@ -1,6 +1,9 @@ +from datetime import datetime 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 .engine import db @@ -51,20 +54,24 @@ class Provider(Base): ), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - provider_name = db.Column(db.String(255), nullable=False) - provider_type = db.Column(db.String(40), nullable=False, server_default=db.text("'custom'::character varying")) - encrypted_config = db.Column(db.Text, nullable=True) - is_valid = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) - last_used = db.Column(db.DateTime, nullable=True) + id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()")) + tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False) + provider_type: Mapped[str] = mapped_column( + db.String(40), nullable=False, server_default=text("'custom'::character varying") + ) + 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_limit = db.Column(db.BigInteger, nullable=True) - quota_used = db.Column(db.BigInteger, default=0) + quota_type: Mapped[Optional[str]] = mapped_column( + db.String(40), nullable=True, server_default=text("''::character varying") + ) + 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()) - updated_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: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) def __repr__(self): return ( @@ -104,15 +111,15 @@ class ProviderModel(Base): ), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - provider_name = db.Column(db.String(255), nullable=False) - model_name = db.Column(db.String(255), nullable=False) - model_type = db.Column(db.String(40), nullable=False) - encrypted_config = db.Column(db.Text, nullable=True) - is_valid = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()")) + tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False) + model_name: Mapped[str] = mapped_column(db.String(255), nullable=False) + model_type: Mapped[str] = mapped_column(db.String(40), nullable=False) + 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")) + 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 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"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - provider_name = db.Column(db.String(255), nullable=False) - model_name = db.Column(db.String(255), nullable=False) - model_type = db.Column(db.String(40), nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()")) + tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False) + model_name: Mapped[str] = mapped_column(db.String(255), nullable=False) + model_type: Mapped[str] = mapped_column(db.String(40), nullable=False) + 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 TenantPreferredModelProvider(Base): @@ -138,12 +145,12 @@ class TenantPreferredModelProvider(Base): db.Index("tenant_preferred_model_provider_tenant_provider_idx", "tenant_id", "provider_name"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - provider_name = db.Column(db.String(255), nullable=False) - preferred_provider_type = db.Column(db.String(40), nullable=False) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()")) + tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False) + preferred_provider_type: Mapped[str] = mapped_column(db.String(40), nullable=False) + 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 ProviderOrder(Base): @@ -153,22 +160,24 @@ class ProviderOrder(Base): db.Index("provider_order_tenant_provider_idx", "tenant_id", "provider_name"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - provider_name = db.Column(db.String(255), nullable=False) - account_id = db.Column(StringUUID, nullable=False) - payment_product_id = db.Column(db.String(191), nullable=False) - payment_id = db.Column(db.String(191)) - transaction_id = db.Column(db.String(191)) - quantity = db.Column(db.Integer, nullable=False, server_default=db.text("1")) - currency = db.Column(db.String(40)) - total_amount = db.Column(db.Integer) - payment_status = db.Column(db.String(40), nullable=False, server_default=db.text("'wait_pay'::character varying")) - paid_at = db.Column(db.DateTime) - pay_failed_at = db.Column(db.DateTime) - refunded_at = db.Column(db.DateTime) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()")) + tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False) + account_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + payment_product_id: Mapped[str] = mapped_column(db.String(191), nullable=False) + payment_id: Mapped[Optional[str]] = mapped_column(db.String(191)) + transaction_id: Mapped[Optional[str]] = mapped_column(db.String(191)) + quantity: Mapped[int] = mapped_column(db.Integer, nullable=False, server_default=text("1")) + currency: Mapped[Optional[str]] = mapped_column(db.String(40)) + total_amount: Mapped[Optional[int]] = mapped_column(db.Integer) + payment_status: Mapped[str] = mapped_column( + db.String(40), nullable=False, server_default=text("'wait_pay'::character varying") + ) + paid_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime) + pay_failed_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime) + 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): @@ -182,15 +191,15 @@ class ProviderModelSetting(Base): 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()")) - tenant_id = db.Column(StringUUID, nullable=False) - provider_name = db.Column(db.String(255), nullable=False) - model_name = db.Column(db.String(255), nullable=False) - model_type = db.Column(db.String(40), nullable=False) - enabled = db.Column(db.Boolean, nullable=False, server_default=db.text("true")) - load_balancing_enabled = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()")) + tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False) + model_name: Mapped[str] = mapped_column(db.String(255), nullable=False) + model_type: Mapped[str] = mapped_column(db.String(40), nullable=False) + enabled: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=text("true")) + load_balancing_enabled: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=text("false")) + 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 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"), ) - id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) - tenant_id = db.Column(StringUUID, nullable=False) - provider_name = db.Column(db.String(255), nullable=False) - model_name = db.Column(db.String(255), nullable=False) - model_type = db.Column(db.String(40), nullable=False) - name = db.Column(db.String(255), nullable=False) - encrypted_config = db.Column(db.Text, nullable=True) - enabled = db.Column(db.Boolean, nullable=False, server_default=db.text("true")) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()")) + tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False) + model_name: Mapped[str] = mapped_column(db.String(255), nullable=False) + model_type: Mapped[str] = mapped_column(db.String(40), nullable=False) + name: Mapped[str] = mapped_column(db.String(255), nullable=False) + encrypted_config: Mapped[Optional[str]] = mapped_column(db.Text, nullable=True) + enabled: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=text("true")) + 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()) diff --git a/api/models/workflow.py b/api/models/workflow.py index e868fb77a7..2fff045543 100644 --- a/api/models/workflow.py +++ b/api/models/workflow.py @@ -16,8 +16,8 @@ if TYPE_CHECKING: from models.model import AppMode import sqlalchemy as sa -from sqlalchemy import UniqueConstraint, func -from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import Index, PrimaryKeyConstraint, UniqueConstraint, func +from sqlalchemy.orm import Mapped, declared_attr, mapped_column from constants import DEFAULT_FILE_NUMBER_LIMITS, HIDDEN_VALUE from core.helper import encrypter @@ -590,28 +590,48 @@ class WorkflowNodeExecutionModel(Base): """ __tablename__ = "workflow_node_executions" - __table_args__ = ( - db.PrimaryKeyConstraint("id", name="workflow_node_execution_pkey"), - db.Index( - "workflow_node_execution_workflow_run_idx", - "tenant_id", - "app_id", - "workflow_id", - "triggered_from", - "workflow_run_id", - ), - db.Index( - "workflow_node_execution_node_run_idx", "tenant_id", "app_id", "workflow_id", "triggered_from", "node_id" - ), - db.Index( - "workflow_node_execution_id_idx", - "tenant_id", - "app_id", - "workflow_id", - "triggered_from", - "node_execution_id", - ), - ) + + @declared_attr + def __table_args__(cls): # noqa + return ( + PrimaryKeyConstraint("id", name="workflow_node_execution_pkey"), + Index( + "workflow_node_execution_workflow_run_idx", + "tenant_id", + "app_id", + "workflow_id", + "triggered_from", + "workflow_run_id", + ), + Index( + "workflow_node_execution_node_run_idx", + "tenant_id", + "app_id", + "workflow_id", + "triggered_from", + "node_id", + ), + Index( + "workflow_node_execution_id_idx", + "tenant_id", + "app_id", + "workflow_id", + "triggered_from", + "node_execution_id", + ), + Index( + # The first argument is the index name, + # which we leave as `None`` to allow auto-generation by the ORM. + None, + cls.tenant_id, + cls.workflow_id, + cls.node_id, + # MyPy may flag the following line because it doesn't recognize that + # the `declared_attr` decorator passes the receiving class as the first + # argument to this method, allowing us to reference class attributes. + cls.created_at.desc(), # type: ignore + ), + ) id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) tenant_id: Mapped[str] = mapped_column(StringUUID) @@ -885,14 +905,29 @@ class WorkflowDraftVariable(Base): selector: Mapped[str] = mapped_column(sa.String(255), nullable=False, name="selector") + # The data type of this variable's value value_type: Mapped[SegmentType] = mapped_column(EnumText(SegmentType, length=20)) - # JSON string + + # The variable's value serialized as a JSON string value: Mapped[str] = mapped_column(sa.Text, nullable=False, name="value") - # visible + # Controls whether the variable should be displayed in the variable inspection panel visible: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=True) + + # Determines whether this variable can be modified by users editable: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=False) + # The `node_execution_id` field identifies the workflow node execution that created this variable. + # It corresponds to the `id` field in the `WorkflowNodeExecutionModel` model. + # + # This field is not `None` for system variables and node variables, and is `None` + # for conversation variables. + node_execution_id: Mapped[str | None] = mapped_column( + StringUUID, + nullable=True, + default=None, + ) + def get_selector(self) -> list[str]: selector = json.loads(self.selector) if not isinstance(selector, list): diff --git a/api/schedule/queue_monitor_task.py b/api/schedule/queue_monitor_task.py new file mode 100644 index 0000000000..e3a7021b9d --- /dev/null +++ b/api/schedule/queue_monitor_task.py @@ -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() diff --git a/api/services/account_service.py b/api/services/account_service.py index ac84a46299..14d238467d 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -1,7 +1,6 @@ import base64 import json import logging -import random import secrets import uuid from datetime import UTC, datetime, timedelta @@ -261,7 +260,7 @@ class AccountService: @staticmethod def generate_account_deletion_verification_code(account: Account) -> tuple[str, str]: - code = "".join([str(random.randint(0, 9)) for _ in range(6)]) + code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)]) token = TokenManager.generate_token( account=account, token_type="account_deletion", additional_data={"code": code} ) @@ -429,7 +428,7 @@ class AccountService: additional_data: dict[str, Any] = {}, ): if not code: - code = "".join([str(random.randint(0, 9)) for _ in range(6)]) + code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)]) additional_data["code"] = code token = TokenManager.generate_token( account=account, email=email, token_type="reset_password", additional_data=additional_data @@ -456,7 +455,7 @@ class AccountService: raise EmailCodeLoginRateLimitExceededError() - code = "".join([str(random.randint(0, 9)) for _ in range(6)]) + code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)]) token = TokenManager.generate_token( account=account, email=email, token_type="email_code_login", additional_data={"code": code} ) diff --git a/api/services/dataset_service.py b/api/services/dataset_service.py index 4a5e9b3520..e98b47921f 100644 --- a/api/services/dataset_service.py +++ b/api/services/dataset_service.py @@ -2,7 +2,7 @@ import copy import datetime import json import logging -import random +import secrets import time import uuid from collections import Counter @@ -970,7 +970,7 @@ class DocumentService: documents.append(document) batch = document.batch else: - batch = time.strftime("%Y%m%d%H%M%S") + str(random.randint(100000, 999999)) + batch = time.strftime("%Y%m%d%H%M%S") + str(100000 + secrets.randbelow(exclusive_upper_bound=900000)) # save process rule if not dataset_process_rule: process_rule = knowledge_config.process_rule diff --git a/api/services/tag_service.py b/api/services/tag_service.py index be748e8dd1..74c6150b44 100644 --- a/api/services/tag_service.py +++ b/api/services/tag_service.py @@ -46,6 +46,8 @@ class TagService: @staticmethod 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 = ( db.session.query(Tag) .filter(Tag.name == tag_name, Tag.tenant_id == current_tenant_id, Tag.type == tag_type) @@ -88,7 +90,7 @@ class TagService: @staticmethod 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") tag = db.session.query(Tag).filter(Tag.id == tag_id).first() if not tag: diff --git a/api/services/webapp_auth_service.py b/api/services/webapp_auth_service.py index b320a7bf66..8f92b3f070 100644 --- a/api/services/webapp_auth_service.py +++ b/api/services/webapp_auth_service.py @@ -1,5 +1,5 @@ import enum -import random +import secrets from datetime import UTC, datetime, timedelta from typing import Any, Optional, cast @@ -69,7 +69,7 @@ class WebAppAuthService: if email is None: raise ValueError("Email must be provided.") - code = "".join([str(random.randint(0, 9)) for _ in range(6)]) + code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)]) token = TokenManager.generate_token( account=account, email=email, token_type="email_code_login", additional_data={"code": code} ) diff --git a/api/tasks/batch_create_segment_to_index_task.py b/api/tasks/batch_create_segment_to_index_task.py index f32bc4f187..51b6343fdc 100644 --- a/api/tasks/batch_create_segment_to_index_task.py +++ b/api/tasks/batch_create_segment_to_index_task.py @@ -5,7 +5,7 @@ import uuid import click from celery import shared_task # type: ignore -from sqlalchemy import func, select +from sqlalchemy import func from sqlalchemy.orm import Session from core.model_manager import ModelManager @@ -68,11 +68,6 @@ def batch_create_segment_to_index_task( model_type=ModelType.TEXT_EMBEDDING, 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 if embedding_model: tokens_list = embedding_model.get_text_embedding_num_tokens( diff --git a/api/templates/queue_monitor_alert_email_template_en-US.html b/api/templates/queue_monitor_alert_email_template_en-US.html new file mode 100644 index 0000000000..2885210864 --- /dev/null +++ b/api/templates/queue_monitor_alert_email_template_en-US.html @@ -0,0 +1,129 @@ + + + + + + + + +
+
+ Dify Logo +
+

Queue Monitoring Alert

+

Our system has detected an abnormal queue status that requires your attention:

+ +
+
Queue Task Alert
+
+ Queue "{{queue_name}}" has {{queue_length}} pending tasks (Threshold: {{threshold}}) +
+
+ +
+

Recommended actions:

+

1. Check the queue processing status in the system dashboard

+

2. Verify if there are any processing bottlenecks

+

3. Consider scaling up workers if needed

+
+ +

Additional Information:

+ +
+ + + diff --git a/api/tests/integration_tests/workflow/nodes/test_llm.py b/api/tests/integration_tests/workflow/nodes/test_llm.py index 5fbee266bd..6aa48b1cbb 100644 --- a/api/tests/integration_tests/workflow/nodes/test_llm.py +++ b/api/tests/integration_tests/workflow/nodes/test_llm.py @@ -3,11 +3,16 @@ import os import time import uuid from collections.abc import Generator -from unittest.mock import MagicMock +from decimal import Decimal +from unittest.mock import MagicMock, patch import pytest +from app_factory import create_app +from configs import dify_config 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.workflow_node_execution import WorkflowNodeExecutionStatus 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 models.enums import UserFrom 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""" 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 +@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: graph_config = { "edges": [ @@ -40,13 +59,19 @@ def init_llm_node(config: dict) -> LLMNode: 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( - tenant_id="1", - app_id="1", + tenant_id=tenant_id, + app_id=app_id, workflow_type=WorkflowType.WORKFLOW, - workflow_id="1", + workflow_id=workflow_id, graph_config=graph_config, - user_id="1", + user_id=user_id, user_from=UserFrom.ACCOUNT, invoke_from=InvokeFrom.DEBUGGER, call_depth=0, @@ -77,115 +102,197 @@ def init_llm_node(config: dict) -> LLMNode: return node -def test_execute_llm(setup_model_mock): - node = init_llm_node( - config={ - "id": "llm", - "data": { - "title": "123", - "type": "llm", - "model": { - "provider": "langgenius/openai/openai", - "name": "gpt-3.5-turbo", - "mode": "chat", - "completion_params": {}, +def test_execute_llm(app): + with app.app_context(): + node = init_llm_node( + config={ + "id": "llm", + "data": { + "title": "123", + "type": "llm", + "model": { + "provider": "langgenius/openai/openai", + "name": "gpt-3.5-turbo", + "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() - db.session.close = MagicMock() + # Create a proper LLM result with real entities + 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( - provider="langgenius/openai/openai", - model="gpt-3.5-turbo", - mode="chat", - credentials=credentials, - ) + mock_message = AssistantPromptMessage(content="This is a test response from the mocked LLM.") - # execute node - result = node._run() - assert isinstance(result, Generator) + mock_llm_result = LLMResult( + model="gpt-3.5-turbo", + prompt_messages=[], + message=mock_message, + usage=mock_usage, + ) - for item in result: - if isinstance(item, RunCompletedEvent): - assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED - assert item.run_result.process_data 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("usage", {})["total_tokens"] > 0 + # 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 + + with ( + 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() + assert isinstance(result, Generator) + + for item in result: + if isinstance(item, RunCompletedEvent): + assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED + assert item.run_result.process_data 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("usage", {})["total_tokens"] > 0 @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 """ - node = init_llm_node( - config={ - "id": "llm", - "data": { - "title": "123", - "type": "llm", - "model": {"provider": "openai", "name": "gpt-3.5-turbo", "mode": "chat", "completion_params": {}}, - "prompt_config": { - "jinja2_variables": [ - {"variable": "sys_query", "value_selector": ["sys", "query"]}, - {"variable": "output", "value_selector": ["abc", "output"]}, - ] + with app.app_context(): + node = init_llm_node( + config={ + "id": "llm", + "data": { + "title": "123", + "type": "llm", + "model": {"provider": "openai", "name": "gpt-3.5-turbo", "mode": "chat", "completion_params": {}}, + "prompt_config": { + "jinja2_variables": [ + {"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", + }, + { + "role": "user", + "text": "{{#sys.query#}}", + "jinja2_text": "{{sys_query}}", + "edition_type": "basic", + }, + ], + "memory": None, + "context": {"enabled": False}, + "vision": {"enabled": False}, }, - "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", - }, - { - "role": "user", - "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() - db.session.close = MagicMock() + # Create a proper LLM result with real entities + 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( - provider="langgenius/openai/openai", - model="gpt-3.5-turbo", - mode="chat", - credentials=credentials, - ) + mock_message = AssistantPromptMessage(content="Test response: sunny weather and what's the weather today?") - # execute node - result = node._run() + mock_llm_result = LLMResult( + model="gpt-3.5-turbo", + prompt_messages=[], + message=mock_message, + usage=mock_usage, + ) - for item in result: - if isinstance(item, RunCompletedEvent): - assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED - assert item.run_result.process_data is not None - assert "sunny" in json.dumps(item.run_result.process_data) - assert "what's the weather today?" in json.dumps(item.run_result.process_data) + # 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 + + with ( + 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: + if isinstance(item, RunCompletedEvent): + assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED + assert item.run_result.process_data is not None + assert "sunny" 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(): diff --git a/api/tests/unit_tests/core/helper/test_ssrf_proxy.py b/api/tests/unit_tests/core/helper/test_ssrf_proxy.py index c688d3952b..37749f0c66 100644 --- a/api/tests/unit_tests/core/helper/test_ssrf_proxy.py +++ b/api/tests/unit_tests/core/helper/test_ssrf_proxy.py @@ -1,4 +1,4 @@ -import random +import secrets from unittest.mock import MagicMock, patch import pytest @@ -34,7 +34,7 @@ def test_retry_logic_success(mock_request): side_effects = [] for _ in range(SSRF_DEFAULT_MAX_RETRIES): - status_code = random.choice(STATUS_FORCELIST) + status_code = secrets.choice(STATUS_FORCELIST) mock_response = MagicMock() mock_response.status_code = status_code side_effects.append(mock_response) diff --git a/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py b/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py index 35d83449c3..4cb1aa93f9 100644 --- a/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py @@ -1,5 +1,7 @@ +import io from unittest.mock import Mock, patch +import pandas as pd import pytest from docx.oxml.text.paragraph import CT_P @@ -187,145 +189,134 @@ def test_node_type(document_extractor_node): @patch("pandas.ExcelFile") def test_extract_text_from_excel_single_sheet(mock_excel_file): - """Test extracting text from Excel file with single sheet.""" - # Mock DataFrame - mock_df = Mock() - mock_df.dropna = Mock() - mock_df.to_markdown.return_value = "| Name | Age |\n|------|-----|\n| John | 25 |" + """Test extracting text from Excel file with single sheet and multiline content.""" + + # Test multi-line cell + data = {"Name\nwith\nnewline": ["John\nDoe", "Jane\nSmith"], "Age": [25, 30]} + + df = pd.DataFrame(data) # Mock ExcelFile mock_excel_instance = Mock() mock_excel_instance.sheet_names = ["Sheet1"] - mock_excel_instance.parse.return_value = mock_df + mock_excel_instance.parse.return_value = df mock_excel_file.return_value = mock_excel_instance file_content = b"fake_excel_content" result = _extract_text_from_excel(file_content) + expected_manual = "| Name with newline | Age |\n| ----------------- | --- |\n\ +| John Doe | 25 |\n| Jane Smith | 30 |\n\n" - expected = "| Name | Age |\n|------|-----|\n| John | 25 |\n\n" - assert result == expected - mock_excel_file.assert_called_once() - mock_df.dropna.assert_called_once_with(how="all", inplace=True) - mock_df.to_markdown.assert_called_once_with(index=False, floatfmt="") + assert expected_manual == result + mock_excel_instance.parse.assert_called_once_with(sheet_name="Sheet1") @patch("pandas.ExcelFile") def test_extract_text_from_excel_multiple_sheets(mock_excel_file): - """Test extracting text from Excel file with multiple sheets.""" - # Mock DataFrames for different sheets - mock_df1 = Mock() - mock_df1.dropna = Mock() - mock_df1.to_markdown.return_value = "| Product | Price |\n|---------|-------|\n| Apple | 1.50 |" + """Test extracting text from Excel file with multiple sheets and multiline content.""" - mock_df2 = Mock() - mock_df2.dropna = Mock() - mock_df2.to_markdown.return_value = "| City | Population |\n|------|------------|\n| NYC | 8000000 |" + # Test multi-line cell + data1 = {"Product\nName": ["Apple\nRed", "Banana\nYellow"], "Price": [1.50, 0.99]} + df1 = pd.DataFrame(data1) + + data2 = {"City\nName": ["New\nYork", "Los\nAngeles"], "Population": [8000000, 3900000]} + df2 = pd.DataFrame(data2) # Mock ExcelFile mock_excel_instance = Mock() mock_excel_instance.sheet_names = ["Products", "Cities"] - mock_excel_instance.parse.side_effect = [mock_df1, mock_df2] + mock_excel_instance.parse.side_effect = [df1, df2] mock_excel_file.return_value = mock_excel_instance file_content = b"fake_excel_content_multiple_sheets" result = _extract_text_from_excel(file_content) - expected = ( - "| Product | Price |\n|---------|-------|\n| Apple | 1.50 |\n\n" - "| City | Population |\n|------|------------|\n| NYC | 8000000 |\n\n" - ) - assert result == expected + expected_manual1 = "| Product Name | Price |\n| ------------ | ----- |\n\ +| Apple Red | 1.5 |\n| Banana Yellow | 0.99 |\n\n" + expected_manual2 = "| City Name | Population |\n| --------- | ---------- |\n\ +| New York | 8000000 |\n| Los Angeles | 3900000 |\n\n" + + assert expected_manual1 in result + assert expected_manual2 in result + assert mock_excel_instance.parse.call_count == 2 @patch("pandas.ExcelFile") def test_extract_text_from_excel_empty_sheets(mock_excel_file): """Test extracting text from Excel file with empty sheets.""" - # Mock empty DataFrame - mock_df = Mock() - mock_df.dropna = Mock() - mock_df.to_markdown.return_value = "" + + # Empty excel + df = pd.DataFrame() # Mock ExcelFile mock_excel_instance = Mock() mock_excel_instance.sheet_names = ["EmptySheet"] - mock_excel_instance.parse.return_value = mock_df + mock_excel_instance.parse.return_value = df mock_excel_file.return_value = mock_excel_instance file_content = b"fake_excel_empty_content" result = _extract_text_from_excel(file_content) - expected = "\n\n" + expected = "| |\n| |\n\n" assert result == expected + mock_excel_instance.parse.assert_called_once_with(sheet_name="EmptySheet") + @patch("pandas.ExcelFile") def test_extract_text_from_excel_sheet_parse_error(mock_excel_file): """Test handling of sheet parsing errors - should continue with other sheets.""" - # Mock DataFrames - one successful, one that raises exception - mock_df_success = Mock() - mock_df_success.dropna = Mock() - mock_df_success.to_markdown.return_value = "| Data | Value |\n|------|-------|\n| Test | 123 |" + + # Test error + data = {"Data": ["Test"], "Value": [123]} + df = pd.DataFrame(data) # Mock ExcelFile mock_excel_instance = Mock() mock_excel_instance.sheet_names = ["GoodSheet", "BadSheet"] - mock_excel_instance.parse.side_effect = [mock_df_success, Exception("Parse error")] + mock_excel_instance.parse.side_effect = [df, Exception("Parse error")] mock_excel_file.return_value = mock_excel_instance file_content = b"fake_excel_mixed_content" result = _extract_text_from_excel(file_content) - expected = "| Data | Value |\n|------|-------|\n| Test | 123 |\n\n" - assert result == expected + expected_manual = "| Data | Value |\n| ---- | ----- |\n| Test | 123 |\n\n" + assert expected_manual == result -@patch("pandas.ExcelFile") -def test_extract_text_from_excel_file_error(mock_excel_file): - """Test handling of Excel file reading errors.""" - mock_excel_file.side_effect = Exception("Invalid Excel file") - - file_content = b"invalid_excel_content" - - with pytest.raises(Exception) as exc_info: - _extract_text_from_excel(file_content) - - # Note: The function should raise TextExtractionError, but since it's not imported in the test, - # we check for the general Exception pattern - assert "Failed to extract text from Excel file" in str(exc_info.value) + assert mock_excel_instance.parse.call_count == 2 @patch("pandas.ExcelFile") def test_extract_text_from_excel_io_bytesio_usage(mock_excel_file): """Test that BytesIO is properly used with the file content.""" - import io - # Mock DataFrame - mock_df = Mock() - mock_df.dropna = Mock() - mock_df.to_markdown.return_value = "| Test | Data |\n|------|------|\n| 1 | A |" + # Test bytesio + data = {"Test": [1], "Data": ["A"]} + df = pd.DataFrame(data) # Mock ExcelFile mock_excel_instance = Mock() mock_excel_instance.sheet_names = ["TestSheet"] - mock_excel_instance.parse.return_value = mock_df + mock_excel_instance.parse.return_value = df mock_excel_file.return_value = mock_excel_instance file_content = b"test_excel_bytes" result = _extract_text_from_excel(file_content) - # Verify that ExcelFile was called with a BytesIO object mock_excel_file.assert_called_once() - call_args = mock_excel_file.call_args[0][0] - assert isinstance(call_args, io.BytesIO) + call_arg = mock_excel_file.call_args[0][0] + assert isinstance(call_arg, io.BytesIO) - expected = "| Test | Data |\n|------|------|\n| 1 | A |\n\n" - assert result == expected + expected_manual = "| Test | Data |\n| ---- | ---- |\n| 1 | A |\n\n" + assert expected_manual == result @patch("pandas.ExcelFile") def test_extract_text_from_excel_all_sheets_fail(mock_excel_file): """Test when all sheets fail to parse - should return empty string.""" + # Mock ExcelFile mock_excel_instance = Mock() mock_excel_instance.sheet_names = ["BadSheet1", "BadSheet2"] @@ -335,29 +326,6 @@ def test_extract_text_from_excel_all_sheets_fail(mock_excel_file): file_content = b"fake_excel_all_bad_sheets" result = _extract_text_from_excel(file_content) - # Should return empty string when all sheets fail assert result == "" - -@patch("pandas.ExcelFile") -def test_extract_text_from_excel_markdown_formatting(mock_excel_file): - """Test that markdown formatting parameters are correctly applied.""" - # Mock DataFrame - mock_df = Mock() - mock_df.dropna = Mock() - mock_df.to_markdown.return_value = "| Float | Int |\n|-------|-----|\n| 123456.78 | 42 |" - - # Mock ExcelFile - mock_excel_instance = Mock() - mock_excel_instance.sheet_names = ["NumberSheet"] - mock_excel_instance.parse.return_value = mock_df - mock_excel_file.return_value = mock_excel_instance - - file_content = b"fake_excel_numbers" - result = _extract_text_from_excel(file_content) - - # Verify to_markdown was called with correct parameters - mock_df.to_markdown.assert_called_once_with(index=False, floatfmt="") - - expected = "| Float | Int |\n|-------|-----|\n| 123456.78 | 42 |\n\n" - assert result == expected + assert mock_excel_instance.parse.call_count == 2 diff --git a/docker/.env.example b/docker/.env.example index ac9536be03..4cf5e202d0 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -1057,7 +1057,7 @@ PLUGIN_MAX_EXECUTION_TIMEOUT=600 PIP_MIRROR_URL= # 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_LOCAL_ROOT=/app/storage 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_AUTH_VERSION=v4 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 @@ -1106,3 +1111,10 @@ OTEL_METRIC_EXPORT_TIMEOUT=30000 # Prevent Clickjacking 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 diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index 74a7b87bf9..75bdab1a06 100644 --- a/docker/docker-compose-template.yaml +++ b/docker/docker-compose-template.yaml @@ -184,6 +184,10 @@ services: ALIYUN_OSS_ACCESS_KEY_SECRET: ${PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET:-} ALIYUN_OSS_AUTH_VERSION: ${PLUGIN_ALIYUN_OSS_AUTH_VERSION:-v4} 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: - "${EXPOSE_PLUGIN_DEBUGGING_PORT:-5003}:${PLUGIN_DEBUGGING_PORT:-5003}" volumes: diff --git a/docker/docker-compose.middleware.yaml b/docker/docker-compose.middleware.yaml index d4a0b94619..8276e2977f 100644 --- a/docker/docker-compose.middleware.yaml +++ b/docker/docker-compose.middleware.yaml @@ -121,6 +121,10 @@ services: ALIYUN_OSS_ACCESS_KEY_SECRET: ${PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET:-} ALIYUN_OSS_AUTH_VERSION: ${PLUGIN_ALIYUN_OSS_AUTH_VERSION:-v4} 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: - "${EXPOSE_PLUGIN_DAEMON_PORT:-5002}:${PLUGIN_DAEMON_PORT:-5002}" - "${EXPOSE_PLUGIN_DEBUGGING_PORT:-5003}:${PLUGIN_DEBUGGING_PORT:-5003}" diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 41e86d015f..e559021684 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -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_AUTH_VERSION: ${PLUGIN_ALIYUN_OSS_AUTH_VERSION:-v4} 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} OTLP_BASE_ENDPOINT: ${OTLP_BASE_ENDPOINT:-http://localhost:4318} 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_METRIC_EXPORT_TIMEOUT: ${OTEL_METRIC_EXPORT_TIMEOUT:-30000} 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: # API service @@ -683,6 +690,10 @@ services: ALIYUN_OSS_ACCESS_KEY_SECRET: ${PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET:-} ALIYUN_OSS_AUTH_VERSION: ${PLUGIN_ALIYUN_OSS_AUTH_VERSION:-v4} 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: - "${EXPOSE_PLUGIN_DEBUGGING_PORT:-5003}:${PLUGIN_DEBUGGING_PORT:-5003}" volumes: diff --git a/docker/middleware.env.example b/docker/middleware.env.example index ba6859885b..66037f281c 100644 --- a/docker/middleware.env.example +++ b/docker/middleware.env.example @@ -152,3 +152,8 @@ PLUGIN_ALIYUN_OSS_ACCESS_KEY_ID= PLUGIN_ALIYUN_OSS_ACCESS_KEY_SECRET= PLUGIN_ALIYUN_OSS_AUTH_VERSION=v4 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= diff --git a/sdks/python-client/dify_client/client.py b/sdks/python-client/dify_client/client.py index ee1b5c57e1..d885dc6fb7 100644 --- a/sdks/python-client/dify_client/client.py +++ b/sdks/python-client/dify_client/client.py @@ -47,7 +47,7 @@ class DifyClient: def text_to_audio(self, text: str, user: str, streaming: bool = False): data = {"text": text, "user": user, "streaming": streaming} - return self._send_request("POST", "/text-to-audio", data=data) + return self._send_request("POST", "/text-to-audio", json=data) def get_meta(self, user): params = {"user": user} diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx index 4afba06eae..646c8bd93d 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx @@ -18,9 +18,10 @@ const queryDateFormat = 'YYYY-MM-DD HH:mm' export type IChartViewProps = { appId: string + headerRight: React.ReactNode } -export default function ChartView({ appId }: IChartViewProps) { +export default function ChartView({ appId, headerRight }: IChartViewProps) { const { t } = useTranslation() const appDetail = useAppStore(state => state.appDetail) const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow' @@ -46,19 +47,24 @@ export default function ChartView({ appId }: IChartViewProps) { return (
-
- {t('appOverview.analysis.title')} - ({ value: k, name: t(`appLog.filter.period.${v.name}`) }))} - className='mt-0 !w-40' - onSelect={(item) => { - const id = item.value - const value = TIME_PERIOD_MAPPING[id]?.value ?? '-1' - const name = item.name || t('appLog.filter.period.allTime') - onSelect({ value, name }) - }} - defaultValue={'2'} - /> +
+
{t('common.appMenus.overview')}
+
+
+ ({ value: k, name: t(`appLog.filter.period.${v.name}`) }))} + className='mt-0 !w-40' + onSelect={(item) => { + const id = item.value + const value = TIME_PERIOD_MAPPING[id]?.value ?? '-1' + const name = item.name || t('appLog.filter.period.allTime') + onSelect({ value, name }) + }} + defaultValue={'2'} + /> +
+ {headerRight} +
{!isWorkflow && (
diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx index 0f1bb7e18d..fc97f5e669 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx @@ -1,6 +1,5 @@ import React from 'react' import ChartView from './chartView' -import CardView from './cardView' import TracingPanel from './tracing/panel' import ApikeyInfoPanel from '@/app/components/app/overview/apikey-info-panel' @@ -18,9 +17,10 @@ const Overview = async (props: IDevelopProps) => { return (
- - - + } + />
) } diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx index 8575117c41..76e90ecf19 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx @@ -23,19 +23,6 @@ import Divider from '@/app/components/base/divider' const I18N_PREFIX = 'app.tracing' -const Title = ({ - className, -}: { - className?: string -}) => { - const { t } = useTranslation() - - return ( -
- {t('common.appMenus.overview')} -
- ) -} const Panel: FC = () => { const { t } = useTranslation() const pathname = usePathname() @@ -154,7 +141,6 @@ const Panel: FC = () => { if (!isLoaded) { return (
- <div className='w-[200px]'> <Loading /> </div> @@ -163,8 +149,7 @@ const Panel: FC = () => { } return ( - <div className={cn('mb-3 flex items-center justify-between')}> - <Title className='h-[41px]' /> + <div className={cn('flex items-center justify-between')}> <div className={cn( 'flex cursor-pointer items-center rounded-xl border-l-[0.5px] border-t border-effects-highlight bg-background-default-dodge p-2 shadow-xs hover:border-effects-highlight-lightmode-off hover:bg-background-default-lighter', diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx index c0b52a9b10..b6c97add48 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx @@ -55,6 +55,7 @@ const weaveConfigTemplate = { entity: '', project: '', endpoint: '', + host: '', } const ProviderConfigModal: FC<Props> = ({ @@ -226,6 +227,13 @@ const ProviderConfigModal: FC<Props> = ({ onChange={handleConfigChange('endpoint')} placeholder={'https://trace.wandb.ai/'} /> + <Field + label='Host' + labelClassName='!text-sm' + value={(config as WeaveConfig).host} + onChange={handleConfigChange('host')} + placeholder={'https://api.wandb.ai'} + /> </> )} {type === TracingProvider.langSmith && ( diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts index 386c58974e..ed468caf65 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts @@ -29,4 +29,5 @@ export type WeaveConfig = { entity: string project: string endpoint: string + host: string } diff --git a/web/app/(commonLayout)/apps/AppCard.tsx b/web/app/(commonLayout)/apps/AppCard.tsx index 42967b96f4..31b9ed87c2 100644 --- a/web/app/(commonLayout)/apps/AppCard.tsx +++ b/web/app/(commonLayout)/apps/AppCard.tsx @@ -4,7 +4,7 @@ import { useContext, useContextSelector } from 'use-context-selector' import { useRouter } from 'next/navigation' import { useCallback, useEffect, useMemo, useState } from 'react' 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 type { App } from '@/types/app' import Confirm from '@/app/components/base/confirm' @@ -338,7 +338,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { </div> <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')}> - <RiGlobalLine className='h-4 w-4 text-text-accent' /> + <RiGlobalLine className='h-4 w-4 text-text-quaternary' /> </Tooltip>} {app.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.specific')}> <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')}> <RiBuildingLine className='h-4 w-4 text-text-quaternary' /> </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 className='title-wrapper h-[90px] px-[14px] text-xs leading-normal text-text-tertiary'> diff --git a/web/app/(commonLayout)/apps/Apps.tsx b/web/app/(commonLayout)/apps/Apps.tsx index 1b7ff39383..d0cc7ff91f 100644 --- a/web/app/(commonLayout)/apps/Apps.tsx +++ b/web/app/(commonLayout)/apps/Apps.tsx @@ -88,11 +88,11 @@ const Apps = () => { const anchorRef = useRef<HTMLDivElement>(null) const options = [ { value: 'all', text: t('app.types.all'), icon: <RiApps2Line className='mr-1 h-[14px] w-[14px]' /> }, + { value: 'workflow', text: t('app.types.workflow'), icon: <RiExchange2Line className='mr-1 h-[14px] w-[14px]' /> }, + { value: 'advanced-chat', text: t('app.types.advanced'), icon: <RiMessage3Line className='mr-1 h-[14px] w-[14px]' /> }, { value: 'chat', text: t('app.types.chatbot'), icon: <RiMessage3Line className='mr-1 h-[14px] w-[14px]' /> }, { value: 'agent-chat', text: t('app.types.agent'), icon: <RiRobot3Line className='mr-1 h-[14px] w-[14px]' /> }, { value: 'completion', text: t('app.types.completion'), icon: <RiFile4Line className='mr-1 h-[14px] w-[14px]' /> }, - { value: 'advanced-chat', text: t('app.types.advanced'), icon: <RiMessage3Line className='mr-1 h-[14px] w-[14px]' /> }, - { value: 'workflow', text: t('app.types.workflow'), icon: <RiExchange2Line className='mr-1 h-[14px] w-[14px]' /> }, ] useEffect(() => { diff --git a/web/app/(commonLayout)/datasets/Container.tsx b/web/app/(commonLayout)/datasets/Container.tsx index 62569ab26b..112b6a752e 100644 --- a/web/app/(commonLayout)/datasets/Container.tsx +++ b/web/app/(commonLayout)/datasets/Container.tsx @@ -87,7 +87,7 @@ const Container = () => { return ( <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 value={activeTab} onChange={newActiveTab => setActiveTab(newActiveTab)} diff --git a/web/app/(commonLayout)/datasets/template/template.ja.mdx b/web/app/(commonLayout)/datasets/template/template.ja.mdx index b9fab19948..a796b65bae 100644 --- a/web/app/(commonLayout)/datasets/template/template.ja.mdx +++ b/web/app/(commonLayout)/datasets/template/template.ja.mdx @@ -192,15 +192,15 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi - original_document_id が渡されない場合、新しい操作が実行され、process_rule が必要です。 - <code>indexing_technique</code> インデックスモード - - <code>high_quality</code> 高品質: 埋め込みモデルを使用してベクトルデータベースインデックスを構築 - - <code>economy</code> 経済: キーワードテーブルインデックスの反転インデックスを構築 + - <code>high_quality</code> 高品質:埋め込みモデルを使用してベクトルデータベースインデックスを構築 + - <code>economy</code> 経済:キーワードテーブルインデックスの反転インデックスを構築 - <code>doc_form</code> インデックス化された内容の形式 - <code>text_model</code> テキストドキュメントは直接埋め込まれます; `economy` モードではこの形式がデフォルト - <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>mode</code> (string) クリーニング、セグメンテーションモード、自動 / カスタム @@ -214,7 +214,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi - <code>segmentation</code> (object) セグメンテーションルール - <code>separator</code> カスタムセグメント識別子。現在は 1 つの区切り文字のみ設定可能。デフォルトは \n - <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>separator</code> セグメンテーション識別子。現在は 1 つの区切り文字のみ許可。デフォルトは <code>***</code> - <code>max_tokens</code> 最大長 (トークン) は親チャンクの長さより短いことを検証する必要があります @@ -324,7 +324,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi - <code>partial_members</code> 一部のメンバー </Property> <Property name='provider' type='string' key='provider'> - プロバイダー (オプション、デフォルト: vendor) + プロバイダー (オプション、デフォルト:vendor) - <code>vendor</code> ベンダー - <code>external</code> 外部ナレッジ </Property> @@ -415,16 +415,16 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi 検索キーワード、オプション </Property> <Property name='tag_ids' type='array[string]' key='tag_ids'> - タグIDリスト、オプション + タグ ID リスト、オプション </Property> <Property name='page' type='string' key='page'> - ページ番号、オプション、デフォルト1 + ページ番号、オプション、デフォルト 1 </Property> <Property name='limit' type='string' key='limit'> - 返されるアイテム数、オプション、デフォルト20、範囲1-100 + 返されるアイテム数、オプション、デフォルト 20、範囲 1-100 </Property> <Property name='include_all' type='boolean' key='include_all'> - すべてのデータセットを含めるかどうか(所有者のみ有効)、オプション、デフォルトはfalse + すべてのデータセットを含めるかどうか(所有者のみ有効)、オプション、デフォルトは false </Property> </Properties> </Col> @@ -2013,7 +2013,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi ### Request Body <Properties> <Property name='name' type='string'> - (text) 新しいタグ名、必須、最大長50文字 + (text) 新しいタグ名、必須、最大長 50 文字 </Property> </Properties> </Col> @@ -2099,10 +2099,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi ### Request Body <Properties> <Property name='name' type='string'> - (text) 変更後のタグ名、必須、最大長50文字 + (text) 変更後のタグ名、必須、最大長 50 文字 </Property> <Property name='tag_id' type='string'> - (text) タグID、必須 + (text) タグ ID、必須 </Property> </Properties> </Col> @@ -2147,7 +2147,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi ### Request Body <Properties> <Property name='tag_id' type='string'> - (text) タグID、必須 + (text) タグ ID、必須 </Property> </Properties> </Col> @@ -2188,10 +2188,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi ### Request Body <Properties> <Property name='tag_ids' type='list'> - (list) タグIDリスト、必須 + (list) タグ ID リスト、必須 </Property> <Property name='target_id' type='string'> - (text) ナレッジベースID、必須 + (text) ナレッジベース ID、必須 </Property> </Properties> </Col> @@ -2230,10 +2230,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi ### Request Body <Properties> <Property name='tag_id' type='string'> - (text) タグID、必須 + (text) タグ ID、必須 </Property> <Property name='target_id' type='string'> - (text) ナレッジベースID、必須 + (text) ナレッジベース ID、必須 </Property> </Properties> </Col> @@ -2273,7 +2273,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi ### Path <Properties> <Property name='dataset_id' type='string'> - (text) ナレッジベースID + (text) ナレッジベース ID </Property> </Properties> </Col> diff --git a/web/app/(commonLayout)/datasets/template/template.zh.mdx b/web/app/(commonLayout)/datasets/template/template.zh.mdx index b10f22002a..d121a93df2 100644 --- a/web/app/(commonLayout)/datasets/template/template.zh.mdx +++ b/web/app/(commonLayout)/datasets/template/template.zh.mdx @@ -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>process_rule</code> 处理规则 - - <code>mode</code> (string) 清洗、分段模式 ,automatic 自动 / custom 自定义 / hierarchical 父子 + - <code>mode</code> (string) 清洗、分段模式,automatic 自动 / custom 自定义 / hierarchical 父子 - <code>rules</code> (object) 自定义规则(自动模式下,该字段为空) - <code>pre_processing_rules</code> (array[object]) 预处理规则 - <code>id</code> (string) 预处理规则的唯一标识符 @@ -234,12 +234,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi - <code>hybrid_search</code> 混合检索 - <code>semantic_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_provider_name</code> (string) Rerank 模型的提供商 - <code>reranking_model_name</code> (string) Rerank 模型的名称 - <code>top_k</code> (int) 召回条数 - - <code>score_threshold_enabled</code> (bool)是否开启召回分数限制 + - <code>score_threshold_enabled</code> (bool) 是否开启召回分数限制 - <code>score_threshold</code> (float) 召回分数限制 </Property> <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>semantic_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_provider_name</code> (string) Rerank 模型的提供商 - <code>reranking_model_name</code> (string) Rerank 模型的名称 - <code>top_k</code> (int) 召回条数 - - <code>score_threshold_enabled</code> (bool)是否开启召回分数限制 + - <code>score_threshold_enabled</code> (bool) 是否开启召回分数限制 - <code>score_threshold</code> (float) 召回分数限制 </Property> </Properties> @@ -1322,7 +1322,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi 文档 ID </Property> <Property name='segment_id' type='string' key='segment_id'> - 文档分段ID + 文档分段 ID </Property> </Properties> </Col> @@ -1435,7 +1435,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi 文档 ID </Property> <Property name='segment_id' type='string' key='segment_id'> - 文档分段ID + 文档分段 ID </Property> </Properties> @@ -2223,7 +2223,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi - <code>document_id</code> (string) 文档 ID - <code>metadata_list</code> (list) 元数据列表 - <code>id</code> (string) 元数据 ID - - <code>type</code> (string) 元数据类型 + - <code>value</code> (string) 元数据值 - <code>name</code> (string) 元数据名称 </Property> </Properties> @@ -2404,7 +2404,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi ### Request Body <Properties> <Property name='name' type='string'> - (text) 新标签名称,必填,最大长度为50 + (text) 新标签名称,必填,最大长度为 50 </Property> </Properties> </Col> @@ -2490,10 +2490,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi ### Request Body <Properties> <Property name='name' type='string'> - (text) 修改后的标签名称,必填,最大长度为50 + (text) 修改后的标签名称,必填,最大长度为 50 </Property> <Property name='tag_id' type='string'> - (text) 标签ID,必填 + (text) 标签 ID,必填 </Property> </Properties> </Col> @@ -2538,7 +2538,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi ### Request Body <Properties> <Property name='tag_id' type='string'> - (text) 标签ID,必填 + (text) 标签 ID,必填 </Property> </Properties> </Col> @@ -2579,10 +2579,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi ### Request Body <Properties> <Property name='tag_ids' type='list'> - (list) 标签ID列表,必填 + (list) 标签 ID 列表,必填 </Property> <Property name='target_id' type='string'> - (text) 知识库ID,必填 + (text) 知识库 ID,必填 </Property> </Properties> </Col> @@ -2621,10 +2621,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi ### Request Body <Properties> <Property name='tag_id' type='string'> - (text) 标签ID,必填 + (text) 标签 ID,必填 </Property> <Property name='target_id' type='string'> - (text) 知识库ID,必填 + (text) 知识库 ID,必填 </Property> </Properties> </Col> @@ -2664,7 +2664,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi ### Path <Properties> <Property name='dataset_id' type='string'> - (text) 知识库ID + (text) 知识库 ID </Property> </Properties> </Col> diff --git a/web/app/(shareLayout)/layout.tsx b/web/app/(shareLayout)/layout.tsx index 83adbd3cae..8db336a17d 100644 --- a/web/app/(shareLayout)/layout.tsx +++ b/web/app/(shareLayout)/layout.tsx @@ -1,14 +1,42 @@ -import React from 'react' +'use client' +import React, { useEffect, useState } from 'react' import type { FC } from 'react' -import type { Metadata } from 'next' - -export const metadata: Metadata = { - icons: 'data:,', // prevent browser from using default favicon -} +import { usePathname, useSearchParams } from 'next/navigation' +import Loading from '../components/base/loading' +import { useGlobalPublicStore } from '@/context/global-public-context' +import { AccessMode } from '@/models/access-control' +import { getAppAccessModeByAppCode } from '@/service/share' const Layout: FC<{ children: React.ReactNode }> = ({ 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 ( <div className="h-full min-w-[300px] pb-[env(safe-area-inset-bottom)]"> {children} diff --git a/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx b/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx new file mode 100644 index 0000000000..da754794b1 --- /dev/null +++ b/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx @@ -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> +} diff --git a/web/app/(shareLayout)/webapp-reset-password/layout.tsx b/web/app/(shareLayout)/webapp-reset-password/layout.tsx new file mode 100644 index 0000000000..e0ac6b9ad6 --- /dev/null +++ b/web/app/(shareLayout)/webapp-reset-password/layout.tsx @@ -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> + </> +} diff --git a/web/app/(shareLayout)/webapp-reset-password/page.tsx b/web/app/(shareLayout)/webapp-reset-password/page.tsx new file mode 100644 index 0000000000..96cd4c5805 --- /dev/null +++ b/web/app/(shareLayout)/webapp-reset-password/page.tsx @@ -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> +} diff --git a/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx b/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx new file mode 100644 index 0000000000..9f9a8ad4e3 --- /dev/null +++ b/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx @@ -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 diff --git a/web/app/(shareLayout)/webapp-signin/check-code/page.tsx b/web/app/(shareLayout)/webapp-signin/check-code/page.tsx new file mode 100644 index 0000000000..1b8f18c98f --- /dev/null +++ b/web/app/(shareLayout)/webapp-signin/check-code/page.tsx @@ -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> +} diff --git a/web/app/(shareLayout)/webapp-signin/components/external-member-sso-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/external-member-sso-auth.tsx new file mode 100644 index 0000000000..e9b15ae331 --- /dev/null +++ b/web/app/(shareLayout)/webapp-signin/components/external-member-sso-auth.tsx @@ -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) diff --git a/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx new file mode 100644 index 0000000000..29af3e3a57 --- /dev/null +++ b/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx @@ -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> + ) +} diff --git a/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx new file mode 100644 index 0000000000..d9e56af1b8 --- /dev/null +++ b/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx @@ -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> +} diff --git a/web/app/(shareLayout)/webapp-signin/components/sso-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/sso-auth.tsx new file mode 100644 index 0000000000..5d649322ba --- /dev/null +++ b/web/app/(shareLayout)/webapp-signin/components/sso-auth.tsx @@ -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 diff --git a/web/app/(shareLayout)/webapp-signin/layout.tsx b/web/app/(shareLayout)/webapp-signin/layout.tsx new file mode 100644 index 0000000000..a03364d326 --- /dev/null +++ b/web/app/(shareLayout)/webapp-signin/layout.tsx @@ -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> + </> +} diff --git a/web/app/(shareLayout)/webapp-signin/normalForm.tsx b/web/app/(shareLayout)/webapp-signin/normalForm.tsx new file mode 100644 index 0000000000..d6bdf607ba --- /dev/null +++ b/web/app/(shareLayout)/webapp-signin/normalForm.tsx @@ -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')} +   + <Link + className='system-xs-medium text-text-secondary hover:underline' + target='_blank' rel='noopener noreferrer' + href='https://dify.ai/terms' + >{t('login.tos')}</Link> +  &  + <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')} +   + <Link + className='system-xs-medium text-text-secondary hover:underline' + href='/install' + >{t('login.setAdminAccount')}</Link> + </div>} + </>} + + </div> + </div> + </> + ) +} + +export default NormalForm diff --git a/web/app/(shareLayout)/webapp-signin/page.tsx b/web/app/(shareLayout)/webapp-signin/page.tsx index 668c3f312c..07b7c88430 100644 --- a/web/app/(shareLayout)/webapp-signin/page.tsx +++ b/web/app/(shareLayout)/webapp-signin/page.tsx @@ -3,30 +3,45 @@ import { useRouter, useSearchParams } from 'next/navigation' import type { FC } from 'react' import React, { useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' -import { RiDoorLockLine } from '@remixicon/react' -import cn from '@/utils/classnames' import Toast from '@/app/components/base/toast' -import { fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share' -import { setAccessToken } from '@/app/components/share/utils' +import { removeAccessToken, setAccessToken } from '@/app/components/share/utils' 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' +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 { t } = useTranslation() const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) + const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode) const searchParams = useSearchParams() const router = useRouter() const redirectUrl = searchParams.get('redirect_url') const tokenFromUrl = searchParams.get('web_sso_token') const message = searchParams.get('message') + const code = searchParams.get('code') - const showErrorToast = (message: string) => { + const getSigninUrl = useCallback(() => { + const params = new URLSearchParams(searchParams) + params.delete('message') + params.delete('code') + return `/webapp-signin?${params.toString()}` + }, [searchParams]) + + const backToHome = useCallback(() => { + removeAccessToken() + const url = getSigninUrl() + router.replace(url) + }, [getSigninUrl, router]) + + const showErrorToast = (msg: string) => { Toast.notify({ type: 'error', - message, + message: msg, }) } @@ -38,102 +53,73 @@ const WebSSOForm: FC = () => { return appCode }, [redirectUrl]) - const processTokenAndRedirect = useCallback(async () => { - const appCode = getAppCodeFromRedirectUrl() - if (!appCode || !tokenFromUrl || !redirectUrl) { - showErrorToast('redirect url or app code or token is invalid.') - return - } + useEffect(() => { + (async () => { + if (message) + 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 + const appCode = getAppCodeFromRedirectUrl() + 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 } - case SSOProtocol.OIDC: { - const oidcRes = await fetchWebOIDCSSOUrl(appCode, redirectUrl) - router.push(oidcRes.url) - break + 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) } - 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]) + })() + }, [getAppCodeFromRedirectUrl, redirectUrl, router, tokenFromUrl, message]) useEffect(() => { - const init = async () => { - if (message) { - showErrorToast(message) - return - } + if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC && redirectUrl) + router.replace(redirectUrl) + }, [webAppAccessMode, router, redirectUrl]) - if (!tokenFromUrl) { - await handleSSOLogin() - return - } - - await processTokenAndRedirect() - } - - init() - }, [message, processTokenAndRedirect, tokenFromUrl, handleSSOLogin]) - if (tokenFromUrl) - return <div className='flex h-full items-center justify-center'><Loading /></div> - if (message) { + if (tokenFromUrl) { return <div className='flex h-full items-center justify-center'> - <AppUnavailable code={'App Unavailable'} unknownReason={message} /> + <Loading /> </div> } - if (systemFeatures.webapp_auth.enabled) { - if (systemFeatures.webapp_auth.allow_sso) { - return ( - <div className="flex h-full items-center justify-center"> - <div className={cn('flex w-full grow flex-col items-center justify-center', 'px-6', 'md:px-[108px]')}> - <Loading /> - </div> - </div> - ) - } - return <div className="flex h-full items-center justify-center"> - <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.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> + if (message) { + return <div className='flex h-full flex-col items-center justify-center gap-y-4'> + <AppUnavailable className='h-auto w-auto' code={code || t('share.common.appUnavailable')} unknownReason={message} /> + <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{code === '403' ? t('common.userProfile.logout') : t('share.login.backToHome')}</span> </div> } - else { + if (!redirectUrl) { + 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.' /> + </div> + } + if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC) { + return <div className='flex h-full items-center justify-center'> + <Loading /> + </div> + } + if (!systemFeatures.webapp_auth.enabled) { return <div className="flex h-full items-center justify-center"> <p className='system-xs-regular text-text-tertiary'>{t('login.webapp.disabled')}</p> </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) diff --git a/web/app/components/app/app-access-control/index.tsx b/web/app/components/app/app-access-control/index.tsx index 2f15c8ec48..13faaea957 100644 --- a/web/app/components/app/app-access-control/index.tsx +++ b/web/app/components/app/app-access-control/index.tsx @@ -1,6 +1,6 @@ 'use client' -import { Dialog } from '@headlessui/react' -import { RiBuildingLine, RiGlobalLine } from '@remixicon/react' +import { Description as DialogDescription, DialogTitle } from '@headlessui/react' +import { RiBuildingLine, RiGlobalLine, RiVerifiedBadgeLine } from '@remixicon/react' import { useTranslation } from 'react-i18next' import { useCallback, useEffect } from 'react' import Button from '../../base/button' @@ -67,8 +67,8 @@ export default function AccessControl(props: AccessControlProps) { return <AccessControlDialog show onClose={onClose}> <div className='flex flex-col gap-y-3'> <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> - <Dialog.Description className='system-xs-regular mt-1 text-text-tertiary'>{t('app.accessControlDialog.description')}</Dialog.Description> + <DialogTitle className='title-2xl-semi-bold text-text-primary'>{t('app.accessControlDialog.title')}</DialogTitle> + <DialogDescription className='system-xs-regular mt-1 text-text-tertiary'>{t('app.accessControlDialog.description')}</DialogDescription> </div> <div className='flex flex-col gap-y-1 px-6 pb-3'> <div className='leading-6'> @@ -80,12 +80,20 @@ export default function AccessControl(props: AccessControlProps) { <RiBuildingLine className='h-4 w-4 text-text-primary' /> <p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.organization')}</p> </div> - {!hideTip && <WebAppSSONotEnabledTip />} </div> </AccessControlItem> <AccessControlItem type={AccessMode.SPECIFIC_GROUPS_MEMBERS}> <SpecificGroupsOrMembers /> </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}> <div className='flex items-center gap-x-2 p-3'> <RiGlobalLine className='h-4 w-4 text-text-primary' /> diff --git a/web/app/components/app/app-access-control/specific-groups-or-members.tsx b/web/app/components/app/app-access-control/specific-groups-or-members.tsx index f4872f8c99..b30c8f1ba3 100644 --- a/web/app/components/app/app-access-control/specific-groups-or-members.tsx +++ b/web/app/components/app/app-access-control/specific-groups-or-members.tsx @@ -3,12 +3,10 @@ import { RiAlertFill, RiCloseCircleFill, RiLockLine, RiOrganizationChart } from import { useTranslation } from 'react-i18next' import { useCallback, useEffect } from 'react' import Avatar from '../../base/avatar' -import Divider from '../../base/divider' import Tooltip from '../../base/tooltip' import Loading from '../../base/loading' import useAccessControlStore from '../../../../context/access-control-store' import AddMemberOrGroupDialog from './add-member-or-group-pop' -import { useGlobalPublicStore } from '@/context/global-public-context' import type { AccessControlAccount, AccessControlGroup } from '@/models/access-control' import { AccessMode } from '@/models/access-control' import { useAppWhiteListSubjects } from '@/service/access-control' @@ -19,11 +17,6 @@ export default function SpecificGroupsOrMembers() { const setSpecificGroups = useAccessControlStore(s => s.setSpecificGroups) const setSpecificMembers = useAccessControlStore(s => s.setSpecificMembers) 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) useEffect(() => { @@ -37,7 +30,6 @@ export default function SpecificGroupsOrMembers() { <RiLockLine className='h-4 w-4 text-text-primary' /> <p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.specific')}</p> </div> - {!hideTip && <WebAppSSONotEnabledTip />} </div> } @@ -48,10 +40,6 @@ export default function SpecificGroupsOrMembers() { <p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.specific')}</p> </div> <div className='flex items-center gap-x-1'> - {!hideTip && <> - <WebAppSSONotEnabledTip /> - <Divider className='ml-2 mr-0 h-[14px]' type="vertical" /> - </>} <AddMemberOrGroupDialog /> </div> </div> diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index 8d0028c7d7..1485964198 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -9,11 +9,14 @@ import dayjs from 'dayjs' import { RiArrowDownSLine, RiArrowRightSLine, + RiBuildingLine, + RiGlobalLine, RiLockLine, RiPlanetLine, RiPlayCircleLine, RiPlayList2Line, RiTerminalBoxLine, + RiVerifiedBadgeLine, } from '@remixicon/react' import { useKeyPress } from 'ahooks' import { getKeyboardKeyCodeBySystem } from '../../workflow/utils' @@ -275,11 +278,33 @@ const AppPublisher = ({ onClick={() => { setShowAppAccessControl(true) }}> - <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 && <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>} - {appDetail?.access_mode === AccessMode.PUBLIC && <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.anyone')}</p>} + <div className='flex grow items-center gap-x-1.5 overflow-hidden pr-1'> + {appDetail?.access_mode === AccessMode.ORGANIZATION + && <> + <RiBuildingLine className='h-4 w-4 shrink-0 text-text-secondary' /> + <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' /> + <div className='grow truncate'> + <span className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.specific')}</span> + </div> + </> + } + {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>} <div className='flex h-4 w-4 shrink-0 items-center justify-center'> diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx index 46cb495801..6e5547d08a 100644 --- a/web/app/components/app/create-app-modal/index.tsx +++ b/web/app/components/app/create-app-modal/index.tsx @@ -1,9 +1,9 @@ 'use client' -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useRouter, useSearchParams } from 'next/navigation' +import { useRouter } from 'next/navigation' import { useContext, useContextSelector } from 'use-context-selector' import { RiArrowRightLine, RiArrowRightSLine, RiCommandLine, RiCornerDownLeftLine, RiExchange2Fill } from '@remixicon/react' import Link from 'next/link' @@ -19,7 +19,6 @@ import AppsContext, { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' import { ToastContext } from '@/app/components/base/toast' import type { AppMode } from '@/types/app' -import { AppModes } from '@/types/app' import { createApp } from '@/service/apps' import Input from '@/app/components/base/input' import Textarea from '@/app/components/base/textarea' @@ -56,14 +55,6 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate }: CreateAppProps) const isCreatingRef = useRef(false) - const searchParams = useSearchParams() - - useEffect(() => { - const category = searchParams.get('category') - if (category && AppModes.includes(category as AppMode)) - setAppMode(category as AppMode) - }, [searchParams]) - const onCreate = useCallback(async () => { if (!appMode) { notify({ type: 'error', message: t('app.newApp.appTypeRequired') }) @@ -128,7 +119,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate }: CreateAppProps) onClick={() => { setAppMode('workflow') }} /> - <AppTypeCard + <AppTypeCard active={appMode === 'advanced-chat'} title={t('app.types.advanced')} description={t('app.newApp.advancedShortDescription')} diff --git a/web/app/components/app/overview/appCard.tsx b/web/app/components/app/overview/appCard.tsx index 9b283cdf5e..9f3b3ac4a6 100644 --- a/web/app/components/app/overview/appCard.tsx +++ b/web/app/components/app/overview/appCard.tsx @@ -5,10 +5,13 @@ import { useTranslation } from 'react-i18next' import { RiArrowRightSLine, RiBookOpenLine, + RiBuildingLine, RiEqualizer2Line, RiExternalLinkLine, + RiGlobalLine, RiLockLine, RiPaintBrushLine, + RiVerifiedBadgeLine, RiWindowLine, } from '@remixicon/react' 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' onClick={handleClickAccessControl}> <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 && <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>} - {appDetail?.access_mode === AccessMode.PUBLIC && <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.anyone')}</p>} - </div> + {appDetail?.access_mode === AccessMode.ORGANIZATION + && <> + <RiBuildingLine className='h-4 w-4 shrink-0 text-text-secondary' /> + <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> {!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'> <RiArrowRightSLine className='h-4 w-4 text-text-quaternary' /> diff --git a/web/app/components/app/type-selector/index.tsx b/web/app/components/app/type-selector/index.tsx index 0accafdf4b..a57bac20db 100644 --- a/web/app/components/app/type-selector/index.tsx +++ b/web/app/components/app/type-selector/index.tsx @@ -15,7 +15,7 @@ export type AppSelectorProps = { onChange: (value: AppSelectorProps['value']) => void } -const allTypes: AppMode[] = ['chat', 'agent-chat', 'completion', 'advanced-chat', 'workflow'] +const allTypes: AppMode[] = ['workflow', 'advanced-chat', 'chat', 'agent-chat', 'completion'] const AppTypeSelector = ({ value, onChange }: AppSelectorProps) => { const [open, setOpen] = useState(false) diff --git a/web/app/components/base/app-unavailable.tsx b/web/app/components/base/app-unavailable.tsx index 4e835cbfcf..c501d36118 100644 --- a/web/app/components/base/app-unavailable.tsx +++ b/web/app/components/base/app-unavailable.tsx @@ -1,4 +1,5 @@ 'use client' +import classNames from '@/utils/classnames' import type { FC } from 'react' import React from 'react' import { useTranslation } from 'react-i18next' @@ -7,18 +8,20 @@ type IAppUnavailableProps = { code?: number | string isUnknownReason?: boolean unknownReason?: string + className?: string } const AppUnavailable: FC<IAppUnavailableProps> = ({ code = 404, isUnknownReason, unknownReason, + className, }) => { const { t } = useTranslation() return ( - <div className='flex h-screen w-screen items-center justify-center'> - <h1 className='mr-5 h-[50px] pr-5 text-[24px] font-medium leading-[50px]' + <div className={classNames('flex h-screen w-screen items-center justify-center', className)}> + <h1 className='mr-5 h-[50px] shrink-0 pr-5 text-[24px] font-medium leading-[50px]' style={{ borderRight: '1px solid rgba(0,0,0,.3)', }}>{code}</h1> diff --git a/web/app/components/base/chat/chat-with-history/context.tsx b/web/app/components/base/chat/chat-with-history/context.tsx index bb09c95705..5bf1514774 100644 --- a/web/app/components/base/chat/chat-with-history/context.tsx +++ b/web/app/components/base/chat/chat-with-history/context.tsx @@ -16,14 +16,12 @@ import type { ConversationItem, } from '@/models/share' import { noop } from 'lodash-es' -import { AccessMode } from '@/models/access-control' export type ChatWithHistoryContextValue = { appInfoError?: any appInfoLoading?: boolean appMeta?: AppMeta appData?: AppData - accessMode?: AccessMode userCanAccess?: boolean appParams?: ChatConfig appChatListDataLoading?: boolean @@ -64,7 +62,6 @@ export type ChatWithHistoryContextValue = { } export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({ - accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS, userCanAccess: false, currentConversationId: '', appPrevChatTree: [], diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index 3694666139..32f74e6457 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -16,7 +16,7 @@ import type { Feedback, } from '../types' import { CONVERSATION_ID_INFO } from '../constants' -import { buildChatItemTree, getProcessedSystemVariablesFromUrlParams } from '../utils' +import { buildChatItemTree, getProcessedSystemVariablesFromUrlParams, getRawInputsFromUrlParams } from '../utils' import { addFileInfos, sortAgentSorts } from '../../../tools/utils' import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' import { @@ -43,9 +43,8 @@ import { useAppFavicon } from '@/hooks/use-app-favicon' import { InputVarType } from '@/app/components/workflow/types' import { TransferMethod } from '@/types/app' 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 { AccessMode } from '@/models/access-control' function getFormattedChatList(messages: any[]) { const newChatList: ChatItem[] = [] @@ -77,11 +76,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) 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({ appId: installedAppInfo?.app.id || appInfo?.app_id, isInstalledApp, @@ -195,6 +189,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { const { t } = useTranslation() const newConversationInputsRef = useRef<Record<string, any>>({}) const [newConversationInputs, setNewConversationInputs] = useState<Record<string, any>>({}) + const [initInputs, setInitInputs] = useState<Record<string, any>>({}) const handleNewConversationInputsChange = useCallback((newInputs: Record<string, any>) => { newConversationInputsRef.current = newInputs setNewConversationInputs(newInputs) @@ -202,20 +197,29 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { const inputsForms = useMemo(() => { return (appParams?.user_input_form || []).filter((item: any) => !item.external_data_tool).map((item: any) => { 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 { ...item.paragraph, + default: value || item.default, type: 'paragraph', } } if (item.number) { + const convertedNumber = Number(initInputs[item.number.variable]) ?? undefined return { ...item.number, + default: convertedNumber || item.default, type: 'number', } } if (item.select) { + const isInputInOptions = item.select.options.includes(initInputs[item.select.variable]) return { ...item.select, + default: (isInputInOptions ? initInputs[item.select.variable] : undefined) || item.default, 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 { ...item['text-input'], + default: value || item.default, type: 'text-input', } }) - }, [appParams]) + }, [initInputs, appParams]) const allInputsHidden = useMemo(() => { return inputsForms.length > 0 && inputsForms.every(item => item.hide === true) }, [inputsForms]) + useEffect(() => { + // init inputs from url params + (async () => { + const inputs = await getRawInputsFromUrlParams() + setInitInputs(inputs) + })() + }, []) + useEffect(() => { const conversationInputs: Record<string, any> = {} @@ -362,11 +379,11 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { if (conversationId) setClearChatList(false) }, [handleConversationIdInfoChange, setClearChatList]) - const handleNewConversation = useCallback(() => { + const handleNewConversation = useCallback(async () => { currentChatInstanceRef.current.handleStop() setShowNewConversationItemInList(true) handleChangeConversation('') - handleNewConversationInputsChange({}) + handleNewConversationInputsChange(await getRawInputsFromUrlParams()) setClearChatList(true) }, [handleChangeConversation, setShowNewConversationItemInList, handleNewConversationInputsChange, setClearChatList]) const handleUpdateConversationList = useCallback(() => { @@ -469,8 +486,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { return { appInfoError, - appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission)), - accessMode: systemFeatures.webapp_auth.enabled ? appAccessMode?.accessMode : AccessMode.PUBLIC, + appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission), userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true, isInstalledApp, appId, diff --git a/web/app/components/base/chat/chat-with-history/index.tsx b/web/app/components/base/chat/chat-with-history/index.tsx index de023e7f58..fe8e7b430d 100644 --- a/web/app/components/base/chat/chat-with-history/index.tsx +++ b/web/app/components/base/chat/chat-with-history/index.tsx @@ -1,5 +1,7 @@ +'use client' import type { FC } from 'react' import { + useCallback, useEffect, useState, } from 'react' @@ -17,10 +19,12 @@ import ChatWrapper from './chat-wrapper' import type { InstalledApp } from '@/models/explore' import Loading from '@/app/components/base/loading' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' -import { checkOrSetAccessToken } from '@/app/components/share/utils' +import { checkOrSetAccessToken, removeAccessToken } from '@/app/components/share/utils' import AppUnavailable from '@/app/components/base/app-unavailable' import cn from '@/utils/classnames' import useDocumentTitle from '@/hooks/use-document-title' +import { useTranslation } from 'react-i18next' +import { usePathname, useRouter, useSearchParams } from 'next/navigation' type ChatWithHistoryProps = { className?: string @@ -38,6 +42,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({ isMobile, themeBuilder, sidebarCollapseState, + isInstalledApp, } = useChatWithHistoryContext() const isSidebarCollapsed = sidebarCollapseState const customConfig = appData?.custom_config @@ -51,13 +56,34 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({ useDocumentTitle(site?.title || 'Chat') + const { t } = useTranslation() + const searchParams = useSearchParams() + const router = useRouter() + const pathname = usePathname() + const getSigninUrl = useCallback(() => { + const params = new URLSearchParams(searchParams) + params.delete('message') + params.set('redirect_url', pathname) + return `/webapp-signin?${params.toString()}` + }, [searchParams, pathname]) + + const backToHome = useCallback(() => { + removeAccessToken() + const url = getSigninUrl() + router.replace(url) + }, [getSigninUrl, router]) + if (appInfoLoading) { return ( <Loading type='app' /> ) } - if (!userCanAccess) - return <AppUnavailable code={403} unknownReason='no permission.' /> + if (!userCanAccess) { + return <div className='flex h-full flex-col items-center justify-center gap-y-2'> + <AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' /> + {!isInstalledApp && <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span>} + </div> + } if (appInfoError) { return ( @@ -124,7 +150,6 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({ const { appInfoError, appInfoLoading, - accessMode, userCanAccess, appData, appParams, @@ -169,7 +194,6 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({ appInfoError, appInfoLoading, appData, - accessMode, userCanAccess, appParams, appMeta, diff --git a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx index fd317ccf91..4e50c1cb79 100644 --- a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx +++ b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx @@ -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 type { ConversationItem } from '@/models/share' import cn from '@/utils/classnames' -import { AccessMode } from '@/models/access-control' import { useGlobalPublicStore } from '@/context/global-public-context' type Props = { @@ -30,7 +29,6 @@ const Sidebar = ({ isPanel }: Props) => { const { t } = useTranslation() const { isInstalledApp, - accessMode, appData, handleNewConversation, pinnedConversationList, @@ -140,7 +138,7 @@ const Sidebar = ({ isPanel }: Props) => { )} </div> <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 */} <div className='shrink-0'> {!appData?.custom_config?.remove_webapp_brand && ( diff --git a/web/app/components/base/chat/chat/answer/__mocks__/markdownContentSVG.ts b/web/app/components/base/chat/chat/answer/__mocks__/markdownContentSVG.ts index bcc3ae628d..51995a4af5 100644 --- a/web/app/components/base/chat/chat/answer/__mocks__/markdownContentSVG.ts +++ b/web/app/components/base/chat/chat/answer/__mocks__/markdownContentSVG.ts @@ -3,7 +3,7 @@ export const markdownContentSVG = ` <svg width="400" height="600" xmlns="http://www.w3.org/2000/svg"> <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"/> diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts index 28f297b90e..10fb455d33 100644 --- a/web/app/components/base/chat/chat/hooks.ts +++ b/web/app/components/base/chat/chat/hooks.ts @@ -366,7 +366,7 @@ export const useChat = ( if (!newResponseItem) return - const isUseAgentThought = newResponseItem.agent_thoughts?.length > 0 + const isUseAgentThought = newResponseItem.agent_thoughts?.length > 0 && newResponseItem.agent_thoughts[newResponseItem.agent_thoughts?.length - 1].thought === newResponseItem.answer updateChatTreeNode(responseItem.id, { content: isUseAgentThought ? '' : newResponseItem.answer, log: [ diff --git a/web/app/components/base/chat/chat/index.tsx b/web/app/components/base/chat/chat/index.tsx index c0842af0c4..801daa6589 100644 --- a/web/app/components/base/chat/chat/index.tsx +++ b/web/app/components/base/chat/chat/index.tsx @@ -303,7 +303,7 @@ const Chat: FC<ChatProps> = ({ { !noChatInput && ( <ChatInputArea - botName={appData?.site.title || ''} + botName={appData?.site.title || 'Bot'} disabled={inputDisabled} showFeatureBar={showFeatureBar} showFileUpload={showFileUpload} diff --git a/web/app/components/base/chat/embedded-chatbot/context.tsx b/web/app/components/base/chat/embedded-chatbot/context.tsx index 5964efd806..d24265ed9e 100644 --- a/web/app/components/base/chat/embedded-chatbot/context.tsx +++ b/web/app/components/base/chat/embedded-chatbot/context.tsx @@ -15,10 +15,8 @@ import type { ConversationItem, } from '@/models/share' import { noop } from 'lodash-es' -import { AccessMode } from '@/models/access-control' export type EmbeddedChatbotContextValue = { - accessMode?: AccessMode userCanAccess?: boolean appInfoError?: any appInfoLoading?: boolean @@ -58,7 +56,6 @@ export type EmbeddedChatbotContextValue = { export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({ userCanAccess: false, - accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS, currentConversationId: '', appPrevChatList: [], pinnedConversationList: [], diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.tsx index 40c56eca7b..0158e8d041 100644 --- a/web/app/components/base/chat/embedded-chatbot/hooks.tsx +++ b/web/app/components/base/chat/embedded-chatbot/hooks.tsx @@ -36,9 +36,8 @@ import { InputVarType } from '@/app/components/workflow/types' import { TransferMethod } from '@/types/app' import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' 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 { AccessMode } from '@/models/access-control' function getFormattedChatList(messages: any[]) { const newChatList: ChatItem[] = [] @@ -70,11 +69,6 @@ export const useEmbeddedChatbot = () => { const isInstalledApp = false const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) 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({ appId: appInfo?.app_id, isInstalledApp, @@ -385,8 +379,7 @@ export const useEmbeddedChatbot = () => { return { appInfoError, - appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission)), - accessMode: systemFeatures.webapp_auth.enabled ? appAccessMode?.accessMode : AccessMode.PUBLIC, + appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission), userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true, isInstalledApp, allowResetChat, diff --git a/web/app/components/base/chat/embedded-chatbot/index.tsx b/web/app/components/base/chat/embedded-chatbot/index.tsx index 002d142542..c54afd78ea 100644 --- a/web/app/components/base/chat/embedded-chatbot/index.tsx +++ b/web/app/components/base/chat/embedded-chatbot/index.tsx @@ -1,4 +1,6 @@ +'use client' import { + useCallback, useEffect, useState, } from 'react' @@ -12,7 +14,7 @@ import { useEmbeddedChatbot } from './hooks' import { isDify } from './utils' import { useThemeContext } from './theme/theme-context' import { CssTransform } from './theme/utils' -import { checkOrSetAccessToken } from '@/app/components/share/utils' +import { checkOrSetAccessToken, removeAccessToken } from '@/app/components/share/utils' import AppUnavailable from '@/app/components/base/app-unavailable' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import Loading from '@/app/components/base/loading' @@ -23,6 +25,7 @@ import DifyLogo from '@/app/components/base/logo/dify-logo' import cn from '@/utils/classnames' import useDocumentTitle from '@/hooks/use-document-title' import { useGlobalPublicStore } from '@/context/global-public-context' +import { usePathname, useRouter, useSearchParams } from 'next/navigation' const Chatbot = () => { const { @@ -36,6 +39,7 @@ const Chatbot = () => { chatShouldReloadKey, handleNewConversation, themeBuilder, + isInstalledApp, } = useEmbeddedChatbotContext() const { t } = useTranslation() const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) @@ -51,6 +55,22 @@ const Chatbot = () => { useDocumentTitle(site?.title || 'Chat') + const searchParams = useSearchParams() + const router = useRouter() + const pathname = usePathname() + const getSigninUrl = useCallback(() => { + const params = new URLSearchParams(searchParams) + params.delete('message') + params.set('redirect_url', pathname) + return `/webapp-signin?${params.toString()}` + }, [searchParams, pathname]) + + const backToHome = useCallback(() => { + removeAccessToken() + const url = getSigninUrl() + router.replace(url) + }, [getSigninUrl, router]) + if (appInfoLoading) { return ( <> @@ -66,8 +86,12 @@ const Chatbot = () => { ) } - if (!userCanAccess) - return <AppUnavailable code={403} unknownReason='no permission.' /> + if (!userCanAccess) { + return <div className='flex h-full flex-col items-center justify-center gap-y-2'> + <AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' /> + {!isInstalledApp && <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span>} + </div> + } if (appInfoError) { return ( @@ -141,7 +165,6 @@ const EmbeddedChatbotWrapper = () => { appInfoError, appInfoLoading, appData, - accessMode, userCanAccess, appParams, appMeta, @@ -176,7 +199,6 @@ const EmbeddedChatbotWrapper = () => { return <EmbeddedChatbotContext.Provider value={{ userCanAccess, - accessMode, appInfoError, appInfoLoading, appData, diff --git a/web/app/components/base/chat/utils.ts b/web/app/components/base/chat/utils.ts index bdac5998fe..8e044375d3 100644 --- a/web/app/components/base/chat/utils.ts +++ b/web/app/components/base/chat/utils.ts @@ -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>> { const urlParams = new URLSearchParams(window.location.search) const inputs: Record<string, any> = {} @@ -184,6 +195,7 @@ function getThreadMessages(tree: ChatItemInTree[], targetMessageId?: string): Ch } export { + getRawInputsFromUrlParams, getProcessedInputsFromUrlParams, getProcessedSystemVariablesFromUrlParams, isValidGeneratedAnswer, diff --git a/web/app/components/base/file-uploader/hooks.ts b/web/app/components/base/file-uploader/hooks.ts index 66d5b46ba7..8e1b2148c5 100644 --- a/web/app/components/base/file-uploader/hooks.ts +++ b/web/app/components/base/file-uploader/hooks.ts @@ -231,7 +231,7 @@ export const useFile = (fileConfig: FileUpload) => { url: res.url, } 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) } if (!checkSizeLimit(newFile.supportFileType, newFile.size)) @@ -257,7 +257,7 @@ export const useFile = (fileConfig: FileUpload) => { const handleLocalFileUpload = useCallback((file: File) => { 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 } const allowedFileTypes = fileConfig.allowed_file_types diff --git a/web/app/components/base/file-uploader/utils.spec.ts b/web/app/components/base/file-uploader/utils.spec.ts index c8cf9fbe74..4a3408ef00 100644 --- a/web/app/components/base/file-uploader/utils.spec.ts +++ b/web/app/components/base/file-uploader/utils.spec.ts @@ -22,7 +22,7 @@ import { FILE_EXTS } from '../prompt-editor/constants' jest.mock('mime', () => ({ __esModule: true, default: { - getExtension: jest.fn(), + getAllExtensions: jest.fn(), }, })) @@ -58,12 +58,27 @@ describe('file-uploader utils', () => { describe('getFileExtension', () => { 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') }) + 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', () => { - jest.mocked(mime.getExtension).mockReturnValue(null) + jest.mocked(mime.getAllExtensions).mockReturnValue(null) expect(getFileExtension('file.txt', '')).toBe('txt') expect(getFileExtension('file.txt.docx', '')).toBe('docx') expect(getFileExtension('file', '')).toBe('') @@ -76,157 +91,157 @@ describe('file-uploader utils', () => { describe('getFileAppearanceType', () => { 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')) .toBe(FileAppearanceTypeEnum.gif) }) 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')) .toBe(FileAppearanceTypeEnum.image) - jest.mocked(mime.getExtension).mockReturnValue('jpeg') + jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['jpeg'])) expect(getFileAppearanceType('image.jpeg', 'image/jpeg')) .toBe(FileAppearanceTypeEnum.image) - jest.mocked(mime.getExtension).mockReturnValue('png') + jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['png'])) expect(getFileAppearanceType('image.png', 'image/png')) .toBe(FileAppearanceTypeEnum.image) - jest.mocked(mime.getExtension).mockReturnValue('webp') + jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['webp'])) expect(getFileAppearanceType('image.webp', 'image/webp')) .toBe(FileAppearanceTypeEnum.image) - jest.mocked(mime.getExtension).mockReturnValue('svg') + jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['svg'])) expect(getFileAppearanceType('image.svg', 'image/svgxml')) .toBe(FileAppearanceTypeEnum.image) }) 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')) .toBe(FileAppearanceTypeEnum.video) - jest.mocked(mime.getExtension).mockReturnValue('mov') + jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['mov'])) expect(getFileAppearanceType('video.mov', 'video/quicktime')) .toBe(FileAppearanceTypeEnum.video) - jest.mocked(mime.getExtension).mockReturnValue('mpeg') + jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['mpeg'])) expect(getFileAppearanceType('video.mpeg', 'video/mpeg')) .toBe(FileAppearanceTypeEnum.video) - jest.mocked(mime.getExtension).mockReturnValue('webm') + jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['webm'])) expect(getFileAppearanceType('video.web', 'video/webm')) .toBe(FileAppearanceTypeEnum.video) }) 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')) .toBe(FileAppearanceTypeEnum.audio) - jest.mocked(mime.getExtension).mockReturnValue('m4a') + jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['m4a'])) expect(getFileAppearanceType('audio.m4a', 'audio/mp4')) .toBe(FileAppearanceTypeEnum.audio) - jest.mocked(mime.getExtension).mockReturnValue('wav') + jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['wav'])) expect(getFileAppearanceType('audio.wav', 'audio/vnd.wav')) .toBe(FileAppearanceTypeEnum.audio) - jest.mocked(mime.getExtension).mockReturnValue('amr') + jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['amr'])) expect(getFileAppearanceType('audio.amr', 'audio/AMR')) .toBe(FileAppearanceTypeEnum.audio) - jest.mocked(mime.getExtension).mockReturnValue('mpga') + jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['mpga'])) expect(getFileAppearanceType('audio.mpga', 'audio/mpeg')) .toBe(FileAppearanceTypeEnum.audio) }) 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')) .toBe(FileAppearanceTypeEnum.code) }) 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')) .toBe(FileAppearanceTypeEnum.pdf) }) 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')) .toBe(FileAppearanceTypeEnum.markdown) - jest.mocked(mime.getExtension).mockReturnValue('markdown') + jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['markdown'])) expect(getFileAppearanceType('file.markdown', 'text/markdown')) .toBe(FileAppearanceTypeEnum.markdown) - jest.mocked(mime.getExtension).mockReturnValue('mdx') + jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['mdx'])) expect(getFileAppearanceType('file.mdx', 'text/mdx')) .toBe(FileAppearanceTypeEnum.markdown) }) 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')) .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')) .toBe(FileAppearanceTypeEnum.excel) }) 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')) .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')) .toBe(FileAppearanceTypeEnum.word) }) 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')) .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')) .toBe(FileAppearanceTypeEnum.ppt) }) 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')) .toBe(FileAppearanceTypeEnum.document) - jest.mocked(mime.getExtension).mockReturnValue('csv') + jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['csv'])) expect(getFileAppearanceType('file.csv', 'text/csv')) .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')) .toBe(FileAppearanceTypeEnum.document) - jest.mocked(mime.getExtension).mockReturnValue('eml') + jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['eml'])) expect(getFileAppearanceType('file.eml', 'message/rfc822')) .toBe(FileAppearanceTypeEnum.document) - jest.mocked(mime.getExtension).mockReturnValue('xml') + jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['xml'])) expect(getFileAppearanceType('file.xml', 'application/rssxml')) .toBe(FileAppearanceTypeEnum.document) - jest.mocked(mime.getExtension).mockReturnValue('epub') + jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['epub'])) expect(getFileAppearanceType('file.epub', 'application/epubzip')) .toBe(FileAppearanceTypeEnum.document) }) it('should handle null mime extension', () => { - jest.mocked(mime.getExtension).mockReturnValue(null) + jest.mocked(mime.getAllExtensions).mockReturnValue(null) expect(getFileAppearanceType('file.txt', 'text/plain')) .toBe(FileAppearanceTypeEnum.document) }) @@ -360,7 +375,7 @@ describe('file-uploader utils', () => { describe('isAllowedFileExtension', () => { it('should validate allowed file extensions', () => { - jest.mocked(mime.getExtension).mockReturnValue('pdf') + jest.mocked(mime.getAllExtensions).mockReturnValue(new Set(['pdf'])) expect(isAllowedFileExtension( 'test.pdf', 'application/pdf', diff --git a/web/app/components/base/file-uploader/utils.ts b/web/app/components/base/file-uploader/utils.ts index e05c0b2087..9b5a449481 100644 --- a/web/app/components/base/file-uploader/utils.ts +++ b/web/app/components/base/file-uploader/utils.ts @@ -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) => { let extension = '' - if (fileMimetype) - extension = mime.getExtension(fileMimetype) || '' + let extensions = new Set<string>() + 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 fileNamePairLength = fileNamePair.length - if (fileNamePairLength > 1) - extension = fileNamePair[fileNamePairLength - 1] + if (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 - extension = '' + extension = extensionInFileName } if (isRemote) diff --git a/web/app/components/base/markdown-blocks/button.tsx b/web/app/components/base/markdown-blocks/button.tsx index 81a3f30660..4646b12921 100644 --- a/web/app/components/base/markdown-blocks/button.tsx +++ b/web/app/components/base/markdown-blocks/button.tsx @@ -1,7 +1,7 @@ import { useChatContext } from '@/app/components/base/chat/chat/context' import Button from '@/app/components/base/button' import cn from '@/utils/classnames' - +import { isValidUrl } from './utils' const MarkdownButton = ({ node }: any) => { const { onSend } = useChatContext() const variant = node.properties.dataVariant @@ -9,25 +9,17 @@ const MarkdownButton = ({ node }: any) => { const link = node.properties.dataLink 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 variant={variant} size={size} className={cn('!h-auto min-h-8 select-none whitespace-normal !px-3')} onClick={() => { - if (is_valid_url(link)) { + if (isValidUrl(link)) { window.open(link, '_blank') return } + if(!message) + return onSend?.(message) }} > diff --git a/web/app/components/base/markdown-blocks/link.tsx b/web/app/components/base/markdown-blocks/link.tsx index b243a525a0..c465b3e4f8 100644 --- a/web/app/components/base/markdown-blocks/link.tsx +++ b/web/app/components/base/markdown-blocks/link.tsx @@ -5,6 +5,7 @@ */ import React from 'react' import { useChatContext } from '@/app/components/base/chat/chat/context' +import { isValidUrl } from './utils' const Link = ({ node, children, ...props }: any) => { 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> } 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> } } diff --git a/web/app/components/base/markdown-blocks/utils.ts b/web/app/components/base/markdown-blocks/utils.ts new file mode 100644 index 0000000000..4e9e98dbed --- /dev/null +++ b/web/app/components/base/markdown-blocks/utils.ts @@ -0,0 +1,3 @@ +export const isValidUrl = (url: string): boolean => { + return ['http:', 'https:', '//', 'mailto:'].some(prefix => url.startsWith(prefix)) +} diff --git a/web/app/components/base/markdown/index.tsx b/web/app/components/base/markdown/index.tsx index 0e0dc41cf2..1e50e6745b 100644 --- a/web/app/components/base/markdown/index.tsx +++ b/web/app/components/base/markdown/index.tsx @@ -7,7 +7,7 @@ import RemarkGfm from 'remark-gfm' import RehypeRaw from 'rehype-raw' import { flow } from 'lodash-es' import cn from '@/utils/classnames' -import { preprocessLaTeX, preprocessThinkTag } from './markdown-utils' +import { customUrlTransform, preprocessLaTeX, preprocessThinkTag } from './markdown-utils' import { AudioBlock, 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 || [])]} components={{ code: CodeBlock, diff --git a/web/app/components/base/markdown/markdown-utils.ts b/web/app/components/base/markdown/markdown-utils.ts index d77b2ddccf..0aa385a1d1 100644 --- a/web/app/components/base/markdown/markdown-utils.ts +++ b/web/app/components/base/markdown/markdown-utils.ts @@ -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'), ])(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 +} diff --git a/web/app/components/base/mermaid/index.tsx b/web/app/components/base/mermaid/index.tsx index a0332ce819..31eaffb813 100644 --- a/web/app/components/base/mermaid/index.tsx +++ b/web/app/components/base/mermaid/index.tsx @@ -487,15 +487,15 @@ const Flowchart = React.forwardRef((props: { 'bg-white': currentTheme === Theme.light, '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-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-400': currentTheme === Theme.dark, }), - errorIcon: cn('w-6 h-6', { + errorIcon: cn('h-6 w-6', { 'text-red-500': currentTheme === Theme.light, 'text-red-400': currentTheme === Theme.dark, }), @@ -503,7 +503,7 @@ const Flowchart = React.forwardRef((props: { 'text-gray-700': currentTheme === Theme.light, '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-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 const getLookButtonClass = (lookType: 'classic' | 'handDrawn') => { 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', 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', @@ -523,7 +523,7 @@ const Flowchart = React.forwardRef((props: { <div ref={ref as React.RefObject<HTMLDivElement>} className={themeClasses.container}> <div className={themeClasses.segmented}> <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 key='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' }} /> {isLoading && !svgCode && ( - <div className='py-4 px-[26px]'> + <div className='px-[26px] py-4'> <LoadingAnim type='text'/> {!isCodeComplete && ( <div className="mt-2 text-sm text-gray-500"> @@ -557,7 +557,7 @@ const Flowchart = React.forwardRef((props: { {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 onClick={(e) => { e.stopPropagation() diff --git a/web/app/components/base/radio/component/radio/index.tsx b/web/app/components/base/radio/component/radio/index.tsx index 7788226484..aa4e6d0c7f 100644 --- a/web/app/components/base/radio/component/radio/index.tsx +++ b/web/app/components/base/radio/component/radio/index.tsx @@ -38,7 +38,7 @@ export default function Radio({ const divClassName = ` flex items-center py-1 relative px-7 cursor-pointer text-text-secondary rounded - bg-components-option-card-option-bg hover:bg-components-option-card-option-bg-hover hover:shadow-xs + hover:bg-components-option-card-option-bg-hover hover:shadow-xs ` return ( diff --git a/web/app/components/develop/template/template.ja.mdx b/web/app/components/develop/template/template.ja.mdx index 5380d4da91..fc6291f522 100755 --- a/web/app/components/develop/template/template.ja.mdx +++ b/web/app/components/develop/template/template.ja.mdx @@ -3,10 +3,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from # Completion アプリ API -テキスト生成アプリケーションはセッションレスをサポートし、翻訳、記事作成、要約AI等に最適です。 +テキスト生成アプリケーションはセッションレスをサポートし、翻訳、記事作成、要約 AI 等に最適です。 <div> - ### ベースURL + ### ベース URL <CodeGroup title="Code" targetCode={props.appDetail.api_base_url}> ```javascript ``` @@ -14,10 +14,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from ### 認証 - サービスAPIは`API-Key`認証を使用します。 - <i>**APIキーの漏洩による重大な結果を避けるため、APIキーはサーバーサイドに保存し、クライアントサイドでは共有や保存しないことを強く推奨します。**</i> + サービス API は `API-Key` 認証を使用します。 + <i>**API キーの漏洩による重大な結果を避けるため、API キーはサーバーサイドに保存し、クライアントサイドでは共有や保存しないことを強く推奨します。**</i> - すべてのAPIリクエストで、以下のように`Authorization` HTTPヘッダーにAPIキーを含めてください: + すべての API リクエストで、以下のように `Authorization` HTTP ヘッダーに API キーを含めてください: <CodeGroup title="Code"> ```javascript @@ -212,7 +212,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from <Row> <Col> メッセージ送信時に使用するファイル(現在は画像のみ対応)をアップロードし、画像とテキストのマルチモーダルな理解を可能にします。 - png、jpg、jpeg、webp、gif形式に対応しています。 + png、jpg、jpeg、webp、gif 形式に対応しています。 <i>アップロードされたファイルは、現在のエンドユーザーのみが使用できます。</i> ### リクエストボディ @@ -223,25 +223,25 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from 開発者のルールで定義されたユーザー識別子。アプリケーション内で一意である必要があります。 ### レスポンス - アップロードが成功すると、サーバーはファイルのIDと関連情報を返します。 + アップロードが成功すると、サーバーはファイルの ID と関連情報を返します。 - `id` (uuid) ID - `name` (string) ファイル名 - `size` (int) ファイルサイズ(バイト) - `extension` (string) ファイル拡張子 - - `mime_type` (string) ファイルのMIMEタイプ + - `mime_type` (string) ファイルの MIME タイプ - `created_by` (uuid) エンドユーザーID - `created_at` (timestamp) 作成タイムスタンプ、例:1705395332 ### エラー - 400, `no_file_uploaded`, ファイルを提供する必要があります - - 400, `too_many_files`, 現在は1つのファイルのみ受け付けています + - 400, `too_many_files`, 現在は 1 つのファイルのみ受け付けています - 400, `unsupported_preview`, ファイルがプレビューに対応していません - 400, `unsupported_estimate`, ファイルが推定に対応していません - 413, `file_too_large`, ファイルが大きすぎます - 415, `unsupported_file_type`, サポートされていない拡張子です。現在はドキュメントファイルのみ受け付けています - - 503, `s3_connection_failed`, S3サービスに接続できません - - 503, `s3_permission_denied`, S3へのファイルアップロード権限がありません - - 503, `s3_file_too_large`, ファイルがS3のサイズ制限を超えています + - 503, `s3_connection_failed`, S3 サービスに接続できません + - 503, `s3_permission_denied`, S3 へのファイルアップロード権限がありません + - 503, `s3_file_too_large`, ファイルが S3 のサイズ制限を超えています - 500, 内部サーバーエラー </Col> @@ -286,7 +286,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from <Col> ストリーミングモードでのみサポートされています。 ### パス - - `task_id` (string) タスクID、ストリーミングチャンクの返信から取得可能 + - `task_id` (string) タスク ID、ストリーミングチャンクの返信から取得可能 リクエストボディ - `user` (string) 必須 ユーザー識別子。エンドユーザーの身元を定義するために使用され、メッセージ送信インターフェースで渡されたユーザーと一致する必要があります。 @@ -655,22 +655,22 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from /> <Row> <Col> - アプリのWebApp設定を取得するために使用します。 + アプリの WebApp 設定を取得するために使用します。 ### レスポンス - - `title` (string) WebApp名 - - `chat_color_theme` (string) チャットの色テーマ、16進数形式 + - `title` (string) WebApp 名 + - `chat_color_theme` (string) チャットの色テーマ、16 進数形式 - `chat_color_theme_inverted` (bool) チャットの色テーマを反転するかどうか - `icon_type` (string) アイコンタイプ、`emoji`-絵文字、`image`-画像 - - `icon` (string) アイコン。`emoji`タイプの場合は絵文字、`image`タイプの場合は画像URL - - `icon_background` (string) 16進数形式の背景色 - - `icon_url` (string) アイコンのURL + - `icon` (string) アイコン。`emoji`タイプの場合は絵文字、`image`タイプの場合は画像 URL + - `icon_background` (string) 16 進数形式の背景色 + - `icon_url` (string) アイコンの URL - `description` (string) 説明 - `copyright` (string) 著作権情報 - `privacy_policy` (string) プライバシーポリシーのリンク - `custom_disclaimer` (string) カスタム免責事項 - `default_language` (string) デフォルト言語 - `show_workflow_steps` (bool) ワークフローの詳細を表示するかどうか - - `use_icon_as_answer_icon` (bool) WebAppのアイコンをチャット内の🤖に置き換えるかどうか + - `use_icon_as_answer_icon` (bool) WebApp のアイコンをチャット内の🤖に置き換えるかどうか </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}'`}> diff --git a/web/app/components/develop/template/template.zh.mdx b/web/app/components/develop/template/template.zh.mdx index 69d955b11f..9e65a4bd9b 100755 --- a/web/app/components/develop/template/template.zh.mdx +++ b/web/app/components/develop/template/template.zh.mdx @@ -60,7 +60,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' <Property name='files' type='array[object]' key='files'> 上传的文件。 - `type` (string) 支持类型:图片 `image`(目前仅支持图片格式) 。 - - `transfer_method` (string) 传递方式: + - `transfer_method` (string) 传递方式: - `remote_url`: 图片地址。 - `local_file`: 上传文件。 - `url` 图片地址。(仅当传递方式为 `remote_url` 时)。 @@ -622,10 +622,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' 用于获取应用的 WebApp 设置 ### Response - `title` (string) WebApp 名称 - - `chat_color_theme` (string) 聊天颜色主题, hex 格式 + - `chat_color_theme` (string) 聊天颜色主题,hex 格式 - `chat_color_theme_inverted` (bool) 聊天颜色主题是否反转 - - `icon_type` (string) 图标类型, `emoji`-表情, `image`-图片 - - `icon` (string) 图标, 如果是 `emoji` 类型, 则是 emoji 表情符号, 如果是 `image` 类型, 则是图片 URL + - `icon_type` (string) 图标类型,`emoji`-表情,`image`-图片 + - `icon` (string) 图标,如果是 `emoji` 类型,则是 emoji 表情符号,如果是 `image` 类型,则是图片 URL - `icon_background` (string) hex 格式的背景色 - `icon_url` (string) 图标 URL - `description` (string) 描述 @@ -879,10 +879,10 @@ ___ 动作,只能是 'enable' 或 'disable' </Property> <Property name='embedding_provider_name' type='string' key='embedding_provider_name'> - 指定的嵌入模型提供商, 必须先在系统内设定好接入的模型,对应的是provider字段 + 指定的嵌入模型提供商,必须先在系统内设定好接入的模型,对应的是 provider 字段 </Property> <Property name='embedding_model_name' type='string' key='embedding_model_name'> - 指定的嵌入模型,对应的是model字段 + 指定的嵌入模型,对应的是 model 字段 </Property> <Property name='score_threshold' type='number' key='score_threshold'> 相似度阈值,当相似度大于该阈值时,系统会自动回复,否则不回复 @@ -890,8 +890,8 @@ ___ </Properties> </Col> <Col sticky> - 嵌入模型的提供商和模型名称可以通过以下接口获取:v1/workspaces/current/models/model-types/text-embedding, 具体见:通过 API 维护知识库。 使用的Authorization是Dataset的API Token。 - 该接口是异步执行,所以会返回一个job_id,通过查询job状态接口可以获取到最终的执行结果。 + 嵌入模型的提供商和模型名称可以通过以下接口获取:v1/workspaces/current/models/model-types/text-embedding,具体见:通过 API 维护知识库。使用的 Authorization 是 Dataset 的 API Token。 + 该接口是异步执行,所以会返回一个 job_id,通过查询 job 状态接口可以获取到最终的执行结果。 <CodeGroup title="Request" tag="POST" diff --git a/web/app/components/develop/template/template_advanced_chat.ja.mdx b/web/app/components/develop/template/template_advanced_chat.ja.mdx index fe8d5929c7..6f94cbb2c7 100644 --- a/web/app/components/develop/template/template_advanced_chat.ja.mdx +++ b/web/app/components/develop/template/template_advanced_chat.ja.mdx @@ -1,12 +1,12 @@ import { CodeGroup } from '../code.tsx' import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx' -# 高度なチャットアプリAPI +# 高度なチャットアプリ API -チャットアプリケーションはセッションの持続性をサポートしており、以前のチャット履歴を応答のコンテキストとして使用できます。これは、チャットボットやカスタマーサービスAIなどに適用できます。 +チャットアプリケーションはセッションの持続性をサポートしており、以前のチャット履歴を応答のコンテキストとして使用できます。これは、チャットボットやカスタマーサービス AI などに適用できます。 <div> - ### ベースURL + ### ベース URL <CodeGroup title="コード" targetCode={props.appDetail.api_base_url}> ```javascript ``` @@ -14,10 +14,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from ### 認証 - サービスAPIは`API-Key`認証を使用します。 - <i>**APIキーはサーバー側に保存し、クライアント側で共有または保存しないことを強くお勧めします。APIキーの漏洩は深刻な結果を招く可能性があります。**</i> + サービス API は `API-Key` 認証を使用します。 + <i>**API キーはサーバー側に保存し、クライアント側で共有または保存しないことを強くお勧めします。API キーの漏洩は深刻な結果を招く可能性があります。**</i> - すべてのAPIリクエストには、以下のように`Authorization`HTTPヘッダーにAPIキーを含めてください: + すべての API リクエストには、以下のように `Authorization`HTTP ヘッダーに API キーを含めてください: <CodeGroup title="コード"> ```javascript @@ -327,25 +327,25 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from ユーザー識別子、開発者のルールによって定義され、アプリケーション内で一意でなければなりません。 ### 応答 - アップロードが成功すると、サーバーはファイルのIDと関連情報を返します。 + アップロードが成功すると、サーバーはファイルの ID と関連情報を返します。 - `id` (uuid) ID - `name` (string) ファイル名 - `size` (int) ファイルサイズ(バイト) - `extension` (string) ファイル拡張子 - - `mime_type` (string) ファイルのMIMEタイプ + - `mime_type` (string) ファイルの MIME タイプ - `created_by` (uuid) エンドユーザーID - `created_at` (timestamp) 作成タイムスタンプ、例:1705395332 ### エラー - 400, `no_file_uploaded`, ファイルが提供されなければなりません - - 400, `too_many_files`, 現在は1つのファイルのみ受け付けます + - 400, `too_many_files`, 現在は 1 つのファイルのみ受け付けます - 400, `unsupported_preview`, ファイルはプレビューをサポートしていません - 400, `unsupported_estimate`, ファイルは推定をサポートしていません - 413, `file_too_large`, ファイルが大きすぎます - 415, `unsupported_file_type`, サポートされていない拡張子、現在はドキュメントファイルのみ受け付けます - - 503, `s3_connection_failed`, S3サービスに接続できません - - 503, `s3_permission_denied`, S3にファイルをアップロードする権限がありません - - 503, `s3_file_too_large`, ファイルがS3のサイズ制限を超えています + - 503, `s3_connection_failed`, S3 サービスに接続できません + - 503, `s3_permission_denied`, S3 にファイルをアップロードする権限がありません + - 503, `s3_file_too_large`, ファイルが S3 のサイズ制限を超えています - 500, 内部サーバーエラー @@ -391,7 +391,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from <Col> ストリーミングモードでのみサポートされています。 ### パス - - `task_id` (string) タスクID、ストリーミングチャンクの返り値から取得できます + - `task_id` (string) タスク ID、ストリーミングチャンクの返り値から取得できます ### リクエストボディ - `user` (string) 必須 ユーザー識別子、エンドユーザーの身元を定義するために使用され、送信メッセージインターフェースで渡されたユーザーと一致している必要があります。 @@ -712,7 +712,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from /> <Row> <Col> - 現在のユーザーの会話リストを取得し、デフォルトで最新の20件を返します。 + 現在のユーザーの会話リストを取得し、デフォルトで最新の 20 件を返します。 ### クエリ @@ -943,7 +943,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `limit` (int) ページごとのアイテム数 - `has_more` (bool) さらにアイテムがあるかどうか - `data` (array[object]) 変数のリスト - - `id` (string) 変数ID + - `id` (string) 変数 ID - `name` (string) 変数名 - `value_type` (string) 変数タイプ(文字列、数値、真偽値など) - `value` (string) 変数値 @@ -1014,7 +1014,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from /> <Row> <Col> - このエンドポイントはmultipart/form-dataリクエストを必要とします。 + このエンドポイントは multipart/form-data リクエストを必要とします。 ### リクエストボディ @@ -1288,9 +1288,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `tool_name` (string) - `icon` (object|string) - (object) アイコンオブジェクト - - `background` (string) 背景色(16進数形式) + - `background` (string) 背景色(16 進数形式) - `content`(string) 絵文字 - - (string) アイコンのURL + - (string) アイコンの URL </Col> <Col> <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> <Col> - アプリのWebApp設定を取得するために使用します。 + アプリの WebApp 設定を取得するために使用します。 ### 応答 - - `title` (string) WebApp名 - - `chat_color_theme` (string) チャットの色テーマ、16進数形式 + - `title` (string) WebApp 名 + - `chat_color_theme` (string) チャットの色テーマ、16 進数形式 - `chat_color_theme_inverted` (bool) チャットの色テーマを反転するかどうか - `icon_type` (string) アイコンタイプ、`emoji`-絵文字、`image`-画像 - - `icon` (string) アイコン。`emoji`タイプの場合は絵文字、`image`タイプの場合は画像URL - - `icon_background` (string) 16進数形式の背景色 - - `icon_url` (string) アイコンのURL + - `icon` (string) アイコン。`emoji`タイプの場合は絵文字、`image`タイプの場合は画像 URL + - `icon_background` (string) 16 進数形式の背景色 + - `icon_url` (string) アイコンの URL - `description` (string) 説明 - `copyright` (string) 著作権情報 - `privacy_policy` (string) プライバシーポリシーのリンク - `custom_disclaimer` (string) カスタム免責事項 - `default_language` (string) デフォルト言語 - `show_workflow_steps` (bool) ワークフローの詳細を表示するかどうか - - `use_icon_as_answer_icon` (bool) WebAppのアイコンをチャット内の🤖に置き換えるかどうか + - `use_icon_as_answer_icon` (bool) WebApp のアイコンをチャット内の🤖に置き換えるかどうか </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}'`}> diff --git a/web/app/components/develop/template/template_advanced_chat.zh.mdx b/web/app/components/develop/template/template_advanced_chat.zh.mdx index 828b8d1f68..3e268d6e65 100755 --- a/web/app/components/develop/template/template_advanced_chat.zh.mdx +++ b/web/app/components/develop/template/template_advanced_chat.zh.mdx @@ -981,7 +981,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' - `limit` (int) 每页项目数 - `has_more` (bool) 是否有更多项目 - `data` (array[object]) 变量列表 - - `id` (string) 变量ID + - `id` (string) 变量 ID - `name` (string) 变量名称 - `value_type` (string) 变量类型(字符串、数字、布尔等) - `value` (string) 变量值 @@ -1300,15 +1300,15 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' /> <Row> <Col> - 用于获取工具icon + 用于获取工具 icon ### Response - `tool_icons`(object[string]) 工具图标 - `工具名称` (string) - `icon` (object|string) - (object) 图标 - - `background` (string) hex格式的背景色 + - `background` (string) hex 格式的背景色 - `content`(string) emoji - - (string) 图标URL + - (string) 图标 URL </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}'`}> @@ -1347,10 +1347,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' 用于获取应用的 WebApp 设置 ### Response - `title` (string) WebApp 名称 - - `chat_color_theme` (string) 聊天颜色主题, hex 格式 + - `chat_color_theme` (string) 聊天颜色主题,hex 格式 - `chat_color_theme_inverted` (bool) 聊天颜色主题是否反转 - - `icon_type` (string) 图标类型, `emoji`-表情, `image`-图片 - - `icon` (string) 图标, 如果是 `emoji` 类型, 则是 emoji 表情符号, 如果是 `image` 类型, 则是图片 URL + - `icon_type` (string) 图标类型,`emoji`-表情,`image`-图片 + - `icon` (string) 图标,如果是 `emoji` 类型,则是 emoji 表情符号,如果是 `image` 类型,则是图片 URL - `icon_background` (string) hex 格式的背景色 - `icon_url` (string) 图标 URL - `description` (string) 描述 @@ -1604,10 +1604,10 @@ ___ 动作,只能是 'enable' 或 'disable' </Property> <Property name='embedding_provider_name' type='string' key='embedding_provider_name'> - 指定的嵌入模型提供商, 必须先在系统内设定好接入的模型,对应的是provider字段 + 指定的嵌入模型提供商,必须先在系统内设定好接入的模型,对应的是 provider 字段 </Property> <Property name='embedding_model_name' type='string' key='embedding_model_name'> - 指定的嵌入模型,对应的是model字段 + 指定的嵌入模型,对应的是 model 字段 </Property> <Property name='score_threshold' type='number' key='score_threshold'> 相似度阈值,当相似度大于该阈值时,系统会自动回复,否则不回复 @@ -1615,7 +1615,7 @@ ___ </Properties> </Col> <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 title="Request" tag="POST" diff --git a/web/app/components/develop/template/template_chat.ja.mdx b/web/app/components/develop/template/template_chat.ja.mdx index 52154feb22..a1b2ae5f1d 100644 --- a/web/app/components/develop/template/template_chat.ja.mdx +++ b/web/app/components/develop/template/template_chat.ja.mdx @@ -1,12 +1,12 @@ import { CodeGroup } from '../code.tsx' import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx' -# チャットアプリAPI +# チャットアプリ API -チャットアプリケーションはセッションの持続性をサポートしており、以前のチャット履歴を応答のコンテキストとして使用できます。これは、チャットボットやカスタマーサービスAIなどに適用できます。 +チャットアプリケーションはセッションの持続性をサポートしており、以前のチャット履歴を応答のコンテキストとして使用できます。これは、チャットボットやカスタマーサービス AI などに適用できます。 <div> - ### ベースURL + ### ベース URL <CodeGroup title="コード" targetCode={props.appDetail.api_base_url}> ```javascript ``` @@ -14,10 +14,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from ### 認証 - サービスAPIは`API-Key`認証を使用します。 - <i>**APIキーの漏洩を防ぐため、APIキーはクライアント側で共有または保存せず、サーバー側で保存することを強くお勧めします。**</i> + サービス API は `API-Key` 認証を使用します。 + <i>**API キーの漏洩を防ぐため、API キーはクライアント側で共有または保存せず、サーバー側で保存することを強くお勧めします。**</i> - すべてのAPIリクエストにおいて、以下のように`Authorization`HTTPヘッダーにAPIキーを含めてください: + すべての API リクエストにおいて、以下のように `Authorization`HTTP ヘッダーに API キーを含めてください: <CodeGroup title="コード"> ```javascript @@ -279,7 +279,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from <Row> <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 - `name` (string) ファイル名 - `size` (int) ファイルサイズ(バイト) - `extension` (string) ファイル拡張子 - - `mime_type` (string) ファイルのMIMEタイプ + - `mime_type` (string) ファイルの MIME タイプ - `created_by` (uuid) エンドユーザーID - `created_at` (timestamp) 作成タイムスタンプ、例:1705395332 ### エラー - 400, `no_file_uploaded`, ファイルが提供されなければなりません - - 400, `too_many_files`, 現在は1つのファイルのみ受け付けます + - 400, `too_many_files`, 現在は 1 つのファイルのみ受け付けます - 400, `unsupported_preview`, ファイルはプレビューをサポートしていません - 400, `unsupported_estimate`, ファイルは推定をサポートしていません - 413, `file_too_large`, ファイルが大きすぎます - 415, `unsupported_file_type`, サポートされていない拡張子、現在はドキュメントファイルのみ受け付けます - - 503, `s3_connection_failed`, S3サービスに接続できません - - 503, `s3_permission_denied`, S3にファイルをアップロードする権限がありません - - 503, `s3_file_too_large`, ファイルがS3のサイズ制限を超えています + - 503, `s3_connection_failed`, S3 サービスに接続できません + - 503, `s3_permission_denied`, S3 にファイルをアップロードする権限がありません + - 503, `s3_file_too_large`, ファイルが S3 のサイズ制限を超えています - 500, 内部サーバーエラー @@ -354,7 +354,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from <Col> ストリーミングモードでのみサポートされています。 ### パス - - `task_id` (string) タスクID、ストリーミングチャンクの返り値から取得できます + - `task_id` (string) タスク ID、ストリーミングチャンクの返り値から取得できます ### リクエストボディ - `user` (string) 必須 ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用され、メッセージ送信インターフェースで渡されたユーザーと一致している必要があります。 @@ -745,7 +745,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from /> <Row> <Col> - 現在のユーザーの会話リストを取得し、デフォルトで最新の20件を返します。 + 現在のユーザーの会話リストを取得し、デフォルトで最新の 20 件を返します。 ### クエリ @@ -975,7 +975,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `limit` (int) ページごとのアイテム数 - `has_more` (bool) さらにアイテムがあるかどうか - `data` (array[object]) 変数のリスト - - `id` (string) 変数ID + - `id` (string) 変数 ID - `name` (string) 変数名 - `value_type` (string) 変数タイプ(文字列、数値、真偽値など) - `value` (string) 変数値 @@ -1046,7 +1046,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from /> <Row> <Col> - このエンドポイントはmultipart/form-dataリクエストを必要とします。 + このエンドポイントは multipart/form-data リクエストを必要とします。 ### リクエストボディ @@ -1315,9 +1315,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `tool_name` (string) - `icon` (object|string) - (object) アイコンオブジェクト - - `background` (string) 背景色(16進数形式) + - `background` (string) 背景色(16 進数形式) - `content`(string) 絵文字 - - (string) アイコンのURL + - (string) アイコンの URL </Col> <Col> <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> <Col> - アプリのWebApp設定を取得するために使用します。 + アプリの WebApp 設定を取得するために使用します。 ### 応答 - - `title` (string) WebApp名 - - `chat_color_theme` (string) チャットの色テーマ、16進数形式 + - `title` (string) WebApp 名 + - `chat_color_theme` (string) チャットの色テーマ、16 進数形式 - `chat_color_theme_inverted` (bool) チャットの色テーマを反転するかどうか - `icon_type` (string) アイコンタイプ、`emoji`-絵文字、`image`-画像 - - `icon` (string) アイコン。`emoji`タイプの場合は絵文字、`image`タイプの場合は画像URL - - `icon_background` (string) 16進数形式の背景色 - - `icon_url` (string) アイコンのURL + - `icon` (string) アイコン。`emoji`タイプの場合は絵文字、`image`タイプの場合は画像 URL + - `icon_background` (string) 16 進数形式の背景色 + - `icon_url` (string) アイコンの URL - `description` (string) 説明 - `copyright` (string) 著作権情報 - `privacy_policy` (string) プライバシーポリシーのリンク - `custom_disclaimer` (string) カスタム免責事項 - `default_language` (string) デフォルト言語 - `show_workflow_steps` (bool) ワークフローの詳細を表示するかどうか - - `use_icon_as_answer_icon` (bool) WebAppのアイコンをチャット内の🤖に置き換えるかどうか + - `use_icon_as_answer_icon` (bool) WebApp のアイコンをチャット内の🤖に置き換えるかどうか </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}'`}> diff --git a/web/app/components/develop/template/template_chat.zh.mdx b/web/app/components/develop/template/template_chat.zh.mdx index 233e68d42f..9c1a168bf5 100644 --- a/web/app/components/develop/template/template_chat.zh.mdx +++ b/web/app/components/develop/template/template_chat.zh.mdx @@ -991,7 +991,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' - `limit` (int) 每页项目数 - `has_more` (bool) 是否有更多项目 - `data` (array[object]) 变量列表 - - `id` (string) 变量ID + - `id` (string) 变量 ID - `name` (string) 变量名称 - `value_type` (string) 变量类型(字符串、数字、布尔等) - `value` (string) 变量值 @@ -1305,15 +1305,15 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' /> <Row> <Col> - 用于获取工具icon + 用于获取工具 icon ### Response - `tool_icons`(object[string]) 工具图标 - `工具名称` (string) - `icon` (object|string) - (object) 图标 - - `background` (string) hex格式的背景色 + - `background` (string) hex 格式的背景色 - `content`(string) emoji - - (string) 图标URL + - (string) 图标 URL </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}'`}> @@ -1353,10 +1353,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' 用于获取应用的 WebApp 设置 ### Response - `title` (string) WebApp 名称 - - `chat_color_theme` (string) 聊天颜色主题, hex 格式 + - `chat_color_theme` (string) 聊天颜色主题,hex 格式 - `chat_color_theme_inverted` (bool) 聊天颜色主题是否反转 - - `icon_type` (string) 图标类型, `emoji`-表情, `image`-图片 - - `icon` (string) 图标, 如果是 `emoji` 类型, 则是 emoji 表情符号, 如果是 `image` 类型, 则是图片 URL + - `icon_type` (string) 图标类型,`emoji`-表情,`image`-图片 + - `icon` (string) 图标,如果是 `emoji` 类型,则是 emoji 表情符号,如果是 `image` 类型,则是图片 URL - `icon_background` (string) hex 格式的背景色 - `icon_url` (string) 图标 URL - `description` (string) 描述 diff --git a/web/app/components/develop/template/template_workflow.ja.mdx b/web/app/components/develop/template/template_workflow.ja.mdx index 3ab286d28a..ab53d05e81 100644 --- a/web/app/components/develop/template/template_workflow.ja.mdx +++ b/web/app/components/develop/template/template_workflow.ja.mdx @@ -1,12 +1,12 @@ import { CodeGroup } from '../code.tsx' import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx' -# ワークフローアプリAPI +# ワークフローアプリ API -ワークフローアプリケーションは、セッションをサポートせず、翻訳、記事作成、要約AIなどに最適です。 +ワークフローアプリケーションは、セッションをサポートせず、翻訳、記事作成、要約 AI などに最適です。 <div> - ### ベースURL + ### ベース URL <CodeGroup title="コード" targetCode={props.appDetail.api_base_url}> ```javascript ``` @@ -14,10 +14,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from ### 認証 - サービスAPIは`API-Key`認証を使用します。 - <i>**APIキーの漏洩を防ぐため、APIキーはクライアント側で共有または保存せず、サーバー側で保存することを強くお勧めします。**</i> + サービス API は `API-Key` 認証を使用します。 + <i>**API キーの漏洩を防ぐため、API キーはクライアント側で共有または保存せず、サーバー側で保存することを強くお勧めします。**</i> - すべてのAPIリクエストにおいて、以下のように`Authorization`HTTPヘッダーにAPIキーを含めてください: + すべての API リクエストにおいて、以下のように `Authorization`HTTP ヘッダーに API キーを含めてください: <CodeGroup title="コード"> ```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))を通じてタイプライターのような出力を実装します。 - `blocking` ブロッキングモード、実行完了後に結果を返します。(プロセスが長い場合、リクエストが中断される可能性があります) - <i>Cloudflareの制限により、100秒後に応答がない場合、リクエストは中断されます。</i> + <i>Cloudflare の制限により、100 秒後に応答がない場合、リクエストは中断されます。</i> - `user` (string) 必須 ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用されます。 アプリケーション内で開発者によって一意に定義される必要があります。 @@ -69,28 +69,28 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from ### 応答 - `response_mode`が`blocking`の場合、CompletionResponseオブジェクトを返します。 - `response_mode`が`streaming`の場合、ChunkCompletionResponseストリームを返します。 + `response_mode`が`blocking`の場合、CompletionResponse オブジェクトを返します。 + `response_mode`が`streaming`の場合、ChunkCompletionResponse ストリームを返します。 ### CompletionResponse アプリの結果を返します。`Content-Type`は`application/json`です。 - - `workflow_run_id` (string) ワークフロー実行の一意のID - - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 + - `workflow_run_id` (string) ワークフロー実行の一意の ID + - `task_id` (string) タスク ID、リクエスト追跡と以下の Stop Generate API に使用 - `data` (object) 結果の詳細 - - `id` (string) ワークフロー実行のID - - `workflow_id` (string) 関連するワークフローのID + - `id` (string) ワークフロー実行の ID + - `workflow_id` (string) 関連するワークフローの ID - `status` (string) 実行のステータス、`running` / `succeeded` / `failed` / `stopped` - `outputs` (json) オプションの出力内容 - `error` (string) オプションのエラー理由 - `elapsed_time` (float) オプションの使用時間(秒) - `total_tokens` (int) オプションの使用トークン数 - - `total_steps` (int) デフォルト0 + - `total_steps` (int) デフォルト 0 - `created_at` (timestamp) 開始時間 - `finished_at` (timestamp) 終了時間 ### ChunkCompletionResponse アプリによって出力されたストリームチャンクを返します。`Content-Type`は`text/event-stream`です。 - 各ストリーミングチャンクは`data:`で始まり、2つの改行文字`\n\n`で区切られます。以下のように表示されます: + 各ストリーミングチャンクは`data:`で始まり、2 つの改行文字`\n\n`で区切られます。以下のように表示されます: <CodeGroup> ```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 @@ -98,45 +98,45 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from </CodeGroup> ストリーミングチャンクの構造は`event`に応じて異なります: - `event: workflow_started` ワークフローが実行を開始 - - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 - - `workflow_run_id` (string) ワークフロー実行の一意のID + - `task_id` (string) タスク ID、リクエスト追跡と以下の Stop Generate API に使用 + - `workflow_run_id` (string) ワークフロー実行の一意の ID - `event` (string) `workflow_started`に固定 - `data` (object) 詳細 - - `id` (string) ワークフロー実行の一意のID - - `workflow_id` (string) 関連するワークフローのID - - `sequence_number` (int) 自己増加シリアル番号、アプリ内で自己増加し、1から始まります + - `id` (string) ワークフロー実行の一意の ID + - `workflow_id` (string) 関連するワークフローの ID + - `sequence_number` (int) 自己増加シリアル番号、アプリ内で自己増加し、1 から始まります - `created_at` (timestamp) 作成タイムスタンプ、例:1705395332 - `event: node_started` ノード実行開始 - - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 - - `workflow_run_id` (string) ワークフロー実行の一意のID + - `task_id` (string) タスク ID、リクエスト追跡と以下の Stop Generate API に使用 + - `workflow_run_id` (string) ワークフロー実行の一意の ID - `event` (string) `node_started`に固定 - `data` (object) 詳細 - - `id` (string) ワークフロー実行の一意のID - - `node_id` (string) ノードのID + - `id` (string) ワークフロー実行の一意の ID + - `node_id` (string) ノードの ID - `node_type` (string) ノードのタイプ - `title` (string) ノードの名前 - `index` (int) 実行シーケンス番号、トレースノードシーケンスを表示するために使用 - - `predecessor_node_id` (string) オプションのプレフィックスノードID、キャンバス表示実行パスに使用 + - `predecessor_node_id` (string) オプションのプレフィックスノード ID、キャンバス表示実行パスに使用 - `inputs` (object) ノードで使用されるすべての前のノード変数の内容 - `created_at` (timestamp) 開始のタイムスタンプ、例:1705395332 - `event: text_chunk` テキストフラグメント - - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 - - `workflow_run_id` (string) ワークフロー実行の一意のID + - `task_id` (string) タスク ID、リクエスト追跡と以下の Stop Generate API に使用 + - `workflow_run_id` (string) ワークフロー実行の一意の ID - `event` (string) `text_chunk`に固定 - `data` (object) 詳細 - `text` (string) テキスト内容 - `from_variable_selector` (array) テキスト生成元パス(開発者がどのノードのどの変数から生成されたかを理解するための情報) - `event: node_finished` ノード実行終了、同じイベントで異なる状態で成功または失敗 - - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 - - `workflow_run_id` (string) ワークフロー実行の一意のID + - `task_id` (string) タスク ID、リクエスト追跡と以下の Stop Generate API に使用 + - `workflow_run_id` (string) ワークフロー実行の一意の ID - `event` (string) `node_finished`に固定 - `data` (object) 詳細 - - `id` (string) ワークフロー実行の一意のID - - `node_id` (string) ノードのID + - `id` (string) ワークフロー実行の一意の ID + - `node_id` (string) ノードの ID - `node_type` (string) ノードのタイプ - `title` (string) ノードの名前 - `index` (int) 実行シーケンス番号、トレースノードシーケンスを表示するために使用 - - `predecessor_node_id` (string) オプションのプレフィックスノードID、キャンバス表示実行パスに使用 + - `predecessor_node_id` (string) オプションのプレフィックスノード ID、キャンバス表示実行パスに使用 - `inputs` (object) ノードで使用されるすべての前のノード変数の内容 - `process_data` (json) オプションのノードプロセスデータ - `outputs` (json) オプションの出力内容 @@ -149,31 +149,31 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `currency` (string) オプション 例:`USD` / `RMB` - `created_at` (timestamp) 開始のタイムスタンプ、例:1705395332 - `event: workflow_finished` ワークフロー実行終了、同じイベントで異なる状態で成功または失敗 - - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 - - `workflow_run_id` (string) ワークフロー実行の一意のID + - `task_id` (string) タスク ID、リクエスト追跡と以下の Stop Generate API に使用 + - `workflow_run_id` (string) ワークフロー実行の一意の ID - `event` (string) `workflow_finished`に固定 - `data` (object) 詳細 - - `id` (string) ワークフロー実行のID - - `workflow_id` (string) 関連するワークフローのID + - `id` (string) ワークフロー実行の ID + - `workflow_id` (string) 関連するワークフローの ID - `status` (string) 実行のステータス、`running` / `succeeded` / `failed` / `stopped` - `outputs` (json) オプションの出力内容 - `error` (string) オプションのエラー理由 - `elapsed_time` (float) オプションの使用時間(秒) - `total_tokens` (int) オプションの使用トークン数 - - `total_steps` (int) デフォルト0 + - `total_steps` (int) デフォルト 0 - `created_at` (timestamp) 開始時間 - `finished_at` (timestamp) 終了時間 - - `event: tts_message` TTSオーディオストリームイベント、つまり音声合成出力。内容はMp3形式のオーディオブロックで、base64文字列としてエンコードされています。再生時には、base64をデコードしてプレーヤーに入力するだけです。(このメッセージは自動再生が有効な場合にのみ利用可能) - - `task_id` (string) タスクID、リクエスト追跡と以下の停止応答インターフェースに使用 - - `message_id` (string) 一意のメッセージID - - `audio` (string) 音声合成後のオーディオ、base64テキストコンテンツとしてエンコードされており、再生時にはbase64をデコードしてプレーヤーに入力するだけです + - `event: tts_message` TTS オーディオストリームイベント、つまり音声合成出力。内容は Mp3 形式のオーディオブロックで、base64 文字列としてエンコードされています。再生時には、base64 をデコードしてプレーヤーに入力するだけです。(このメッセージは自動再生が有効な場合にのみ利用可能) + - `task_id` (string) タスク ID、リクエスト追跡と以下の停止応答インターフェースに使用 + - `message_id` (string) 一意のメッセージ ID + - `audio` (string) 音声合成後のオーディオ、base64 テキストコンテンツとしてエンコードされており、再生時には base64 をデコードしてプレーヤーに入力するだけです - `created_at` (int) 作成タイムスタンプ、例:1705395332 - - `event: tts_message_end` TTSオーディオストリーム終了イベント。このイベントを受信すると、オーディオストリームの終了を示します。 - - `task_id` (string) タスクID、リクエスト追跡と以下の停止応答インターフェースに使用 - - `message_id` (string) 一意のメッセージID + - `event: tts_message_end` TTS オーディオストリーム終了イベント。このイベントを受信すると、オーディオストリームの終了を示します。 + - `task_id` (string) タスク ID、リクエスト追跡と以下の停止応答インターフェースに使用 + - `message_id` (string) 一意のメッセージ ID - `audio` (string) 終了イベントにはオーディオがないため、これは空の文字列です - `created_at` (int) 作成タイムスタンプ、例:1705395332 - - `event: ping` 接続を維持するために10秒ごとに送信されるPingイベント。 + - `event: ping` 接続を維持するために 10 秒ごとに送信される Ping イベント。 ### エラー - 400, `invalid_param`, 異常なパラメータ入力 @@ -342,12 +342,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from /> <Row> <Col> - ワークフロー実行IDに基づいて、ワークフロータスクの現在の実行結果を取得します。 + ワークフロー実行 ID に基づいて、ワークフロータスクの現在の実行結果を取得します。 ### パス - `workflow_id` (string) ワークフローID、ストリーミングチャンクの返り値から取得可能 ### 応答 - - `id` (string) ワークフロー実行のID - - `workflow_id` (string) 関連するワークフローのID + - `id` (string) ワークフロー実行の ID + - `workflow_id` (string) 関連するワークフローの ID - `status` (string) 実行のステータス、`running` / `succeeded` / `failed` / `stopped` - `inputs` (json) 入力内容 - `outputs` (json) 出力内容 @@ -401,7 +401,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from <Col> ストリーミングモードでのみサポートされています。 ### パス - - `task_id` (string) タスクID、ストリーミングチャンクの返り値から取得可能 + - `task_id` (string) タスク ID、ストリーミングチャンクの返り値から取得可能 ### リクエストボディ - `user` (string) 必須 ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用され、送信メッセージインターフェースで渡されたユーザーと一致している必要があります。 @@ -454,25 +454,25 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。 ### 応答 - アップロードが成功すると、サーバーはファイルのIDと関連情報を返します。 + アップロードが成功すると、サーバーはファイルの ID と関連情報を返します。 - `id` (uuid) ID - `name` (string) ファイル名 - `size` (int) ファイルサイズ(バイト) - `extension` (string) ファイル拡張子 - - `mime_type` (string) ファイルのMIMEタイプ + - `mime_type` (string) ファイルの MIME タイプ - `created_by` (uuid) エンドユーザーID - `created_at` (timestamp) 作成タイムスタンプ、例:1705395332 ### エラー - 400, `no_file_uploaded`, ファイルが提供されていません - - 400, `too_many_files`, 現在は1つのファイルのみ受け付けています + - 400, `too_many_files`, 現在は 1 つのファイルのみ受け付けています - 400, `unsupported_preview`, ファイルはプレビューをサポートしていません - 400, `unsupported_estimate`, ファイルは推定をサポートしていません - 413, `file_too_large`, ファイルが大きすぎます - 415, `unsupported_file_type`, サポートされていない拡張子、現在はドキュメントファイルのみ受け付けています - - 503, `s3_connection_failed`, S3サービスに接続できません - - 503, `s3_permission_denied`, S3にファイルをアップロードする権限がありません - - 503, `s3_file_too_large`, ファイルがS3のサイズ制限を超えています + - 503, `s3_connection_failed`, S3 サービスに接続できません + - 503, `s3_permission_denied`, S3 にファイルをアップロードする権限がありません + - 503, `s3_file_too_large`, ファイルが S3 のサイズ制限を超えています - 500, 内部サーバーエラー @@ -550,7 +550,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `error` (string) オプションのエラー理由 - `elapsed_time` (float) 使用される総秒数 - `total_tokens` (int) 使用されるトークン数 - - `total_steps` (int) デフォルト0 + - `total_steps` (int) デフォルト 0 - `created_at` (timestamp) 開始時間 - `finished_at` (timestamp) 終了時間 - `created_from` (string) 作成元 @@ -560,7 +560,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `id` (string) ID - `type` (string) タイプ - `is_anonymous` (bool) 匿名かどうか - - `session_id` (string) セッションID + - `session_id` (string) セッション ID - `created_at` (timestamp) 作成時間 </Col> <Col sticky> @@ -750,13 +750,13 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from /> <Row> <Col> - アプリのWebApp設定を取得するために使用します。 + アプリの WebApp 設定を取得するために使用します。 ### 応答 - - `title` (string) WebApp名 + - `title` (string) WebApp 名 - `icon_type` (string) アイコンタイプ、`emoji`-絵文字、`image`-画像 - - `icon` (string) アイコン。`emoji`タイプの場合は絵文字、`image`タイプの場合は画像URL - - `icon_background` (string) 16進数形式の背景色 - - `icon_url` (string) アイコンのURL + - `icon` (string) アイコン。`emoji`タイプの場合は絵文字、`image`タイプの場合は画像 URL + - `icon_background` (string) 16 進数形式の背景色 + - `icon_url` (string) アイコンの URL - `description` (string) 説明 - `copyright` (string) 著作権情報 - `privacy_policy` (string) プライバシーポリシーのリンク diff --git a/web/app/components/develop/template/template_workflow.zh.mdx b/web/app/components/develop/template/template_workflow.zh.mdx index 17690ec3d0..fe59988eda 100644 --- a/web/app/components/develop/template/template_workflow.zh.mdx +++ b/web/app/components/develop/template/template_workflow.zh.mdx @@ -346,7 +346,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 - `total_tokens` (int) 任务执行总 tokens - `created_at` (timestamp) 任务开始时间 - `finished_at` (timestamp) 任务结束时间 - - `elapsed_time` (float) 耗时(s) + - `elapsed_time` (float) 耗时 (s) </Col> <Col sticky> ### Request Example @@ -505,7 +505,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 /> <Row> <Col> - 倒序返回workflow日志 + 倒序返回 workflow 日志 ### Query @@ -534,10 +534,10 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 - `workflow_run` (object) Workflow 执行日志 - `id` (string) 标识 - `version` (string) 版本 - - `status` (string) 执行状态, `running` / `succeeded` / `failed` / `stopped` + - `status` (string) 执行状态,`running` / `succeeded` / `failed` / `stopped` - `error` (string) (可选) 错误 - `elapsed_time` (float) 耗时,单位秒 - - `total_tokens` (int) 消耗的token数量 + - `total_tokens` (int) 消耗的 token 数量 - `total_steps` (int) 执行步骤长度 - `created_at` (timestamp) 开始时间 - `finished_at` (timestamp) 结束时间 @@ -741,8 +741,8 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 用于获取应用的 WebApp 设置 ### Response - `title` (string) WebApp 名称 - - `icon_type` (string) 图标类型, `emoji`-表情, `image`-图片 - - `icon` (string) 图标, 如果是 `emoji` 类型, 则是 emoji 表情符号, 如果是 `image` 类型, 则是图片 URL + - `icon_type` (string) 图标类型,`emoji`-表情,`image`-图片 + - `icon` (string) 图标,如果是 `emoji` 类型,则是 emoji 表情符号,如果是 `image` 类型,则是图片 URL - `icon_background` (string) hex 格式的背景色 - `icon_url` (string) 图标 URL - `description` (string) 描述 diff --git a/web/app/components/header/maintenance-notice.tsx b/web/app/components/header/maintenance-notice.tsx index 78715bb53e..f9c00dd01e 100644 --- a/web/app/components/header/maintenance-notice.tsx +++ b/web/app/components/header/maintenance-notice.tsx @@ -1,11 +1,10 @@ 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 { NOTICE_I18N } from '@/i18n/language' +import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' const MaintenanceNotice = () => { - const { locale } = useContext(I18n) + const locale = useLanguage() const [showNotice, setShowNotice] = useState(localStorage.getItem('hide-maintenance-notice') !== '1') const handleJumpNotice = () => { diff --git a/web/app/components/plugins/plugin-item/index.tsx b/web/app/components/plugins/plugin-item/index.tsx index efc635e36e..b2d5f91caa 100644 --- a/web/app/components/plugins/plugin-item/index.tsx +++ b/web/app/components/plugins/plugin-item/index.tsx @@ -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>} <Badge className='ml-1 shrink-0' 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 className='flex items-center justify-between'> diff --git a/web/app/components/share/text-generation/index.tsx b/web/app/components/share/text-generation/index.tsx index 6fd6d17278..9dc7ffcd79 100644 --- a/web/app/components/share/text-generation/index.tsx +++ b/web/app/components/share/text-generation/index.tsx @@ -9,7 +9,7 @@ import { import { useBoolean } from 'ahooks' import { usePathname, useRouter, useSearchParams } from 'next/navigation' import TabHeader from '../../base/tab-header' -import { checkOrSetAccessToken } from '../utils' +import { checkOrSetAccessToken, removeAccessToken } from '../utils' import MenuDropdown from './menu-dropdown' import RunBatch from './run-batch' import ResDownload from './run-batch/res-download' @@ -536,14 +536,31 @@ const TextGeneration: FC<IMainProps> = ({ </div> ) + const getSigninUrl = useCallback(() => { + const params = new URLSearchParams(searchParams) + params.delete('message') + params.set('redirect_url', pathname) + return `/webapp-signin?${params.toString()}` + }, [searchParams, pathname]) + + const backToHome = useCallback(() => { + removeAccessToken() + const url = getSigninUrl() + router.replace(url) + }, [getSigninUrl, router]) + if (!appId || !siteInfo || !promptConfig || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission))) { return ( <div className='flex h-screen items-center'> <Loading type='app' /> </div>) } - if (systemFeatures.webapp_auth.enabled && !userCanAccessResult?.result) - return <AppUnavailable code={403} unknownReason='no permission.' /> + if (systemFeatures.webapp_auth.enabled && !userCanAccessResult?.result) { + return <div className='flex h-full flex-col items-center justify-center gap-y-2'> + <AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' /> + {!isInstalledApp && <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span>} + </div> + } return ( <div className={cn( diff --git a/web/app/components/share/text-generation/menu-dropdown.tsx b/web/app/components/share/text-generation/menu-dropdown.tsx index 19b660b083..adb926c7ca 100644 --- a/web/app/components/share/text-generation/menu-dropdown.tsx +++ b/web/app/components/share/text-generation/menu-dropdown.tsx @@ -6,9 +6,8 @@ import type { Placement } from '@floating-ui/react' import { RiEqualizer2Line, } from '@remixicon/react' -import { useRouter } from 'next/navigation' +import { usePathname, useRouter } from 'next/navigation' import Divider from '../../base/divider' -import { removeAccessToken } from '../utils' import InfoModal from './info-modal' import ActionButton from '@/app/components/base/action-button' import { @@ -19,6 +18,8 @@ import { import ThemeSwitcher from '@/app/components/base/theme-switcher' import type { SiteInfo } from '@/models/share' import cn from '@/utils/classnames' +import { useGlobalPublicStore } from '@/context/global-public-context' +import { AccessMode } from '@/models/access-control' type Props = { data?: SiteInfo @@ -31,7 +32,9 @@ const MenuDropdown: FC<Props> = ({ placement, hideLogout, }) => { + const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode) const router = useRouter() + const pathname = usePathname() const { t } = useTranslation() const [open, doSetOpen] = useState(false) const openRef = useRef(open) @@ -45,9 +48,10 @@ const MenuDropdown: FC<Props> = ({ }, [setOpen]) const handleLogout = useCallback(() => { - removeAccessToken() - router.replace(`/webapp-signin?redirect_url=${window.location.href}`) - }, [router]) + localStorage.removeItem('token') + localStorage.removeItem('webapp_access_token') + router.replace(`/webapp-signin?redirect_url=${pathname}`) + }, [router, pathname]) 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' >{t('common.userProfile.about')}</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> </PortalToFollowElemContent> </PortalToFollowElem> diff --git a/web/app/components/share/utils.ts b/web/app/components/share/utils.ts index 9ce891a50c..8a897ab59a 100644 --- a/web/app/components/share/utils.ts +++ b/web/app/components/share/utils.ts @@ -10,8 +10,8 @@ export const getInitialTokenV2 = (): Record<string, any> => ({ version: 2, }) -export const checkOrSetAccessToken = async () => { - const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0] +export const checkOrSetAccessToken = async (appCode?: string) => { + const sharedToken = appCode || globalThis.location.pathname.split('/').slice(-1)[0] const userId = (await getProcessedSystemVariablesFromUrlParams()).user_id const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2()) let accessTokenJson = getInitialTokenV2() @@ -23,8 +23,10 @@ export const checkOrSetAccessToken = async () => { catch { } + 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], [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()) let accessTokenJson = getInitialTokenV2() try { @@ -55,21 +57,6 @@ export const setAccessToken = async (sharedToken: string, token: string, user_id } export const removeAccessToken = () => { - const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0] - - const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2()) - let accessTokenJson = getInitialTokenV2() - try { - accessTokenJson = JSON.parse(accessToken) - if (isTokenV1(accessTokenJson)) - accessTokenJson = getInitialTokenV2() - } - catch { - - } - - localStorage.removeItem(CONVERSATION_ID_INFO) - - delete accessTokenJson[sharedToken] - localStorage.setItem('token', JSON.stringify(accessTokenJson)) + localStorage.removeItem('token') + localStorage.removeItem('webapp_access_token') } diff --git a/web/app/components/workflow/nodes/code/code-parser.spec.ts b/web/app/components/workflow/nodes/code/code-parser.spec.ts index b5d28dd136..67f2c218e1 100644 --- a/web/app/components/workflow/nodes/code/code-parser.spec.ts +++ b/web/app/components/workflow/nodes/code/code-parser.spec.ts @@ -57,7 +57,7 @@ describe('extractFunctionParams', () => { }) }) - // JavaScriptのテストケース + // JavaScript のテストケース describe('JavaScript', () => { test('handles no parameters', () => { const result = extractFunctionParams(SAMPLE_CODES.javascript.noParams, CodeLanguage.javascript) @@ -180,7 +180,7 @@ function main(name, age, city) { } describe('extractReturnType', () => { - // Python3のテスト + // Python3 のテスト describe('Python3', () => { test('extracts single return value', () => { const result = extractReturnType(RETURN_TYPE_SAMPLES.python3.singleReturn, CodeLanguage.python3) @@ -247,7 +247,7 @@ describe('extractReturnType', () => { }) }) - // JavaScriptのテスト + // JavaScript のテスト describe('JavaScript', () => { test('extracts single return value', () => { const result = extractReturnType(RETURN_TYPE_SAMPLES.javascript.singleReturn, CodeLanguage.javascript) diff --git a/web/app/components/workflow/nodes/code/code-parser.ts b/web/app/components/workflow/nodes/code/code-parser.ts index 0973a01bd0..216e13eaca 100644 --- a/web/app/components/workflow/nodes/code/code-parser.ts +++ b/web/app/components/workflow/nodes/code/code-parser.ts @@ -31,7 +31,7 @@ export const extractReturnType = (code: string, language: CodeLanguage): OutputV if (returnIndex === -1) return {} - // returnから始まる部分文字列を取得 + // return から始まる部分文字列を取得 const codeAfterReturn = codeWithoutComments.slice(returnIndex) let bracketCount = 0 diff --git a/web/app/signin/LoginLogo.tsx b/web/app/signin/LoginLogo.tsx index 0753d1f98a..73dfb88205 100644 --- a/web/app/signin/LoginLogo.tsx +++ b/web/app/signin/LoginLogo.tsx @@ -1,8 +1,8 @@ 'use client' import type { FC } from 'react' import classNames from '@/utils/classnames' -import { useSelector } from '@/context/app-context' import { useGlobalPublicStore } from '@/context/global-public-context' +import { useTheme } from 'next-themes' type LoginLogoProps = { className?: string @@ -12,11 +12,7 @@ const LoginLogo: FC<LoginLogoProps> = ({ className, }) => { const { systemFeatures } = useGlobalPublicStore() - const { theme } = useSelector((s) => { - return { - theme: s.theme, - } - }) + const { theme } = useTheme() let src = theme === 'light' ? '/logo/logo-site.png' : `/logo/logo-site-${theme}.png` if (systemFeatures.branding.enabled) diff --git a/web/context/global-public-context.tsx b/web/context/global-public-context.tsx index 5aa5e7a302..26ad84be65 100644 --- a/web/context/global-public-context.tsx +++ b/web/context/global-public-context.tsx @@ -7,19 +7,24 @@ import type { SystemFeatures } from '@/types/feature' import { defaultSystemFeatures } from '@/types/feature' import { getSystemFeatures } from '@/service/common' import Loading from '@/app/components/base/loading' +import { AccessMode } from '@/models/access-control' type GlobalPublicStore = { - isPending: boolean - setIsPending: (isPending: boolean) => void + isGlobalPending: boolean + setIsGlobalPending: (isPending: boolean) => void systemFeatures: SystemFeatures setSystemFeatures: (systemFeatures: SystemFeatures) => void + webAppAccessMode: AccessMode, + setWebAppAccessMode: (webAppAccessMode: AccessMode) => void } export const useGlobalPublicStore = create<GlobalPublicStore>(set => ({ - isPending: true, - setIsPending: (isPending: boolean) => set(() => ({ isPending })), + isGlobalPending: true, + setIsGlobalPending: (isPending: boolean) => set(() => ({ isGlobalPending: isPending })), systemFeatures: defaultSystemFeatures, setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })), + webAppAccessMode: AccessMode.PUBLIC, + setWebAppAccessMode: (webAppAccessMode: AccessMode) => set(() => ({ webAppAccessMode })), })) const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({ @@ -29,7 +34,7 @@ const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({ queryKey: ['systemFeatures'], queryFn: getSystemFeatures, }) - const { setSystemFeatures, setIsPending } = useGlobalPublicStore() + const { setSystemFeatures, setIsGlobalPending: setIsPending } = useGlobalPublicStore() useEffect(() => { if (data) setSystemFeatures({ ...defaultSystemFeatures, ...data }) diff --git a/web/hooks/use-document-title.spec.ts b/web/hooks/use-document-title.spec.ts index 88239ffbdf..a8d3d56cff 100644 --- a/web/hooks/use-document-title.spec.ts +++ b/web/hooks/use-document-title.spec.ts @@ -11,7 +11,7 @@ describe('title should be empty if systemFeatures is pending', () => { act(() => { useGlobalPublicStore.setState({ systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } }, - isPending: true, + isGlobalPending: true, }) }) it('document title should be empty if set title', () => { @@ -28,7 +28,7 @@ describe('use default branding', () => { beforeEach(() => { act(() => { useGlobalPublicStore.setState({ - isPending: false, + isGlobalPending: false, systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } }, }) }) @@ -48,7 +48,7 @@ describe('use specific branding', () => { beforeEach(() => { act(() => { useGlobalPublicStore.setState({ - isPending: false, + isGlobalPending: false, systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: true, application_title: 'Test' } }, }) }) diff --git a/web/hooks/use-document-title.ts b/web/hooks/use-document-title.ts index 10275a196f..2c848a1f56 100644 --- a/web/hooks/use-document-title.ts +++ b/web/hooks/use-document-title.ts @@ -3,7 +3,7 @@ import { useGlobalPublicStore } from '@/context/global-public-context' import { useFavicon, useTitle } from 'ahooks' 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 prefix = title ? `${title} - ` : '' let titleStr = '' diff --git a/web/i18n/README.md b/web/i18n/README.md index 9384ffc519..b81ffbf4c3 100644 --- a/web/i18n/README.md +++ b/web/i18n/README.md @@ -115,13 +115,13 @@ export const languages = [ }, { value: 'ja-JP', - name: '日本語(日本)', + name: '日本語 (日本)', example: 'こんにちは、Dify!', supported: false, }, { value: 'ko-KR', - name: '한국어(대한민국)', + name: '한국어 (대한민국)', example: '안녕, Dify!', supported: true, }, diff --git a/web/i18n/de-DE/app.ts b/web/i18n/de-DE/app.ts index b9fdde58ff..1373dd611b 100644 --- a/web/i18n/de-DE/app.ts +++ b/web/i18n/de-DE/app.ts @@ -220,12 +220,14 @@ const translation = { anyone: 'Jeder kann auf die Webanwendung zugreifen.', specific: 'Nur bestimmte Gruppen oder Mitglieder können auf die Webanwendung zugreifen.', organization: 'Jeder in der Organisation kann auf die Webanwendung zugreifen.', + external: 'Nur authentifizierte externe Benutzer können auf die Webanwendung zugreifen.', }, accessControlDialog: { accessItems: { anyone: 'Jeder mit dem Link', specific: 'Spezifische Gruppen oder Mitglieder', organization: 'Nur Mitglieder innerhalb des Unternehmens', + external: 'Authentifizierte externe Benutzer', }, operateGroupAndMember: { searchPlaceholder: 'Gruppen und Mitglieder suchen', diff --git a/web/i18n/de-DE/share-app.ts b/web/i18n/de-DE/share-app.ts index 462286fa23..33c40917dd 100644 --- a/web/i18n/de-DE/share-app.ts +++ b/web/i18n/de-DE/share-app.ts @@ -77,6 +77,9 @@ const translation = { executions: '{{num}} HINRICHTUNGEN', execution: 'AUSFÜHRUNG', }, + login: { + backToHome: 'Zurück zur Startseite', + }, } export default translation diff --git a/web/i18n/en-US/app.ts b/web/i18n/en-US/app.ts index 20a80ba4cd..ccfe23ead6 100644 --- a/web/i18n/en-US/app.ts +++ b/web/i18n/en-US/app.ts @@ -197,9 +197,10 @@ const translation = { }, accessControl: 'Web App Access Control', accessItemsDescription: { - anyone: 'Anyone can access the web app', - specific: 'Only specific groups or members can access the web app', - organization: 'Anyone in the organization can access the web app', + anyone: 'Anyone can access the web app (no login required)', + specific: 'Only specific members within the platform can access the Web application', + organization: 'All members within the platform can access the Web application', + external: 'Only authenticated external users can access the Web application', }, accessControlDialog: { title: 'Web App Access Control', @@ -207,15 +208,16 @@ const translation = { accessLabel: 'Who has access', accessItems: { anyone: 'Anyone with the link', - specific: 'Specific groups or members', - organization: 'Only members within the enterprise', + specific: 'Specific members within the platform', + organization: 'All members within the platform', + external: 'Authenticated external users', }, groups_one: '{{count}} GROUP', groups_other: '{{count}} GROUPS', members_one: '{{count}} MEMBER', members_other: '{{count}} MEMBERS', 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: { searchPlaceholder: 'Search groups and members', allMembers: 'All members', diff --git a/web/i18n/en-US/education.ts b/web/i18n/en-US/education.ts index ea125a1332..dd7fedc10f 100644 --- a/web/i18n/en-US/education.ts +++ b/web/i18n/en-US/education.ts @@ -24,7 +24,7 @@ const translation = { desc: { front: 'Your information and use of Education Verified status are subject to our', and: 'and', - end: '. By submitting:', + end: '. By submitting:', termsOfService: 'Terms of Service', privacyPolicy: 'Privacy Policy', }, diff --git a/web/i18n/en-US/share-app.ts b/web/i18n/en-US/share-app.ts index bf99005d71..ab589ffb76 100644 --- a/web/i18n/en-US/share-app.ts +++ b/web/i18n/en-US/share-app.ts @@ -77,6 +77,9 @@ const translation = { atLeastOne: 'Please input at least one row in the uploaded file.', }, }, + login: { + backToHome: 'Back to Home', + }, } export default translation diff --git a/web/i18n/es-ES/app-debug.ts b/web/i18n/es-ES/app-debug.ts index ab5b82e7d1..8c986bf669 100644 --- a/web/i18n/es-ES/app-debug.ts +++ b/web/i18n/es-ES/app-debug.ts @@ -280,6 +280,7 @@ const translation = { 'inputPlaceholder': 'Por favor ingresa', 'content': 'Contenido', 'required': 'Requerido', + 'hide': 'Ocultar', 'errorMsg': { varNameRequired: 'Nombre de la variable es requerido', labelNameRequired: 'Nombre de la etiqueta es requerido', diff --git a/web/i18n/es-ES/app.ts b/web/i18n/es-ES/app.ts index c183485294..c1147720e7 100644 --- a/web/i18n/es-ES/app.ts +++ b/web/i18n/es-ES/app.ts @@ -212,12 +212,14 @@ const translation = { anyone: 'Cualquiera puede acceder a la aplicación web.', specific: 'Solo grupos o miembros específicos pueden acceder a la aplicación web', organization: 'Cualquiera en la organización puede acceder a la aplicación web', + external: 'Solo los usuarios externos autenticados pueden acceder a la aplicación web.', }, accessControlDialog: { accessItems: { anyone: 'Cualquiera con el enlace', specific: 'Grupos o miembros específicos', organization: 'Solo miembros dentro de la empresa', + external: 'Usuarios externos autenticados', }, operateGroupAndMember: { searchPlaceholder: 'Buscar grupos y miembros', diff --git a/web/i18n/es-ES/common.ts b/web/i18n/es-ES/common.ts index 22c70f6bff..82ed315f1c 100644 --- a/web/i18n/es-ES/common.ts +++ b/web/i18n/es-ES/common.ts @@ -654,6 +654,7 @@ const translation = { auto: 'sistema', light: 'luz', theme: 'Tema', + dark: 'noche', }, compliance: { iso27001: 'Certificación ISO 27001:2022', diff --git a/web/i18n/es-ES/share-app.ts b/web/i18n/es-ES/share-app.ts index 41aa35c43e..caeb056d89 100644 --- a/web/i18n/es-ES/share-app.ts +++ b/web/i18n/es-ES/share-app.ts @@ -77,6 +77,9 @@ const translation = { execution: 'EJECUCIÓN', executions: '{{num}} EJECUCIONES', }, + login: { + backToHome: 'Volver a Inicio', + }, } export default translation diff --git a/web/i18n/fa-IR/app-debug.ts b/web/i18n/fa-IR/app-debug.ts index 00891f3b17..5cf9c15efe 100644 --- a/web/i18n/fa-IR/app-debug.ts +++ b/web/i18n/fa-IR/app-debug.ts @@ -315,6 +315,7 @@ const translation = { 'inputPlaceholder': 'لطفاً وارد کنید', 'content': 'محتوا', 'required': 'مورد نیاز', + 'hide': 'مخفی کردن', 'errorMsg': { varNameRequired: 'نام متغیر مورد نیاز است', labelNameRequired: 'نام برچسب مورد نیاز است', diff --git a/web/i18n/fa-IR/app.ts b/web/i18n/fa-IR/app.ts index d37f4e8f90..13473d21f5 100644 --- a/web/i18n/fa-IR/app.ts +++ b/web/i18n/fa-IR/app.ts @@ -213,12 +213,14 @@ const translation = { specific: 'فقط گروه‌ها یا اعضای خاصی می‌توانند به اپلیکیشن وب دسترسی پیدا کنند.', anyone: 'هر کسی می‌تواند به وب‌اپلیکیشن دسترسی پیدا کند', organization: 'هر کسی در سازمان می‌تواند به اپلیکیشن وب دسترسی پیدا کند.', + external: 'تنها کاربران خارجی تأیید شده می‌توانند به برنامه وب دسترسی پیدا کنند.', }, accessControlDialog: { accessItems: { specific: 'گروه‌ها یا اعضای خاص', organization: 'فقط اعضای داخل سازمان', anyone: 'هر کسی که لینک را داشته باشد', + external: 'کاربران خارجی تأیید شده', }, operateGroupAndMember: { searchPlaceholder: 'گروه‌ها و اعضا را جستجو کنید', diff --git a/web/i18n/fa-IR/share-app.ts b/web/i18n/fa-IR/share-app.ts index bf1c0dec50..03ed4e8ea9 100644 --- a/web/i18n/fa-IR/share-app.ts +++ b/web/i18n/fa-IR/share-app.ts @@ -73,6 +73,9 @@ const translation = { executions: '{{num}} اعدام', execution: 'اجرا', }, + login: { + backToHome: 'بازگشت به خانه', + }, } export default translation diff --git a/web/i18n/fr-FR/app-debug.ts b/web/i18n/fr-FR/app-debug.ts index 2fd863742b..6671092930 100644 --- a/web/i18n/fr-FR/app-debug.ts +++ b/web/i18n/fr-FR/app-debug.ts @@ -268,6 +268,7 @@ const translation = { 'labelName': 'Label Name', 'inputPlaceholder': 'Please input', 'required': 'Required', + 'hide': 'Caché', 'errorMsg': { varNameRequired: 'Variable name is required', labelNameRequired: 'Label name is required', diff --git a/web/i18n/fr-FR/app.ts b/web/i18n/fr-FR/app.ts index ffa00c758a..5c0965815e 100644 --- a/web/i18n/fr-FR/app.ts +++ b/web/i18n/fr-FR/app.ts @@ -207,17 +207,20 @@ const translation = { modelNotSupported: 'Modèle non pris en charge', moreFillTip: 'Affichage d\'un maximum de 10 niveaux d\'imbrication', configure: 'Configurer', + structured: 'systématique', }, accessItemsDescription: { anyone: 'Tout le monde peut accéder à l\'application web.', specific: 'Seules des groupes ou membres spécifiques peuvent accéder à l\'application web.', organization: 'Toute personne dans l\'organisation peut accéder à l\'application web.', + external: 'Seuls les utilisateurs externes authentifiés peuvent accéder à l\'application Web.', }, accessControlDialog: { accessItems: { anyone: 'Quiconque avec le lien', specific: 'Groupes ou membres spécifiques', organization: 'Seuls les membres au sein de l\'entreprise', + external: 'Utilisateurs externes authentifiés', }, operateGroupAndMember: { searchPlaceholder: 'Rechercher des groupes et des membres', diff --git a/web/i18n/fr-FR/share-app.ts b/web/i18n/fr-FR/share-app.ts index d0b3a5047e..2374da70e6 100644 --- a/web/i18n/fr-FR/share-app.ts +++ b/web/i18n/fr-FR/share-app.ts @@ -77,6 +77,9 @@ const translation = { executions: '{{num}} EXÉCUTIONS', execution: 'EXÉCUTION', }, + login: { + backToHome: 'Retour à l\'accueil', + }, } export default translation diff --git a/web/i18n/hi-IN/app-debug.ts b/web/i18n/hi-IN/app-debug.ts index 1b0633ef32..3f4b06c08b 100644 --- a/web/i18n/hi-IN/app-debug.ts +++ b/web/i18n/hi-IN/app-debug.ts @@ -312,6 +312,7 @@ const translation = { 'inputPlaceholder': 'कृपया इनपुट करें', 'content': 'सामग्री', 'required': 'आवश्यक', + 'hide': 'छुपाएँ', 'errorMsg': { varNameRequired: 'वेरिएबल नाम आवश्यक है', labelNameRequired: 'लेबल नाम आवश्यक है', diff --git a/web/i18n/hi-IN/app.ts b/web/i18n/hi-IN/app.ts index f9486b93ec..3929dfeb6a 100644 --- a/web/i18n/hi-IN/app.ts +++ b/web/i18n/hi-IN/app.ts @@ -213,12 +213,14 @@ const translation = { anyone: 'कोई भी वेब ऐप तक पहुँच सकता है', organization: 'संस्थान के किसी भी व्यक्ति को वेब ऐप तक पहुंच प्राप्त है', specific: 'केवल विशेष समूह या सदस्य ही वेब ऐप तक पहुंच सकते हैं', + external: 'केवल प्रमाणित बाहरी उपयोगकर्ता वेब अनुप्रयोग तक पहुँच सकते हैं', }, accessControlDialog: { accessItems: { anyone: 'लिंक के साथ कोई भी', specific: 'विशिष्ट समूह या सदस्य', organization: 'केवल उद्यम के भीतर के सदस्य', + external: 'प्रमाणित बाहरी उपयोगकर्ता', }, operateGroupAndMember: { searchPlaceholder: 'समूहों और सदस्यों की खोज करें', diff --git a/web/i18n/hi-IN/dataset-settings.ts b/web/i18n/hi-IN/dataset-settings.ts index c89097d89e..9a05847ac8 100644 --- a/web/i18n/hi-IN/dataset-settings.ts +++ b/web/i18n/hi-IN/dataset-settings.ts @@ -1,6 +1,6 @@ const translation = { title: 'ज्ञान सेटिंग्ज', - desc: 'यहां आप ज्ञान की संपत्ति और कार्य प्रक्रियाओं को modify कर सकते हैं。', + desc: 'यहां आप ज्ञान की संपत्ति और कार्य प्रक्रियाओं को modify कर सकते हैं.', form: { name: 'ज्ञान नाम', namePlaceholder: 'कृपया ज्ञान नाम दर्ज करें', diff --git a/web/i18n/hi-IN/share-app.ts b/web/i18n/hi-IN/share-app.ts index e0296fda83..a1e716b5bc 100644 --- a/web/i18n/hi-IN/share-app.ts +++ b/web/i18n/hi-IN/share-app.ts @@ -80,6 +80,9 @@ const translation = { execution: 'अनु执行', executions: '{{num}} फाँसी', }, + login: { + backToHome: 'होम पर वापस', + }, } export default translation diff --git a/web/i18n/it-IT/app-debug.ts b/web/i18n/it-IT/app-debug.ts index e4555b973a..c8b3c08302 100644 --- a/web/i18n/it-IT/app-debug.ts +++ b/web/i18n/it-IT/app-debug.ts @@ -314,6 +314,7 @@ const translation = { 'inputPlaceholder': 'Per favore inserisci', 'content': 'Contenuto', 'required': 'Richiesto', + 'hide': 'Nascondi', 'errorMsg': { varNameRequired: 'Il nome della variabile è richiesto', labelNameRequired: 'Il nome dell\'etichetta è richiesto', diff --git a/web/i18n/it-IT/app.ts b/web/i18n/it-IT/app.ts index f6855873db..2bd5069b6c 100644 --- a/web/i18n/it-IT/app.ts +++ b/web/i18n/it-IT/app.ts @@ -224,12 +224,14 @@ const translation = { anyone: 'Chiunque può accedere all\'app web', specific: 'Solo gruppi o membri specifici possono accedere all\'app web.', organization: 'Qualsiasi persona nell\'organizzazione può accedere all\'app web', + external: 'Solo gli utenti esterni autenticati possono accedere all\'applicazione Web', }, accessControlDialog: { accessItems: { anyone: 'Chiunque con il link', specific: 'Gruppi o membri specifici', organization: 'Solo i membri all\'interno dell\'impresa', + external: 'Utenti esterni autenticati', }, operateGroupAndMember: { searchPlaceholder: 'Cerca gruppi e membri', diff --git a/web/i18n/it-IT/share-app.ts b/web/i18n/it-IT/share-app.ts index 2e1c96a396..4c6c18ff33 100644 --- a/web/i18n/it-IT/share-app.ts +++ b/web/i18n/it-IT/share-app.ts @@ -79,6 +79,9 @@ const translation = { execution: 'ESECUZIONE', executions: '{{num}} ESECUZIONI', }, + login: { + backToHome: 'Torna alla home', + }, } export default translation diff --git a/web/i18n/ja-JP/app-annotation.ts b/web/i18n/ja-JP/app-annotation.ts index 297e01d184..38b891d9d8 100644 --- a/web/i18n/ja-JP/app-annotation.ts +++ b/web/i18n/ja-JP/app-annotation.ts @@ -42,9 +42,9 @@ const translation = { }, batchModal: { title: '一括インポート', - csvUploadTitle: 'CSVファイルをここにドラッグ&ドロップするか、', + csvUploadTitle: 'CSV ファイルをここにドラッグ&ドロップするか、', browse: '参照', - tip: 'CSVファイルは以下の構造に準拠する必要があります:', + tip: 'CSV ファイルは以下の構造に準拠する必要があります:', question: '質問', answer: '回答', contentTitle: 'チャンクの内容', diff --git a/web/i18n/ja-JP/app-api.ts b/web/i18n/ja-JP/app-api.ts index 4d30a71a10..e344ad04a9 100644 --- a/web/i18n/ja-JP/app-api.ts +++ b/web/i18n/ja-JP/app-api.ts @@ -1,6 +1,6 @@ const translation = { - apiServer: 'APIサーバー', - apiKey: 'APIキー', + apiServer: 'API サーバー', + apiKey: 'API キー', status: 'ステータス', disabled: '無効', ok: '稼働中', @@ -15,8 +15,8 @@ const translation = { }, never: 'なし', apiKeyModal: { - apiSecretKey: 'APIシークレットキー', - apiSecretKeyTips: 'APIの悪用を防ぐために、APIキーを保護してください。フロントエンドのコードで平文として使用しないでください。:)', + apiSecretKey: 'API シークレットキー', + apiSecretKeyTips: 'API の悪用を防ぐために、API キーを保護してください。フロントエンドのコードで平文として使用しないでください。:)', createNewSecretKey: '新しいシークレットキーを作成', secretKey: 'シークレットキー', created: '作成日時', @@ -29,44 +29,44 @@ const translation = { ok: 'OK', }, completionMode: { - title: '補完アプリAPI', - info: '記事、要約、翻訳などの高品質なテキスト生成には、ユーザーの入力を使用した補完メッセージAPIを使用します。テキスト生成は、Dify Prompt Engineeringで設定されたモデルパラメータとプロンプトテンプレートに依存しています。', + title: '補完アプリ API', + info: '記事、要約、翻訳などの高品質なテキスト生成には、ユーザーの入力を使用した補完メッセージ API を使用します。テキスト生成は、Dify Prompt Engineering で設定されたモデルパラメータとプロンプトテンプレートに依存しています。', createCompletionApi: '補完メッセージの作成', createCompletionApiTip: '質疑応答モードをサポートするために、補完メッセージを作成します。', - inputsTips: '(オプション)Prompt Engの変数に対応するキーと値のペアとしてユーザー入力フィールドを提供します。キーは変数名で、値はパラメータの値です。フィールドのタイプがSelectの場合、送信される値は事前に設定された選択肢のいずれかである必要があります。', + inputsTips: '(オプション)Prompt Eng の変数に対応するキーと値のペアとしてユーザー入力フィールドを提供します。キーは変数名で、値はパラメータの値です。フィールドのタイプが Select の場合、送信される値は事前に設定された選択肢のいずれかである必要があります。', queryTips: 'ユーザーの入力テキスト内容。', blocking: 'ブロッキングタイプで、実行が完了して結果が返されるまで待機します。(処理が長い場合、リクエストは中断される場合があります)', streaming: 'ストリーミングの返却。SSE(Server-Sent Events)に基づいたストリーミングの返却の実装。', messageFeedbackApi: 'メッセージフィードバック(いいね)', messageFeedbackApiTip: 'エンドユーザーの代わりに受信したメッセージを「いいね」または「いいね」で評価します。このデータはログ&注釈ページで表示され、将来のモデルの微調整に使用されます。', - messageIDTip: 'メッセージID', - ratingTip: 'いいねまたはいいね、nullは元に戻す', + messageIDTip: 'メッセージ ID', + ratingTip: 'いいねまたはいいね、null は元に戻す', parametersApi: 'アプリケーションパラメータ情報の取得', parametersApiTip: '変数名、フィールド名、タイプ、デフォルト値を含む設定済みの入力パラメータを取得します。通常、これらのフィールドをフォームに表示したり、クライアントの読み込み後にデフォルト値を入力したりするために使用されます。', }, chatMode: { - title: 'チャットアプリAPI', - info: '質疑応答形式を使用した多目的の対話型アプリケーションには、チャットメッセージAPIを呼び出して対話を開始します。返されたconversation_idを渡すことで、継続的な会話を維持します。応答パラメータとテンプレートは、Dify Prompt Engの設定に依存します。', + title: 'チャットアプリ API', + info: '質疑応答形式を使用した多目的の対話型アプリケーションには、チャットメッセージ API を呼び出して対話を開始します。返された conversation_id を渡すことで、継続的な会話を維持します。応答パラメータとテンプレートは、Dify Prompt Eng の設定に依存します。', createChatApi: 'チャットメッセージの作成', createChatApiTip: '新しい会話メッセージを作成するか、既存の対話を継続します。', - inputsTips: '(オプション)Prompt Engの変数に対応するキーと値のペアとしてユーザー入力フィールドを提供します。キーは変数名で、値はパラメータの値です。フィールドのタイプがSelectの場合、送信される値は事前に設定された選択肢のいずれかである必要があります。', + inputsTips: '(オプション)Prompt Eng の変数に対応するキーと値のペアとしてユーザー入力フィールドを提供します。キーは変数名で、値はパラメータの値です。フィールドのタイプが Select の場合、送信される値は事前に設定された選択肢のいずれかである必要があります。', queryTips: 'ユーザーの入力/質問内容', blocking: 'ブロッキングタイプで、実行が完了して結果が返されるまで待機します。(処理が長い場合、リクエストは中断される場合があります)', streaming: 'ストリーミングの返却。SSE(Server-Sent Events)に基づいたストリーミングの返却の実装。', - conversationIdTip: '(オプション)会話ID:初回の会話の場合は空白のままにしておき、継続する場合はコンテキストからconversation_idを渡します。', + conversationIdTip: '(オプション)会話 ID:初回の会話の場合は空白のままにしておき、継続する場合はコンテキストから conversation_id を渡します。', messageFeedbackApi: 'メッセージ端末ユーザーフィードバック、いいね', messageFeedbackApiTip: 'エンドユーザーの代わりに受信したメッセージを「いいね」または「いいね」で評価します。このデータはログ&注釈ページで表示され、将来のモデルの微調整に使用されます。', - messageIDTip: 'メッセージID', - ratingTip: 'いいねまたはいいね、nullは元に戻す', + messageIDTip: 'メッセージ ID', + ratingTip: 'いいねまたはいいね、null は元に戻す', chatMsgHistoryApi: 'チャット履歴メッセージの取得', chatMsgHistoryApiTip: '最初のページは最新の「limit」バーを返します。逆順です。', - chatMsgHistoryConversationIdTip: '会話ID', - chatMsgHistoryFirstId: '現在のページの最初のチャットレコードのID。デフォルトはなし。', - chatMsgHistoryLimit: '1回のリクエストで返されるチャットの数', + chatMsgHistoryConversationIdTip: '会話 ID', + chatMsgHistoryFirstId: '現在のページの最初のチャットレコードの ID。デフォルトはなし。', + chatMsgHistoryLimit: '1 回のリクエストで返されるチャットの数', conversationsListApi: '会話リストの取得', - conversationsListApiTip: '現在のユーザーのセッションリストを取得します。デフォルトでは、最後の20のセッションが返されます。', - conversationsListFirstIdTip: '現在のページの最後のレコードのID、デフォルトはなし。', - conversationsListLimitTip: '1回のリクエストで返されるチャットの数', + conversationsListApiTip: '現在のユーザーのセッションリストを取得します。デフォルトでは、最後の 20 のセッションが返されます。', + conversationsListFirstIdTip: '現在のページの最後のレコードの ID、デフォルトはなし。', + conversationsListLimitTip: '1 回のリクエストで返されるチャットの数', conversationRenamingApi: '会話の名前変更', conversationRenamingApiTip: '会話の名前を変更します。名前はマルチセッションクライアントインターフェースに表示されます。', conversationRenamingNameTip: '新しい名前', diff --git a/web/i18n/ja-JP/app-debug.ts b/web/i18n/ja-JP/app-debug.ts index 350d58d60a..f862f3f2f7 100644 --- a/web/i18n/ja-JP/app-debug.ts +++ b/web/i18n/ja-JP/app-debug.ts @@ -5,12 +5,12 @@ const translation = { }, orchestrate: 'オーケストレーション', promptMode: { - simple: 'エキスパートモードに切り替えて、PROMPT全体を編集します', + simple: 'エキスパートモードに切り替えて、PROMPT 全体を編集します', advanced: 'エキスパートモード', switchBack: '基本モードに戻る', advancedWarning: { - title: 'エキスパートモードに切り替えました。PROMPTを変更すると、基本モードに戻ることはできません。', - description: 'エキスパートモードでは、PROMPT全体を編集できます。', + title: 'エキスパートモードに切り替えました。PROMPT を変更すると、基本モードに戻ることはできません。', + description: 'エキスパートモードでは、PROMPT 全体を編集できます。', learnMore: '詳細はこちら', ok: 'OK', }, @@ -33,14 +33,14 @@ const translation = { userAction: 'ユーザー', }, notSetAPIKey: { - title: 'LLMプロバイダーキーが設定されていません', + title: 'LLM プロバイダーキーが設定されていません', trailFinished: 'トライアル終了', - description: 'LLMプロバイダーキーが設定されていません。デバッグする前に設定する必要があります。', + description: 'LLM プロバイダーキーが設定されていません。デバッグする前に設定する必要があります。', settingBtn: '設定に移動', }, trailUseGPT4Info: { - title: '現在、gpt-4はサポートされていません', - description: 'gpt-4を使用するには、APIキーを設定してください。', + title: '現在、gpt-4 はサポートされていません', + description: 'gpt-4 を使用するには、API キーを設定してください。', }, feature: { groupChat: { @@ -52,12 +52,12 @@ const translation = { }, conversationOpener: { title: '会話の開始', - description: 'チャットアプリでは、AIがユーザーに最初にアクティブに話しかける最初の文は、通常、歓迎メッセージとして使用されます。', + description: 'チャットアプリでは、AI がユーザーに最初にアクティブに話しかける最初の文は、通常、歓迎メッセージとして使用されます。', }, suggestedQuestionsAfterAnswer: { title: 'フォローアップ', description: '次の質問の提案を設定すると、ユーザーにより良いチャットが提供されます。', - resDes: 'ユーザーの次の質問に関する3つの提案。', + resDes: 'ユーザーの次の質問に関する 3 つの提案。', tryToAsk: '質問してみてください', }, moreLikeThis: { @@ -128,7 +128,7 @@ const translation = { }, tools: { title: 'ツール', - tips: 'ツールは、ユーザー入力または変数をリクエストパラメーターとして使用して外部データをコンテキストとしてクエリするための標準的なAPI呼び出し方法を提供します。', + tips: 'ツールは、ユーザー入力または変数をリクエストパラメーターとして使用して外部データをコンテキストとしてクエリするための標準的な API 呼び出し方法を提供します。', toolsInUse: '{{count}} 個のツールが使用中', modal: { title: 'ツール', @@ -162,7 +162,7 @@ const translation = { }, moderation: { title: 'コンテンツのモデレーション', - description: 'モデレーションAPIを使用するか、機密語リストを維持することで、モデルの出力を安全にします。', + description: 'モデレーション API を使用するか、機密語リストを維持することで、モデルの出力を安全にします。', contentEnableLabel: 'モデレート・コンテンツを有効にする', allEnabled: '入力/出力コンテンツが有効になっています', inputEnabled: '入力コンテンツが有効になっています', @@ -171,16 +171,16 @@ const translation = { title: 'コンテンツのモデレーション設定', provider: { title: 'プロバイダ', - openai: 'OpenAIモデレーション', + openai: 'OpenAI モデレーション', openaiTip: { - prefix: 'OpenAIモデレーションには、', - suffix: 'にOpenAI APIキーが設定されている必要があります。', + prefix: 'OpenAI モデレーションには、', + suffix: 'に OpenAI API キーが設定されている必要があります。', }, keywords: 'キーワード', }, keywords: { - tip: '1行ごとに1つ、行区切りで入力してください。1行あたり最大100文字。', - placeholder: '1行ごとに、行区切りで入力してください', + tip: '1 行ごとに 1 つ、行区切りで入力してください。1 行あたり最大 100 文字。', + placeholder: '1 行ごとに、行区切りで入力してください', line: '行', }, content: { @@ -188,14 +188,14 @@ const translation = { output: '出力コンテンツをモデレート', preset: 'プリセット返信', placeholder: 'ここにプリセット返信の内容を入力', - condition: '少なくとも1つの入力および出力コンテンツをモデレートする', - fromApi: 'プリセット返信はAPIによって返されます', + condition: '少なくとも 1 つの入力および出力コンテンツをモデレートする', + fromApi: 'プリセット返信は API によって返されます', errorMessage: 'プリセット返信は空にできません', - supportMarkdown: 'Markdownがサポートされています', + supportMarkdown: 'Markdown がサポートされています', }, openaiNotConfig: { - before: 'OpenAIモデレーションには、', - after: 'にOpenAI APIキーが設定されている必要があります。', + before: 'OpenAI モデレーションには、', + after: 'に OpenAI API キーが設定されている必要があります。', }, }, }, @@ -214,13 +214,13 @@ const translation = { modalTitle: '画像アップロード設置', }, bar: { - empty: 'Webアプリのユーザーエクスペリアンスを強化させる機能を有効にする', + empty: 'Web アプリのユーザーエクスペリアンスを強化させる機能を有効にする', enableText: '有効な機能', manage: '管理', }, documentUpload: { title: 'ドキュメント', - description: 'ドキュメント機能を有効にすると、AIモデルがファイルを処理し、その内容に基づいて質問に回答できるようになります。', + description: 'ドキュメント機能を有効にすると、AI モデルがファイルを処理し、その内容に基づいて質問に回答できるようになります。', }, }, codegen: { @@ -228,7 +228,7 @@ const translation = { description: 'コードジェネレーターは、設定されたモデルを使用して指示に基づいて高品質なコードを生成します。明確で詳細な指示を提供してください。', instruction: '指示', instructionPlaceholder: '生成したいコードの詳細な説明を入力してください。', - noDataLine1: '左側に使用例を記入してください,', + noDataLine1: '左側に使用例を記入してください,', noDataLine2: 'コードのプレビューがこちらに表示されます。', generate: '生成', generatedCodeTitle: '生成されたコード', @@ -247,7 +247,7 @@ const translation = { instructionPlaceHolder: '具体的で明確な指示を入力してください。', generate: '生成', resTitle: '生成されたプロンプト', - noDataLine1: '左側に使用例を記入してください,', + noDataLine1: '左側に使用例を記入してください,', noDataLine2: 'オーケストレーションのプレビューがこちらに表示されます。', apply: '適用', noData: '左側にユースケースを入力すると、こちらでプレビューができます。', @@ -276,12 +276,12 @@ const translation = { instruction: 'ユーザーが簡単に旅行計画を立てられるように設計されたツール', }, SQLSorcerer: { - name: 'SQLソーサラー', - instruction: '日常言語をSQLクエリに変換する', + name: 'SQL ソーサラー', + instruction: '日常言語を SQL クエリに変換する', }, GitGud: { name: 'Git gud', - instruction: 'ユーザーが記述したバージョン管理アクションに対応するGitコマンドを生成する', + instruction: 'ユーザーが記述したバージョン管理アクションに対応する Git コマンドを生成する', }, meetingTakeaways: { name: '会議の要点', @@ -298,7 +298,7 @@ const translation = { message: '変更が破棄され、最後に公開された構成が復元されます。', }, errorMessage: { - nameOfKeyRequired: 'キーの名前: {{key}} が必要です', + nameOfKeyRequired: 'キーの名前:{{key}} が必要です', valueOfVarRequired: '{{key}} の値は空にできません', queryRequired: 'リクエストテキストが必要です。', waitForResponse: '前のメッセージへの応答が完了するまでお待ちください。', @@ -309,7 +309,7 @@ const translation = { }, chatSubTitle: 'プロンプト', completionSubTitle: '接頭辞プロンプト', - promptTip: 'プロンプトは、AIの応答を指示と制約で誘導します。 {{input}} のような変数を挿入します。このプロンプトはユーザーには表示されません。', + promptTip: 'プロンプトは、AI の応答を指示と制約で誘導します。 {{input}} のような変数を挿入します。このプロンプトはユーザーには表示されません。', formattingChangedTitle: '書式が変更されました', formattingChangedText: '書式を変更すると、デバッグ領域がリセットされます。よろしいですか?', variableTitle: '変数', @@ -327,7 +327,7 @@ const translation = { }, varKeyError: { canNoBeEmpty: '{{key}} は必須です', - tooLong: '{{key}} が長すぎます。30文字を超えることはできません', + tooLong: '{{key}} が長すぎます。30 文字を超えることはできません', notValid: '{{key}} が無効です。文字、数字、アンダースコアのみを含めることができます', notStartWithNumber: '{{key}} は数字で始めることはできません', keyAlreadyExists: '{{key}} はすでに存在します', @@ -354,11 +354,12 @@ const translation = { 'maxLength': '最大長', 'options': 'オプション', 'addOption': 'オプションを追加', - 'apiBasedVar': 'APIベースの変数', + 'apiBasedVar': 'API ベースの変数', 'varName': '変数名', 'labelName': 'ラベル名', 'inputPlaceholder': '入力してください', 'required': '必須', + 'hide': '非表示', 'file': { supportFileTypes: 'サポートされたファイルタイプ', image: { @@ -376,7 +377,7 @@ const translation = { custom: { name: '他のファイルタイプ', description: '他のファイルタイプを指定する。', - createPlaceholder: '+ 拡張子, 例:.doc', + createPlaceholder: '+ 拡張子,例:.doc', }, }, 'uploadFileTypes': 'アップロードされたファイルのタイプ', @@ -388,7 +389,7 @@ const translation = { varNameRequired: '変数名は必須です', labelNameRequired: 'ラベル名は必須です', varNameCanBeRepeat: '変数名は繰り返すことができません', - atLeastOneOption: '少なくとも1つのオプションが必要です', + atLeastOneOption: '少なくとも 1 つのオプションが必要です', optionRepeat: '繰り返しオプションがあります', }, }, @@ -473,10 +474,10 @@ const translation = { title: 'マルチパスリトリーバル', description: 'ユーザーの意図に基づいて、すべてのナレッジをクエリし、複数のソースから関連するテキストを取得し、再順位付け後、ユーザークエリに最適な結果を選択します。再順位付けモデル API の構成が必要です。', }, - embeddingModelRequired: 'Embeddingモデルが設定されていない', + embeddingModelRequired: 'Embedding モデルが設定されていない', rerankModelRequired: '再順位付けモデルが必要です', params: 'パラメータ', - top_k: 'トップK', + top_k: 'トップ K', top_kTip: 'ユーザーの質問に最も類似したチャンクをフィルタリングするために使用されます。システムは、選択したモデルの max_tokens に応じて、動的に Top K の値を調整します。', score_threshold: 'スコア閾値', score_thresholdTip: 'チャンクフィルタリングの類似性閾値を設定するために使用されます。', @@ -518,7 +519,7 @@ const translation = { promptPlaceholder: 'ここにプロンプトを入力してください', tools: { name: 'ツール', - description: 'ツールを使用すると、インターネットの検索や科学的計算など、LLMの機能を拡張できます', + description: 'ツールを使用すると、インターネットの検索や科学的計算など、LLM の機能を拡張できます', enabled: '有効', }, }, diff --git a/web/i18n/ja-JP/app-log.ts b/web/i18n/ja-JP/app-log.ts index d4553e830c..43a6fb9151 100644 --- a/web/i18n/ja-JP/app-log.ts +++ b/web/i18n/ja-JP/app-log.ts @@ -1,6 +1,6 @@ const translation = { title: 'ログ', - description: 'ログは、アプリケーションの実行状態を記録します。ユーザーの入力やAIの応答などが含まれます。', + description: 'ログは、アプリケーションの実行状態を記録します。ユーザーの入力や AI の応答などが含まれます。', dateTimeFormat: 'MM/DD/YYYY hh:mm A', table: { header: { @@ -29,13 +29,13 @@ const translation = { noOutput: '出力がありません', element: { title: '誰かいますか?', - content: 'ここでは、エンドユーザーとAIアプリケーション間の相互作用を観察し、注釈を付けることで、AIの精度を継続的に向上させます。Webアプリを<shareLink>共有</shareLink>または<testLink>テスト</testLink>してみて、このページに戻ってください。', + content: 'ここでは、エンドユーザーと AI アプリケーション間の相互作用を観察し、注釈を付けることで、AI の精度を継続的に向上させます。Web アプリを<shareLink>共有</shareLink>または<testLink>テスト</testLink>してみて、このページに戻ってください。', }, }, }, detail: { time: '時間', - conversationId: '会話ID', + conversationId: '会話 ID', promptTemplate: 'プロンプトテンプレート', promptTemplateBeforeChat: 'チャット前のプロンプトテンプレート・システムメッセージとして', annotationTip: '{{user}} によってマークされた改善', @@ -48,7 +48,7 @@ const translation = { dislike: 'いいね解除', addAnnotation: '改善を追加', editAnnotation: '改善を編集', - annotationPlaceholder: '将来のモデルの微調整やテキスト生成品質の継続的改善のためにAIが返信することを期待する答えを入力してください。', + annotationPlaceholder: '将来のモデルの微調整やテキスト生成品質の継続的改善のために AI が返信することを期待する答えを入力してください。', }, variables: '変数', uploadImages: 'アップロードされた画像', @@ -57,10 +57,10 @@ const translation = { filter: { period: { today: '今日', - last7days: '過去7日間', - last4weeks: '過去4週間', - last3months: '過去3ヶ月', - last12months: '過去12ヶ月', + last7days: '過去 7 日間', + last4weeks: '過去 4 週間', + last3months: '過去 3 ヶ月', + last12months: '過去 12 ヶ月', monthToDate: '月初から今日まで', quarterToDate: '四半期初から今日まで', yearToDate: '年初から今日まで', diff --git a/web/i18n/ja-JP/app-overview.ts b/web/i18n/ja-JP/app-overview.ts index 3b0505b0de..d948bc3b28 100644 --- a/web/i18n/ja-JP/app-overview.ts +++ b/web/i18n/ja-JP/app-overview.ts @@ -1,9 +1,9 @@ const translation = { welcome: { firstStepTip: 'はじめるには、', - enterKeyTip: '以下にOpenAI APIキーを入力してください', - getKeyTip: 'OpenAIダッシュボードからAPIキーを取得してください', - placeholder: 'OpenAI APIキー(例:sk-xxxx)', + enterKeyTip: '以下に OpenAI API キーを入力してください', + getKeyTip: 'OpenAI ダッシュボードから API キーを取得してください', + placeholder: 'OpenAI API キー(例:sk-xxxx)', }, apiKeyInfo: { cloud: { @@ -12,7 +12,7 @@ const translation = { description: 'トライアルクォータはテスト用に提供されます。トライアルクォータのコールが使い切られる前に、独自のモデルプロバイダを設定するか、追加のクォータを購入してください。', }, exhausted: { - title: 'トライアルクォータが使い切れました。APIキーを設定してください。', + title: 'トライアルクォータが使い切れました。API キーを設定してください。', description: 'トライアルクォータが使い切れました。独自のモデルプロバイダを設定するか、追加のクォータを購入してください。', }, }, @@ -25,36 +25,36 @@ const translation = { callTimes: 'コール回数', usedToken: '使用済みトークン', setAPIBtn: 'モデルプロバイダの設定へ', - tryCloud: 'またはDifyのクラウドバージョンを無料見積もりでお試しください', + tryCloud: 'または Dify のクラウドバージョンを無料見積もりでお試しください', }, overview: { title: '概要', appInfo: { - explanation: '使いやすいAI Webアプリ', - accessibleAddress: '公開URL', + explanation: '使いやすい AI Web アプリ', + accessibleAddress: '公開 URL', preview: 'プレビュー', regenerate: '再生成', - regenerateNotice: '公開URLを再生成しますか?', - preUseReminder: '続行する前にWebアプリを有効にしてください。', + regenerateNotice: '公開 URL を再生成しますか?', + preUseReminder: '続行する前に Web アプリを有効にしてください。', settings: { entry: '設定', - title: 'Webアプリの設定', - webName: 'Webアプリの名前', - webDesc: 'Webアプリの説明', + title: 'Web アプリの設定', + webName: 'Web アプリの名前', + webDesc: 'Web アプリの説明', webDescTip: 'このテキストはクライアント側に表示され、アプリケーションの使用方法の基本的なガイダンスを提供します。', - webDescPlaceholder: 'Webアプリの説明を入力してください', + webDescPlaceholder: 'Web アプリの説明を入力してください', language: '言語', workflow: { title: 'ワークフローステップ', show: '表示', hide: '非表示', subTitle: 'ワークフローの詳細', - showDesc: 'Webアプリでワークフローの詳細を表示または非表示にする', + showDesc: 'Web アプリでワークフローの詳細を表示または非表示にする', }, chatColorTheme: 'チャットボットのカラーテーマ', chatColorThemeDesc: 'チャットボットのカラーテーマを設定します', chatColorThemeInverted: '反転', - invalidHexMessage: '無効な16進数値', + invalidHexMessage: '無効な 16 進数値', invalidPrivacyPolicy: '無効なプライバシーポリシーのリンクです。http または https で始まる有効なリンクを使用してください', more: { entry: 'その他の設定を表示', @@ -62,18 +62,18 @@ const translation = { copyRightPlaceholder: '著作者または組織名を入力してください', privacyPolicy: 'プライバシーポリシー', privacyPolicyPlaceholder: 'プライバシーポリシーリンクを入力してください', - privacyPolicyTip: '訪問者がアプリケーションが収集するデータを理解し、Difyの<privacyPolicyLink>プライバシーポリシー</privacyPolicyLink>を参照できるようにします。', + privacyPolicyTip: '訪問者がアプリケーションが収集するデータを理解し、Dify の<privacyPolicyLink>プライバシーポリシー</privacyPolicyLink>を参照できるようにします。', customDisclaimer: 'カスタム免責事項', customDisclaimerPlaceholder: '免責事項を入力してください', customDisclaimerTip: 'アプリケーションの使用に関する免責事項を提供します。', copyrightTooltip: 'プロフェッショナルプラン以上にアップグレードしてください', - copyrightTip: 'Webアプリに著作権情報を表示する', + copyrightTip: 'Web アプリに著作権情報を表示する', }, sso: { - title: 'WebアプリのSSO', - tooltip: '管理者に問い合わせて、WebアプリのSSOを有効にします', - label: 'SSO認証', - description: 'すべてのユーザーは、Webアプリを使用する前にSSOでログインする必要があります', + title: 'Web アプリの SSO', + tooltip: '管理者に問い合わせて、Web アプリの SSO を有効にします', + label: 'SSO 認証', + description: 'すべてのユーザーは、Web アプリを使用する前に SSO でログインする必要があります', }, modalTip: 'クライアント側の Web アプリ設定。', }, @@ -81,45 +81,45 @@ const translation = { entry: '埋め込み', title: 'ウェブサイトに埋め込む', explanation: 'チャットアプリをウェブサイトに埋め込む方法を選択します。', - iframe: 'ウェブサイトの任意の場所にチャットアプリを追加するには、このiframeをHTMLコードに追加してください。', - scripts: 'ウェブサイトの右下にチャットアプリを追加するには、このコードをHTMLに追加してください。', - chromePlugin: 'Dify Chatbot Chrome拡張機能をインストール', + iframe: 'ウェブサイトの任意の場所にチャットアプリを追加するには、この iframe を HTML コードに追加してください。', + scripts: 'ウェブサイトの右下にチャットアプリを追加するには、このコードを HTML に追加してください。', + chromePlugin: 'Dify Chatbot Chrome 拡張機能をインストール', copied: 'コピーしました', copy: 'コピー', }, qrcode: { - title: '共有用QRコード', + title: '共有用 QR コード', scan: 'アプリケーションの共有をスキャン', - download: 'QRコードをダウンロード', + download: 'QR コードをダウンロード', }, customize: { way: '方法', entry: 'カスタマイズ', - title: 'AI Webアプリのカスタマイズ', - explanation: 'シナリオとスタイルのニーズに合わせてWebアプリのフロントエンドをカスタマイズできます。', + title: 'AI Web アプリのカスタマイズ', + explanation: 'シナリオとスタイルのニーズに合わせて Web アプリのフロントエンドをカスタマイズできます。', way1: { - name: 'クライアントコードをフォークして修正し、Vercelにデプロイします(推奨)', + name: 'クライアントコードをフォークして修正し、Vercel にデプロイします(推奨)', step1: 'クライアントコードをフォークして修正します', - step1Tip: 'ここをクリックしてソースコードをGitHubアカウントにフォークし、コードを修正します', + step1Tip: 'ここをクリックしてソースコードを GitHub アカウントにフォークし、コードを修正します', step1Operation: 'Dify-WebClient', - step2: 'Vercelにデプロイします', - step2Tip: 'ここをクリックしてリポジトリをVercelにインポートし、デプロイします', + step2: 'Vercel にデプロイします', + step2Tip: 'ここをクリックしてリポジトリを Vercel にインポートし、デプロイします', step2Operation: 'リポジトリをインポート', step3: '環境変数を設定します', - step3Tip: 'Vercelに次の環境変数を追加します', + step3Tip: 'Vercel に次の環境変数を追加します', }, way2: { - name: 'クライアントサイドのコードを記述してAPIを呼び出し、サーバーにデプロイします', + name: 'クライアントサイドのコードを記述して API を呼び出し、サーバーにデプロイします', operation: 'ドキュメント', }, }, launch: '発射', }, apiInfo: { - title: 'バックエンドサービスAPI', + title: 'バックエンドサービス API', explanation: 'あなたのアプリケーションに簡単に統合できます', - accessibleAddress: 'サービスAPIエンドポイント', - doc: 'APIリファレンス', + accessibleAddress: 'サービス API エンドポイント', + doc: 'API リファレンス', }, status: { running: '稼働中', @@ -132,15 +132,15 @@ const translation = { tokenPS: 'トークン/秒', totalMessages: { title: 'トータルメッセージ数', - explanation: '日次AIインタラクション数。', + explanation: '日次 AI インタラクション数。', }, totalConversations: { title: '総会話数', - explanation: '日次AI会話数;プロンプトエンジニアリング/デバッグは除外。', + explanation: '日次 AI 会話数;プロンプトエンジニアリング/デバッグは除外。', }, activeUsers: { title: 'アクティブユーザー数', - explanation: 'AIとのQ&Aに参加しているユニークユーザー数;工学的/デバッグ目的のプロンプトは除外されます。', + explanation: 'AI との Q&A に参加しているユニークユーザー数;工学的/デバッグ目的のプロンプトは除外されます。', }, tokenUsage: { title: 'トークン使用量', @@ -149,7 +149,7 @@ const translation = { }, avgSessionInteractions: { title: '平均セッションインタラクション数', - explanation: 'ユーザーとAIの連続的なコミュニケーション数;対話型アプリケーション向け。', + explanation: 'ユーザーと AI の連続的なコミュニケーション数;対話型アプリケーション向け。', }, avgUserInteractions: { title: '平均ユーザーインタラクション数', @@ -157,15 +157,15 @@ const translation = { }, userSatisfactionRate: { title: 'ユーザー満足度率', - explanation: '1,000件のメッセージあたりの「いいね」の数。これは、ユーザーが非常に満足している回答の割合を示します。', + explanation: '1,000 件のメッセージあたりの「いいね」の数。これは、ユーザーが非常に満足している回答の割合を示します。', }, avgResponseTime: { title: '平均応答時間', - explanation: 'AIが処理/応答する時間(ミリ秒);テキストベースのアプリケーション向け。', + explanation: 'AI が処理/応答する時間(ミリ秒);テキストベースのアプリケーション向け。', }, tps: { title: 'トークン出力速度', - explanation: 'LLMのパフォーマンスを測定します。リクエストの開始から出力の完了までのLLMのトークン出力速度を数えます。', + explanation: 'LLM のパフォーマンスを測定します。リクエストの開始から出力の完了までの LLM のトークン出力速度を数えます。', }, }, } diff --git a/web/i18n/ja-JP/app.ts b/web/i18n/ja-JP/app.ts index 283dd0f003..b501bc129e 100644 --- a/web/i18n/ja-JP/app.ts +++ b/web/i18n/ja-JP/app.ts @@ -19,10 +19,10 @@ const translation = { exportFailed: 'DSL のエクスポートに失敗しました。', importDSL: 'DSL ファイルをインポート', createFromConfigFile: 'DSL ファイルから作成する', - importFromDSL: 'DSLからインポート', - importFromDSLFile: 'DSLファイルから', - importFromDSLUrl: 'URLから', - importFromDSLUrlPlaceholder: 'DSLリンクをここに貼り付けます', + importFromDSL: 'DSL からインポート', + importFromDSLFile: 'DSL ファイルから', + importFromDSLUrl: 'URL から', + importFromDSLUrlPlaceholder: 'DSL リンクをここに貼り付けます', deleteAppConfirmTitle: 'このアプリを削除しますか?', deleteAppConfirmContent: 'アプリを削除すると、元に戻すことはできません。他のユーザーはもはやこのアプリにアクセスできず、すべてのプロンプトの設定とログが永久に削除されます。', @@ -73,11 +73,11 @@ const translation = { appCreateFailed: 'アプリの作成に失敗しました', Confirm: '確認する', caution: '注意', - appCreateDSLErrorPart2: '続行しますか?', - appCreateDSLErrorPart4: 'システムがサポートするDSLバージョン:', - appCreateDSLErrorPart3: '現在のアプリケーションの DSL バージョン:', + appCreateDSLErrorPart2: '続行しますか?', + appCreateDSLErrorPart4: 'システムがサポートする DSL バージョン:', + appCreateDSLErrorPart3: '現在のアプリケーションの DSL バージョン:', appCreateDSLErrorTitle: 'バージョンの非互換性', - appCreateDSLWarning: '注意:DSLのバージョンの違いは、特定の機能に影響を与える可能性があります', + appCreateDSLWarning: '注意:DSL のバージョンの違いは、特定の機能に影響を与える可能性があります', appCreateDSLErrorPart1: 'DSL バージョンに大きな違いが検出されました。インポートを強制すると、アプリケーションが誤動作する可能性があります。', optional: '随意', forBeginners: '初心者向けの基本的なアプリタイプ', @@ -95,11 +95,11 @@ const translation = { forAdvanced: '上級ユーザー向け', chooseAppType: 'アプリタイプを選択', learnMore: '詳細情報', - noIdeaTip: 'アイデアがありませんか?テンプレートをご覧ください', - chatbotShortDescription: '簡単なセットアップのLLMベースのチャットボット', - chatbotUserDescription: '簡単な設定でLLMベースのチャットボットを迅速に構築します。Chatflowは後で切り替えることができます。', - workflowUserDescription: 'ドラッグ&ドロップの簡易性で自律型AIワークフローを視覚的に構築', - completionUserDescription: '簡単な構成でテキスト生成タスク用のAIアシスタントをすばやく構築します。', + noIdeaTip: 'アイデアがありませんか?テンプレートをご覧ください', + chatbotShortDescription: '簡単なセットアップの LLM ベースのチャットボット', + chatbotUserDescription: '簡単な設定で LLM ベースのチャットボットを迅速に構築します。Chatflow は後で切り替えることができます。', + workflowUserDescription: 'ドラッグ&ドロップの簡易性で自律型 AI ワークフローを視覚的に構築', + completionUserDescription: '簡単な構成でテキスト生成タスク用の AI アシスタントをすばやく構築します。', }, editApp: '情報を編集する', editAppTitle: 'アプリ情報を編集する', @@ -129,7 +129,7 @@ const translation = { }, tracing: { title: 'アプリのパフォーマンスの追跡', - description: 'サードパーティのLLMOpsサービスとトレースアプリケーションのパフォーマンス設定を行います。', + description: 'サードパーティの LLMOps サービスとトレースアプリケーションのパフォーマンス設定を行います。', config: '設定', view: '見る', collapse: '折りたたむ', @@ -138,7 +138,7 @@ const translation = { disabled: '無効しました', disabledTip: 'まずはサービスの設定から始めましょう。', enabled: '有効しました', - tracingDescription: 'LLMの呼び出し、コンテキスト、プロンプト、HTTPリクエストなど、アプリケーション実行の全ての文脈をサードパーティのトレースプラットフォームで取り込みます。', + tracingDescription: 'LLM の呼び出し、コンテキスト、プロンプト、HTTP リクエストなど、アプリケーション実行の全ての文脈をサードパーティのトレースプラットフォームで取り込みます。', configProviderTitle: { configured: '設定しました', notConfigured: 'トレース機能を有効化するためには、サービスの設定が必要です。', @@ -146,11 +146,11 @@ const translation = { }, langsmith: { title: 'LangSmith', - description: 'LLMを利用したアプリケーションのライフサイクル全段階を支援する、オールインワンの開発者向けプラットフォームです。', + description: 'LLM を利用したアプリケーションのライフサイクル全段階を支援する、オールインワンの開発者向けプラットフォームです。', }, langfuse: { title: 'Langfuse', - description: 'トレース、評価、プロンプトの管理、そしてメトリクスを駆使して、LLMアプリケーションのデバッグや改善に役立てます。', + description: 'トレース、評価、プロンプトの管理、そしてメトリクスを駆使して、LLM アプリケーションのデバッグや改善に役立てます。', }, opik: { title: 'オピック', @@ -169,13 +169,13 @@ const translation = { }, weave: { title: '織る', - description: 'Weaveは、LLMアプリケーションを評価、テスト、および監視するためのオープンソースプラットフォームです。', + description: 'Weave は、LLM アプリケーションを評価、テスト、および監視するためのオープンソースプラットフォームです。', }, }, answerIcon: { - title: 'Webアプリアイコンを使用して🤖を置き換える', - description: '共有アプリケーションの中で Webアプリアイコンを使用して🤖を置き換えるかどうか', - descriptionInExplore: 'ExploreでWebアプリアイコンを使用して🤖を置き換えるかどうか', + title: 'Web アプリアイコンを使用して🤖を置き換える', + description: '共有アプリケーションの中で Web アプリアイコンを使用して🤖を置き換えるかどうか', + descriptionInExplore: 'Explore で Web アプリアイコンを使用して🤖を置き換えるかどうか', }, newAppFromTemplate: { sidebar: { @@ -198,42 +198,39 @@ const translation = { placeholder: 'アプリを選択...', }, structOutput: { - moreFillTip: '最大10レベルのネストを表示します', + moreFillTip: '最大 10 レベルのネストを表示します', required: '必須', - LLMResponse: 'LLMのレスポンス', + LLMResponse: 'LLM のレスポンス', configure: '設定', notConfiguredTip: '構造化出力が未設定です', structured: '構造化出力', - structuredTip: '構造化出力は、モデルが常に指定されたJSONスキーマに準拠した応答を生成することを保証する機能です。', + structuredTip: '構造化出力は、モデルが常に指定された JSON スキーマに準拠した応答を生成することを保証する機能です。', modelNotSupported: 'モデルが対応していません', modelNotSupportedTip: '現在のモデルはこの機能に対応しておらず、自動的にプロンプトインジェクションに切り替わります。', }, - accessControl: 'Webアプリアクセス制御', + accessControl: 'Web アプリアクセス制御', accessItemsDescription: { - anyone: '誰でも Web アプリにアクセス可能', - specific: '特定のグループまたはメンバーのみが Web アプリにアクセス可能', - organization: '組織内の誰でも Web アプリにアクセス可能', + anyone: '誰でもこの web アプリにアクセスできます(ログイン不要)', + specific: '特定のプラットフォーム内メンバーのみがこの Web アプリにアクセスできます', + organization: 'プラットフォーム内の全メンバーがこの Web アプリにアクセスできます', + external: '認証済みの外部ユーザーのみがこの Web アプリにアクセスできます', }, accessControlDialog: { title: 'アクセス権限', - description: 'Webアプリのアクセス権限を設定します', + description: 'Web アプリのアクセス権限を設定します', accessLabel: '誰がアクセスできますか', - accessItemsDescription: { - anyone: '誰でもWebアプリにアクセス可能です', - specific: '特定のグループやメンバーがWebアプリにアクセス可能です', - organization: '組織内の誰でもWebアプリにアクセス可能です', - }, accessItems: { - anyone: 'すべてのユーザー', - specific: '特定のグループメンバー', - organization: 'グループ内の全員', + anyone: 'リンクを知っているすべてのユーザー', + specific: '特定のプラットフォーム内メンバー', + organization: 'プラットフォーム内の全メンバー', + external: '認証済みの外部ユーザー', }, groups_one: '{{count}} グループ', groups_other: '{{count}} グループ', members_one: '{{count}} メンバー', members_other: '{{count}} メンバー', noGroupsOrMembers: 'グループまたはメンバーが選択されていません', - webAppSSONotEnabledTip: 'Webアプリの認証方式設定については、企業管理者へご連絡ください。', + webAppSSONotEnabledTip: 'Web アプリの外部認証方式を設定するには、組織の管理者にお問い合わせください。', operateGroupAndMember: { searchPlaceholder: 'グループやメンバーを検索', allMembers: 'すべてのメンバー', @@ -243,11 +240,11 @@ const translation = { updateSuccess: '更新が成功しました', }, publishApp: { - title: 'Webアプリへのアクセス権', + title: 'Web アプリへのアクセス権', notSet: '未設定', - notSetDesc: '現在このWebアプリには誰もアクセスできません。権限を設定してください。', + notSetDesc: '現在この Web アプリには誰もアクセスできません。権限を設定してください。', }, - noAccessPermission: 'Webアプリにアクセス権限がありません', + noAccessPermission: 'Web アプリにアクセス権限がありません', } export default translation diff --git a/web/i18n/ja-JP/billing.ts b/web/i18n/ja-JP/billing.ts index b96a972504..ade390917d 100644 --- a/web/i18n/ja-JP/billing.ts +++ b/web/i18n/ja-JP/billing.ts @@ -16,11 +16,11 @@ const translation = { viewBilling: '請求とサブスクリプションの管理', buyPermissionDeniedTip: 'サブスクリプションするには、エンタープライズ管理者に連絡してください', plansCommon: { - title: 'あなたのAIの旅を支える価格設定', + title: 'あなたの AI の旅を支える価格設定', freeTrialTipPrefix: 'サインアップ後、', - freeTrialTip: '200回のOpenAIコールの無料に受け取る', + freeTrialTip: '200 回の OpenAI コールの無料に受け取る', freeTrialTipSuffix: '。クレジットカード不要', - yearlyTip: '10ヶ月分支払って、1年間楽しもう!', + yearlyTip: '10 ヶ月分支払って、1 年間楽しもう!', mostPopular: '人気', cloud: 'クラウドサービス', self: 'セルフホストサービス', @@ -53,11 +53,11 @@ const translation = { vectorSpace: '{{size}}の知識データストレージ', vectorSpaceTooltip: '高品質インデックスモードのドキュメントは、知識データストレージのリソースを消費します。知識データストレージの上限に達すると、新しいドキュメントはアップロードされません。', documentsRequestQuota: '{{count,number}}/分のナレッジ リクエストのレート制限', - documentsRequestQuotaTooltip: 'ナレッジベース内でワークスペースが1分間に実行できる操作の総数を示します。これには、データセットの作成、削除、更新、ドキュメントのアップロード、修正、アーカイブ、およびナレッジベースクエリが含まれます。この指標は、ナレッジベースリクエストのパフォーマンスを評価するために使用されます。例えば、Sandbox ユーザーが1分間に10回連続でヒットテストを実行した場合、そのワークスペースは次の1分間、データセットの作成、削除、更新、ドキュメントのアップロードや修正などの操作を一時的に実行できなくなります。', - apiRateLimit: 'APIレート制限', + documentsRequestQuotaTooltip: 'ナレッジベース内でワークスペースが 1 分間に実行できる操作の総数を示します。これには、データセットの作成、削除、更新、ドキュメントのアップロード、修正、アーカイブ、およびナレッジベースクエリが含まれます。この指標は、ナレッジベースリクエストのパフォーマンスを評価するために使用されます。例えば、Sandbox ユーザーが 1 分間に 10 回連続でヒットテストを実行した場合、そのワークスペースは次の 1 分間、データセットの作成、削除、更新、ドキュメントのアップロードや修正などの操作を一時的に実行できなくなります。', + apiRateLimit: 'API レート制限', apiRateLimitUnit: '{{count,number}}/日', - unlimitedApiRate: '無制限のAPIコール', - apiRateLimitTooltip: 'APIレート制限は、テキスト生成、チャットボット、ワークフロー、ドキュメント処理など、Dify API経由のすべてのリクエストに適用されます。', + unlimitedApiRate: '無制限の API コール', + apiRateLimitTooltip: 'API レート制限は、テキスト生成、チャットボット、ワークフロー、ドキュメント処理など、Dify API 経由のすべてのリクエストに適用されます。', documentProcessingPriority: '文書処理', documentProcessingPriorityUpgrade: 'より高い精度と高速な速度でデータを処理します。', priority: { @@ -76,16 +76,16 @@ const translation = { emailSupport: 'メールサポート', priorityEmail: '優先メール&チャットサポート', logoChange: 'ロゴ変更', - SSOAuthentication: 'SSO認証', + SSOAuthentication: 'SSO 認証', personalizedSupport: '個別サポート', - dedicatedAPISupport: '専用APIサポート', + dedicatedAPISupport: '専用 API サポート', customIntegration: 'カスタム統合とサポート', - ragAPIRequest: 'RAG APIリクエスト', + ragAPIRequest: 'RAG API リクエスト', bulkUpload: 'ドキュメントの一括アップロード', agentMode: 'エージェントモード', workflow: 'ワークフロー', - llmLoadingBalancing: 'LLMロードバランシング', - llmLoadingBalancingTooltip: 'APIレート制限を効果的に回避するために、モデルに複数のAPIキーを追加する。', + llmLoadingBalancing: 'LLM ロードバランシング', + llmLoadingBalancingTooltip: 'API レート制限を効果的に回避するために、モデルに複数の API キーを追加する。', }, comingSoon: '近日公開', member: 'メンバー', @@ -93,13 +93,13 @@ const translation = { messageRequest: { title: '{{count,number}}メッセージクレジット', titlePerMonth: '{{count,number}}メッセージクレジット/月', - tooltip: 'メッセージクレジットは、DifyでさまざまなOpenAIモデルを簡単にお試しいただくためのものです。モデルタイプに応じてクレジットが消費され、使い切った後はご自身のOpenAI APIキーに切り替えていただけます。', + tooltip: 'メッセージクレジットは、Dify でさまざまな OpenAI モデルを簡単にお試しいただくためのものです。モデルタイプに応じてクレジットが消費され、使い切った後はご自身の OpenAI API キーに切り替えていただけます。', }, annotatedResponse: { title: '{{count,number}}の注釈クォータ制限', tooltip: '手動での回答の編集と注釈により、カスタマイズ可能な高品質の質問応答機能をアプリに提供します。(チャットアプリのみに適用)', }, - ragAPIRequestTooltip: 'Difyのナレッジベース処理機能のみを呼び出すAPI呼び出しの数を指します。', + ragAPIRequestTooltip: 'Dify のナレッジベース処理機能のみを呼び出す API 呼び出しの数を指します。', receiptInfo: 'チームオーナーとチーム管理者のみが購読および請求情報を表示できます', }, plans: { @@ -124,11 +124,11 @@ const translation = { description: 'オープンソース版の無料プラン', price: '無料', btnText: 'コミュニティ版を始めましょう', - includesTitle: '無料機能:', + includesTitle: '無料機能:', features: [ 'パブリックリポジトリの全コア機能', 'シングルワークスペース', - 'Difyオープンソースライセンス準拠', + 'Dify オープンソースライセンス準拠', ], }, premium: { @@ -138,12 +138,12 @@ const translation = { price: '従量制', priceTip: 'クラウドマーケットプレイス基準', btnText: 'プレミアム版を取得', - includesTitle: 'コミュニティ版機能に加えて:', + includesTitle: 'コミュニティ版機能に加えて:', comingSoon: 'Microsoft Azure & Google Cloud 近日対応', features: [ 'クラウドプロバイダーによる自己管理', 'シングルワークスペース', - 'Webアプリのロゴ&ブランドカスタマイズ', + 'Web アプリのロゴ&ブランドカスタマイズ', '優先メール/チャットサポート', ], }, @@ -154,14 +154,14 @@ const translation = { price: 'カスタム', priceTip: '年間契約専用', btnText: '営業に相談', - includesTitle: 'プレミアム版機能に加えて:', + includesTitle: 'プレミアム版機能に加えて:', features: [ 'エンタープライズ向け拡張ソリューション', '商用ライセンス認可', '企業専用機能', 'マルチワークスペース管理', 'シングルサインオン(SSO)', - 'DifyパートナーによるSLA保証', + 'Dify パートナーによる SLA 保証', '高度なセキュリティ管理', '公式メンテナンス&アップデート', 'プロフェッショナル技術支援', diff --git a/web/i18n/ja-JP/common.ts b/web/i18n/ja-JP/common.ts index 882e00cec8..85f5863761 100644 --- a/web/i18n/ja-JP/common.ts +++ b/web/i18n/ja-JP/common.ts @@ -115,9 +115,9 @@ const translation = { temperature: '温度', temperatureTip: 'ランダム性を制御します:温度を下げると、よりランダムな完成品が得られます。温度がゼロに近づくにつれて、モデルは決定的で反復的になります。', - top_p: '上位P', + top_p: '上位 P', top_pTip: - 'ニュークリアスサンプリングによる多様性の制御:0.5は、すべての尤度加重オプションの半分が考慮されることを意味します。', + 'ニュークリアスサンプリングによる多様性の制御:0.5 は、すべての尤度加重オプションの半分が考慮されることを意味します。', presence_penalty: '存在ペナルティ', presence_penaltyTip: 'これまでのテキストにトークンが表示されるかどうかに基づいて、新しいトークンにいくらペナルティを科すかを制御します。\nモデルが新しいトピックについて話す可能性が高まります。', @@ -126,11 +126,11 @@ const translation = { 'これまでのテキスト内のトークンの既存の頻度に基づいて、新しいトークンにどれだけペナルティを科すかを制御します。\nモデルが同じ行を文字通りに繰り返す可能性が低くなります。', max_tokens: '最大トークン', max_tokensTip: - '返信の最大長をトークン単位で制限するために使用されます。\n大きな値はプロンプトの単語、チャットログ、およびナレッジのために残されたスペースを制限する可能性があります。\nそれを2/3以下に設定することをお勧めします。\ngpt-4-1106-preview、gpt-4-vision-previewの最大トークン(入力128k出力4k)以下に設定することをお勧めします。', - maxTokenSettingTip: '最大トークン設定が高いため、プロンプト、クエリ、およびデータのスペースが制限される可能性があります。現在のモデルの最大トークンの80%以下に設定してください。', - setToCurrentModelMaxTokenTip: '最大トークンが現在のモデルの最大トークンの80%に更新されました {{maxToken}}.', + '返信の最大長をトークン単位で制限するために使用されます。\n大きな値はプロンプトの単語、チャットログ、およびナレッジのために残されたスペースを制限する可能性があります。\nそれを 2/3 以下に設定することをお勧めします。\ngpt-4-1106-preview、gpt-4-vision-preview の最大トークン(入力 128k 出力 4k)以下に設定することをお勧めします。', + maxTokenSettingTip: '最大トークン設定が高いため、プロンプト、クエリ、およびデータのスペースが制限される可能性があります。現在のモデルの最大トークンの 80% 以下に設定してください。', + setToCurrentModelMaxTokenTip: '最大トークンが現在のモデルの最大トークンの 80% に更新されました {{maxToken}}.', stop_sequences: '停止シーケンス', - stop_sequencesTip: 'APIが進行中のトークンの生成を停止する最大4つのシーケンス。返されたテキストには停止シーケンスは含まれません。', + stop_sequencesTip: 'API が進行中のトークンの生成を停止する最大 4 つのシーケンス。返されたテキストには停止シーケンスは含まれません。', stop_sequencesPlaceholder: 'シーケンスを入力してタブキーを押してください', }, tone: { @@ -150,9 +150,9 @@ const translation = { appDetail: 'アプリの詳細', account: 'アカウント', plugins: 'プラグイン', - pluginsTips: 'サードパーティのプラグインを統合するか、ChatGPT互換のAIプラグインを作成します。', + pluginsTips: 'サードパーティのプラグインを統合するか、ChatGPT 互換の AI プラグインを作成します。', datasets: 'ナレッジ', - datasetsTips: '近日公開:独自のテキストデータをインポートするか、Webhookを介してリアルタイムにデータを記述してLLMコンテキストを強化します。', + datasetsTips: '近日公開:独自のテキストデータをインポートするか、Webhook を介してリアルタイムにデータを記述して LLM コンテキストを強化します。', newApp: '新しいアプリ', newDataset: 'ナレッジの作成', tools: 'ツール', @@ -169,7 +169,7 @@ const translation = { communityFeedback: 'フィードバック', roadmap: 'ロードマップ', community: 'コミュニティ', - about: 'Difyについて', + about: 'Dify について', logout: 'ログアウト', github: 'GitHub', }, @@ -192,7 +192,7 @@ const translation = { provider: 'モデルプロバイダー', dataSource: 'データソース', plugin: 'プラグイン', - apiBasedExtension: 'API拡張', + apiBasedExtension: 'API 拡張', generalGroup: '一般', }, account: { @@ -206,7 +206,7 @@ const translation = { currentPassword: '現在のパスワード', newPassword: '新しいパスワード', confirmPassword: 'パスワードを確認', - notEqual: '2つのパスワードが異なります。', + notEqual: '2 つのパスワードが異なります。', langGeniusAccount: 'アカウント関連データ', langGeniusAccountTip: 'アカウントに関連するユーザーデータ。', editName: '名前を編集', @@ -223,7 +223,7 @@ const translation = { deleteLabel: '確認するには、以下にメールアドレスを入力してください', deletePlaceholder: 'メールアドレスを入力してください', verificationLabel: '認証コード', - verificationPlaceholder: '6桁のコードを貼り付けます', + verificationPlaceholder: '6 桁のコードを貼り付けます', permanentlyDeleteButton: 'アカウントを完全に削除', feedbackTitle: 'フィードバック', feedbackLabel: 'アカウントを削除した理由を教えてください。', @@ -260,7 +260,7 @@ const translation = { sendInvite: '招待を送る', invitedAsRole: '{{role}}ユーザーとして招待されました', invitationSent: '招待が送信されました', - invitationSentTip: '招待が送信され、彼らはDifyにサインインしてあなたのチームデータにアクセスできます。', + invitationSentTip: '招待が送信され、彼らは Dify にサインインしてあなたのチームデータにアクセスできます。', invitationLink: '招待リンク', failedInvitationEmails: '以下のユーザーは正常に招待されませんでした', ok: 'OK', @@ -277,9 +277,9 @@ const translation = { integrations: { connected: '接続済み', google: 'Google', - googleAccount: 'Googleアカウントでログイン', + googleAccount: 'Google アカウントでログイン', github: 'GitHub', - githubAccount: 'GitHubアカウントでログイン', + githubAccount: 'GitHub アカウントでログイン', connect: '接続', }, language: { @@ -287,29 +287,29 @@ const translation = { timezone: 'タイムゾーン', }, provider: { - apiKey: 'APIキー', - enterYourKey: 'ここにAPIキーを入力してください', - invalidKey: '無効なOpenAI APIキー', + apiKey: 'API キー', + enterYourKey: 'ここに API キーを入力してください', + invalidKey: '無効な OpenAI API キー', validatedError: '検証に失敗しました:', validating: 'キーの検証中...', - saveFailed: 'APIキーの保存に失敗しました', - apiKeyExceedBill: 'このAPI KEYには使用可能なクォータがありません。詳細は', + saveFailed: 'API キーの保存に失敗しました', + apiKeyExceedBill: 'この API KEY には使用可能なクォータがありません。詳細は', addKey: 'キーを追加', comingSoon: '近日公開', editKey: '編集', - invalidApiKey: '無効なAPIキー', + invalidApiKey: '無効な API キー', azure: { - apiBase: 'APIベース', - apiBasePlaceholder: 'Azure OpenAIエンドポイントのAPIベースURL。', - apiKey: 'APIキー', - apiKeyPlaceholder: 'ここにAPIキーを入力してください', - helpTip: 'Azure OpenAIサービスを学ぶ', + apiBase: 'API ベース', + apiBasePlaceholder: 'Azure OpenAI エンドポイントの API ベース URL。', + apiKey: 'API キー', + apiKeyPlaceholder: 'ここに API キーを入力してください', + helpTip: 'Azure OpenAI サービスを学ぶ', }, openaiHosted: { - openaiHosted: 'ホステッドOpenAI', + openaiHosted: 'ホステッド OpenAI', onTrial: 'トライアル中', exhausted: 'クォータが使い果たされました', - desc: 'Difyが提供するOpenAIホスティングサービスを使用すると、GPT-3.5などのモデルを使用できます。トライアルクォータが使い果たされる前に、他のモデルプロバイダを設定する必要があります。', + desc: 'Dify が提供する OpenAI ホスティングサービスを使用すると、GPT-3.5 などのモデルを使用できます。トライアルクォータが使い果たされる前に、他のモデルプロバイダを設定する必要があります。', callTimes: '通話回数', usedUp: 'トライアルクォータが使い果たされました。独自のモデルプロバイダを追加してください。', useYourModel: '現在、独自のモデルプロバイダを使用しています。', @@ -328,12 +328,12 @@ const translation = { }, anthropic: { using: '埋め込み機能は使用中です', - enableTip: 'Anthropicモデルを有効にするには、まずOpenAIまたはAzure OpenAIサービスにバインドする必要があります。', + enableTip: 'Anthropic モデルを有効にするには、まず OpenAI または Azure OpenAI サービスにバインドする必要があります。', notEnabled: '有効にされていません', - keyFrom: 'AnthropicからAPIキーを取得してください', + keyFrom: 'Anthropic から API キーを取得してください', }, encrypted: { - front: 'API KEYは', + front: 'API KEY は', back: '技術を使用して暗号化および保存されます。', }, }, @@ -361,8 +361,8 @@ const translation = { tip: '会話でのテキスト-to-音声入力に使用するデフォルトモデルを設定します。', }, rerankModel: { - key: 'Rerankモデル', - tip: 'Rerankモデルは、ユーザークエリとの意味的一致に基づいて候補文書リストを再配置し、意味的ランキングの結果を向上させます。', + key: 'Rerank モデル', + tip: 'Rerank モデルは、ユーザークエリとの意味的一致に基づいて候補文書リストを再配置し、意味的ランキングの結果を向上させます。', }, apiKey: 'API-キー', quota: 'クォータ', @@ -374,7 +374,7 @@ const translation = { tip: 'このモデルは削除されました。別のモデルを追加するか、別のモデルを選択してください。', emptyTip: '利用可能なモデルはありません', emptySetting: '設定に移動して構成してください', - rerankTip: 'Rerankモデルを設定してください', + rerankTip: 'Rerank モデルを設定してください', }, card: { quota: 'クォータ', @@ -385,17 +385,17 @@ const translation = { tokens: 'トークン', buyQuota: 'クォータを購入', priorityUse: '優先利用', - removeKey: 'APIキーを削除', + removeKey: 'API キーを削除', tip: '有料クォータは優先して使用されます。有料クォータを使用し終えた後、トライアルクォータが利用されます。', }, item: { deleteDesc: '{{modelName}}はシステムが推測するモデルとして利用されています。削除すると、一部の機能が使用不可能になる可能性があります。ご確認ください。', freeQuota: '無料のクォータ', }, - addApiKey: 'APIキーを追加', - invalidApiKey: '無効なAPIキー', + addApiKey: 'API キーを追加', + invalidApiKey: '無効な API キー', encrypted: { - front: 'APIキーは', + front: 'API キーは', back: ' の技術で暗号化されて保存されます。', }, freeQuota: { @@ -429,17 +429,17 @@ const translation = { providerManaged: 'プロバイダ管理', providerManagedDescription: 'モデルプロバイダによって提供される認証情報を使用します。', defaultConfig: 'デフォルトの設定', - apiKeyStatusNormal: 'APIキーの状態は正常', + apiKeyStatusNormal: 'API キーの状態は正常', apiKeyRateLimit: 'レート制限に到達しました。{{seconds}}秒後に再度利用可能です', addConfig: '設定を追加', editConfig: '設定を編集', - loadBalancingLeastKeyWarning: '負荷分散を利用するには、最低2つのキーを有効化する必要があります。', - loadBalancingInfo: 'デフォルトでは、負荷分散はラウンドロビン方式を採用しています。レート制限が発生した場合、1分間のクールダウン期間が適用されます。', + loadBalancingLeastKeyWarning: '負荷分散を利用するには、最低 2 つのキーを有効化する必要があります。', + loadBalancingInfo: 'デフォルトでは、負荷分散はラウンドロビン方式を採用しています。レート制限が発生した場合、1 分間のクールダウン期間が適用されます。', upgradeForLoadBalancing: '負荷分散を利用するには、プランのアップグレードが必要です。', emptyProviderTitle: 'モデルプロバイダーが設定されていません', discoverMore: 'もっと発見する', installProvider: 'モデルプロバイダーをインストールする', - configureTip: 'APIキーを設定するか、使用するモデルを追加してください', + configureTip: 'API キーを設定するか、使用するモデルを追加してください', toBeConfigured: '設定中', emptyProviderTip: '最初にモデルプロバイダーをインストールしてください。', }, @@ -449,7 +449,7 @@ const translation = { configure: '設定', notion: { title: 'Notion', - description: 'ナレッジデータソースとしてNotionを使用します。', + description: 'ナレッジデータソースとして Notion を使用します。', connectedWorkspace: '接続済みワークスペース', addWorkspace: 'ワークスペースの追加', connected: '接続済み', @@ -477,36 +477,36 @@ const translation = { }, plugin: { serpapi: { - apiKey: 'APIキー', - apiKeyPlaceholder: 'APIキーを入力してください', - keyFrom: 'SerpAPIアカウントページからSerpAPIキーを取得してください', + apiKey: 'API キー', + apiKeyPlaceholder: 'API キーを入力してください', + keyFrom: 'SerpAPI アカウントページから SerpAPI キーを取得してください', }, }, apiBasedExtension: { - title: 'API拡張機能は、Difyのアプリケーション全体での簡単な使用のための設定を簡素化し、集中的なAPI管理を提供します。', - link: '独自のAPI拡張機能を開発する方法について学ぶ。', + title: 'API 拡張機能は、Dify のアプリケーション全体での簡単な使用のための設定を簡素化し、集中的な API 管理を提供します。', + link: '独自の API 拡張機能を開発する方法について学ぶ。', linkUrl: 'https://docs.dify.ai/en/guides/extension/api-based-extension/README', - add: 'API拡張機能を追加', + add: 'API 拡張機能を追加', selector: { - title: 'API拡張機能', - placeholder: 'API拡張機能を選択してください', - manage: 'API拡張機能を管理', + title: 'API 拡張機能', + placeholder: 'API 拡張機能を選択してください', + manage: 'API 拡張機能を管理', }, modal: { - title: 'API拡張機能を追加', - editTitle: 'API拡張機能を編集', + title: 'API 拡張機能を追加', + editTitle: 'API 拡張機能を編集', name: { title: '名前', placeholder: '名前を入力してください', }, apiEndpoint: { - title: 'APIエンドポイント', - placeholder: 'APIエンドポイントを入力してください', + title: 'API エンドポイント', + placeholder: 'API エンドポイントを入力してください', }, apiKey: { - title: 'APIキー', - placeholder: 'APIキーを入力してください', - lengthError: 'APIキーの長さは5文字未満にできません', + title: 'API キー', + placeholder: 'API キーを入力してください', + lengthError: 'API キーの長さは 5 文字未満にできません', }, }, type: 'タイプ', @@ -520,7 +520,7 @@ const translation = { appMenus: { overview: '監視', promptEng: 'オーケストレート', - apiAccess: 'APIアクセス', + apiAccess: 'API アクセス', logAndAnn: 'ログ&アナウンス', logs: 'ログ', }, @@ -565,10 +565,10 @@ const translation = { citation: { title: '引用', linkToDataset: 'ナレッジへのリンク', - characters: '文字数:', - hitCount: '検索回数:', - vectorHash: 'ベクトルハッシュ:', - hitScore: '検索スコア:', + characters: '文字数:', + hitCount: '検索回数:', + vectorHash: 'ベクトルハッシュ:', + hitScore: '検索スコア:', }, inputPlaceholder: '{{botName}} と話す', thought: '思考', @@ -596,7 +596,7 @@ const translation = { modal: { title: '例', user: 'こんにちは', - assistant: 'こんにちは! 今日はどのようにお手伝いできますか?', + assistant: 'こんにちは!今日はどのようにお手伝いできますか?', edit: '会話の役割名を編集', }, }, @@ -650,7 +650,7 @@ const translation = { fileUploader: { uploadFromComputer: 'ローカルアップロード', pasteFileLink: 'ファイルリンクの貼り付け', - pasteFileLinkInputPlaceholder: 'URLを入力...', + pasteFileLinkInputPlaceholder: 'URL を入力...', uploadFromComputerLimit: 'アップロードファイルは{{size}}を超えてはなりません', uploadFromComputerUploadError: 'ファイルのアップロードに失敗しました。再度アップロードしてください。', uploadFromComputerReadError: 'ファイルの読み取りに失敗しました。もう一度やり直してください。', @@ -659,7 +659,7 @@ const translation = { }, license: { expiring_plural: '有効期限 {{count}} 日', - expiring: '1日で有効期限が切れます', + expiring: '1 日で有効期限が切れます', unlimited: '無制限', }, pagination: { @@ -668,7 +668,7 @@ const translation = { you: 'あなた', imageInput: { browse: 'ブラウズする', - supportedFormats: 'PNG、JPG、JPEG、WEBP、およびGIFをサポートしています。', + supportedFormats: 'PNG、JPG、JPEG、WEBP、および GIF をサポートしています。', dropImageHere: 'ここに画像をドロップするか、', }, } diff --git a/web/i18n/ja-JP/custom.ts b/web/i18n/ja-JP/custom.ts index d9750713c5..a7dcf4e7a8 100644 --- a/web/i18n/ja-JP/custom.ts +++ b/web/i18n/ja-JP/custom.ts @@ -7,14 +7,14 @@ const translation = { suffix: 'ブランドをカスタマイズしましょう。', }, webapp: { - title: 'WebAppブランドのカスタマイズ', - removeBrand: 'Powered by Difyを削除', - changeLogo: 'Powered byブランド画像を変更', - changeLogoTip: '最小サイズ40x40pxのSVGまたはPNG形式', + title: 'WebApp ブランドのカスタマイズ', + removeBrand: 'Powered by Dify を削除', + changeLogo: 'Powered by ブランド画像を変更', + changeLogoTip: '最小サイズ 40x40px の SVG または PNG 形式', }, app: { title: 'アプリヘッダーブランドのカスタマイズ', - changeLogoTip: '最小サイズ80x80pxのSVGまたはPNG形式', + changeLogoTip: '最小サイズ 80x80px の SVG または PNG 形式', }, upload: 'アップロード', uploading: 'アップロード中', diff --git a/web/i18n/ja-JP/dataset-creation.ts b/web/i18n/ja-JP/dataset-creation.ts index a3f573cab1..15bee57c0b 100644 --- a/web/i18n/ja-JP/dataset-creation.ts +++ b/web/i18n/ja-JP/dataset-creation.ts @@ -11,31 +11,31 @@ const translation = { unavailable: 'このナレッジベースは利用できません', }, firecrawl: { - configFirecrawl: '🔥Firecrawlの設定', - apiKeyPlaceholder: 'firecrawl.devからのAPIキー', - getApiKeyLinkText: 'firecrawl.devからAPIキーを取得する', + configFirecrawl: '🔥Firecrawl の設定', + apiKeyPlaceholder: 'firecrawl.dev からの API キー', + getApiKeyLinkText: 'firecrawl.dev から API キーを取得する', }, jinaReader: { - getApiKeyLinkText: '無料のAPIキーを jina.ai で取得', + getApiKeyLinkText: '無料の API キーを jina.ai で取得', apiKeyPlaceholder: 'jina.ai からの API キー', - configJinaReader: 'Jina Readerの設定', + configJinaReader: 'Jina Reader の設定', }, stepOne: { filePreview: 'ファイルプレビュー', pagePreview: 'ページプレビュー', dataSourceType: { file: 'テキストファイルからインポート', - notion: 'Notionから同期', + notion: 'Notion から同期', web: 'ウェブサイトから同期', }, uploader: { title: 'テキストファイルをアップロード', button: 'ファイルまたはフォルダをドラッグアンドドロップする', browse: '参照', - tip: '{{supportTypes}}をサポートしています。1つあたりの最大サイズは{{size}}MBです。', + tip: '{{supportTypes}}をサポートしています。1 つあたりの最大サイズは{{size}}MB です。', validation: { typeError: 'サポートされていないファイルタイプです', - size: 'ファイルサイズが大きすぎます。最大サイズは{{size}}MBです', + size: 'ファイルサイズが大きすぎます。最大サイズは{{size}}MB です', count: '複数のファイルはサポートされていません', filesNumber: 'バッチアップロードの制限({{filesNumber}}個)に達しました。', }, @@ -43,8 +43,8 @@ const translation = { change: '変更', failed: 'アップロードに失敗しました', }, - notionSyncTitle: 'Notionが接続されていません', - notionSyncTip: 'Notionと同期するには、まずNotionへの接続が必要です。', + notionSyncTitle: 'Notion が接続されていません', + notionSyncTip: 'Notion と同期するには、まず Notion への接続が必要です。', connect: '接続する', cancel: 'キャンセル', button: '次へ', @@ -55,49 +55,49 @@ const translation = { input: 'ナレッジベースの名称', placeholder: '入力してください', nameNotEmpty: '名前は空にできません', - nameLengthInvalid: '名前は1〜40文字である必要があります', + nameLengthInvalid: '名前は 1〜40 文字である必要があります', cancelButton: 'キャンセル', confirmButton: '作成', failed: '作成に失敗しました', }, website: { chooseProvider: 'プロバイダーを選択する', - fireCrawlNotConfigured: 'Firecrawlが設定されていません', + fireCrawlNotConfigured: 'Firecrawl が設定されていません', fireCrawlNotConfiguredDescription: 'Firecrawl を使用するには、Firecrawl の API キーを設定してください。', jinaReaderNotConfigured: 'Jina Reader が設定されていません', - jinaReaderNotConfiguredDescription: '無料のAPIキーを入力して、Jina Readerを設定します。', + jinaReaderNotConfiguredDescription: '無料の API キーを入力して、Jina Reader を設定します。', configure: '設定', configureFirecrawl: '配置 Firecrawl', configureJinaReader: '配置 Jina Reader', run: '実行', - firecrawlTitle: '🔥Firecrawlを使っでウエブコンテンツを抽出', - firecrawlDoc: 'Firecrawlドキュメント', + firecrawlTitle: '🔥Firecrawl を使っでウエブコンテンツを抽出', + firecrawlDoc: 'Firecrawl ドキュメント', firecrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', - jinaReaderTitle: 'サイト全体をMarkdownに変換する', - jinaReaderDoc: 'Jina Readerの詳細', + jinaReaderTitle: 'サイト全体を Markdown に変換する', + jinaReaderDoc: 'Jina Reader の詳細', jinaReaderDocLink: 'https://jina.ai/reader', - useSitemap: 'sitemap(サイトマップ)を使用する', - useSitemapTooltip: 'サイトマップに沿ってサイトをクロールします。そうでない場合、Jina Readerはページの関連性に基づいて繰り返しクロールし、ページ数は少なくなりますが、高品質のページが得られます。', + useSitemap: 'sitemap(サイトマップ) を使用する', + useSitemapTooltip: 'サイトマップに沿ってサイトをクロールします。そうでない場合、Jina Reader はページの関連性に基づいて繰り返しクロールし、ページ数は少なくなりますが、高品質のページが得られます。', options: 'オプション', crawlSubPage: 'サブページをクロールする', limit: '制限', maxDepth: '最大深度', excludePaths: 'パスを除外する', includeOnlyPaths: 'パスのみを含める', - extractOnlyMainContent: 'メインコンテンツのみを抽出する(ヘッダー、ナビ、フッターなどは抽出しない)', - exceptionErrorTitle: 'Firecrawl ジョブの実行中に例外が発生しました:', + extractOnlyMainContent: 'メインコンテンツのみを抽出する (ヘッダー、ナビ、フッターなどは抽出しない)', + exceptionErrorTitle: 'Firecrawl ジョブの実行中に例外が発生しました:', unknownError: '不明なエラー', - totalPageScraped: 'スクレイピングされた総ページ数:', + totalPageScraped: 'スクレイピングされた総ページ数:', selectAll: 'すべて選択', resetAll: 'すべてリセット', scrapTimeInfo: '{{time}} 秒以内に合計 {{total}} ページをスクレイピングしました', preview: 'プレビュー', - maxDepthTooltip: '入力されたURLを基にしたクローリング作業での設定可能な最大深度について説明します。深度0は入力されたURL自体のページを対象としたスクレイピングを意味します。深度1では、元のURLの直下にあるページ(URLに続く最初の"/"以降の内容)もスクレイピングの対象になります。この深度は指定した数値まで増加させることができ、それに応じてスクレイピングの範囲も広がっていきます。', - waterCrawlNotConfiguredDescription: 'APIキーを使ってWatercrawlを設定します。', + maxDepthTooltip: '入力された URL を基にしたクローリング作業での設定可能な最大深度について説明します。深度 0 は入力された URL 自体のページを対象としたスクレイピングを意味します。深度 1 では、元の URL の直下にあるページ(URL に続く最初の"/"以降の内容)もスクレイピングの対象になります。この深度は指定した数値まで増加させることができ、それに応じてスクレイピングの範囲も広がっていきます。', + waterCrawlNotConfiguredDescription: 'API キーを使って Watercrawl を設定します。', configureWatercrawl: 'ウォータークローラーを設定する', watercrawlDoc: 'ウォータークローリングの文書', - watercrawlTitle: 'Watercrawlを使用してウェブコンテンツを抽出する', - waterCrawlNotConfigured: 'Watercrawlは設定されていません', + watercrawlTitle: 'Watercrawl を使用してウェブコンテンツを抽出する', + waterCrawlNotConfigured: 'Watercrawl は設定されていません', watercrawlDocLink: 'https://docs.dify.ai/ja/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', }, }, @@ -110,22 +110,22 @@ const translation = { general: '汎用', generalTip: '汎用テキスト分割モードです。検索とコンテキスト抽出に同じチャンクを使用します。', parentChild: '親子', - parentChildTip: '親子分割モード(階層分割モード)では、子チャンクを検索に、親チャンクをコンテキスト抽出に使用します。', + parentChildTip: '親子分割モード (階層分割モード) では、子チャンクを検索に、親チャンクをコンテキスト抽出に使用します。', parentChunkForContext: 'コンテキスト用親チャンク', childChunkForRetrieval: '検索用子チャンク', paragraph: '段落', paragraphTip: '区切り文字と最大チャンク長に基づいてテキストを段落に分割し、分割されたテキストを検索用の親チャンクとして使用します。', fullDoc: '全文', - fullDocTip: 'ドキュメント全体を親チャンクとして使用し、直接検索します。パフォーマンス上の理由から、10000トークンを超えるテキストは自動的に切り捨てられます。', + fullDocTip: 'ドキュメント全体を親チャンクとして使用し、直接検索します。パフォーマンス上の理由から、10000 トークンを超えるテキストは自動的に切り捨てられます。', separator: 'チャンク識別子', separatorPlaceholder: '例えば改行(\\\\n)や特殊なセパレータ(例:「***」)', maxLength: '最大チャンク長', overlap: 'チャンクのオーバーラップ', - overlapTip: 'チャンクのオーバーラップを設定することで、それらの間の意味的な関連性を維持し、検索効果を向上させることができます。最大チャンクサイズの10%〜25%を設定することをおすすめします。', + overlapTip: 'チャンクのオーバーラップを設定することで、それらの間の意味的な関連性を維持し、検索効果を向上させることができます。最大チャンクサイズの 10%〜25% を設定することをおすすめします。', overlapCheck: 'チャンクのオーバーラップは最大チャンク長を超えてはいけません', rules: 'テキストの前処理ルール', removeExtraSpaces: '連続するスペース、改行、タブを置換する', - removeUrlEmails: 'すべてのURLとメールアドレスを削除する', + removeUrlEmails: 'すべての URL とメールアドレスを削除する', removeStopwords: '「a」「an」「the」などのストップワードを削除する', preview: 'プレビュー', previewChunk: 'チャンクをプレビュー', @@ -134,15 +134,15 @@ const translation = { qualified: '高品質', highQualityTip: '高品質モードで埋め込みを終了したら、経済的モードに戻すことはできません。', recommend: '推奨', - qualifiedTip: '埋め込みモデルを呼び出してドキュメントを処理し、より正確な検索を行うと、LLMが高品質の回答を生成するのに役立ちます。', - warning: 'モデルプロバイダのAPIキーを設定してください。', + qualifiedTip: '埋め込みモデルを呼び出してドキュメントを処理し、より正確な検索を行うと、LLM が高品質の回答を生成するのに役立ちます。', + warning: 'モデルプロバイダの API キーを設定してください。', click: '設定に移動', economical: '経済的', - economicalTip: '検索時にチャンクあたり10個のキーワードを使用することで、精度は低下しますが、トークン消費を抑えられます。', + economicalTip: '検索時にチャンクあたり 10 個のキーワードを使用することで、精度は低下しますが、トークン消費を抑えられます。', QATitle: '質問と回答形式でセグメント化', QATip: 'このオプションを有効にすると、追加のトークンが消費されます', QALanguage: '使用言語', - useQALanguage: 'Q&A形式で分割', + useQALanguage: 'Q&A 形式で分割', estimateCost: '見積もり', estimateSegment: '推定チャンク数', segmentCount: 'チャンク', @@ -159,28 +159,28 @@ const translation = { save: '保存して処理', cancel: 'キャンセル', sideTipTitle: 'なぜチャンクと前処理が必要なのか', - sideTipP1: 'テキストデータを処理する際、チャンクとクリーニングは2つの重要な前処理ステップです。', + sideTipP1: 'テキストデータを処理する際、チャンクとクリーニングは 2 つの重要な前処理ステップです。', sideTipP2: 'セグメンテーションは長いテキストを段落に分割し、モデルがより理解しやすくします。これにより、モデルの結果の品質と関連性が向上します。', sideTipP3: 'クリーニングは不要な文字や書式を削除し、ナレッジベースをよりクリーンで解析しやすいものにします。', sideTipP4: '適切なチャンクとクリーニングはモデルのパフォーマンスを向上させ、より正確で価値のある結果を提供します。', previewTitle: 'プレビュー', previewTitleButton: 'プレビュー', - previewButton: 'Q&A形式に切り替える', + previewButton: 'Q&A 形式に切り替える', previewSwitchTipStart: '現在のチャンクプレビューはテキスト形式です。質問と回答形式のプレビューに切り替えると、', previewSwitchTipEnd: ' 追加のトークンが消費されます', characters: '文字', indexSettingTip: 'インデックス方法を変更するには、', retrievalSettingTip: '検索方法を変更するには、', datasetSettingLink: 'ナレッジベース設定', - separatorTip: '区切り文字は、テキストを区切るために使用される文字です。\\n\\n と \\n は、段落と行を区切るために一般的に使用される区切り記号です。カンマ (\\n\\n,\\n) と組み合わせると、最大チャンク長を超えると、段落は行で区切られます。自分で定義した特別な区切り文字を使用することもできます(例:***)。', + separatorTip: '区切り文字は、テキストを区切るために使用される文字です。\\n\\n と \\n は、段落と行を区切るために一般的に使用される区切り記号です。カンマ (\\n\\n,\\n) と組み合わせると、最大チャンク長を超えると、段落は行で区切られます。自分で定義した特別な区切り文字を使用することもできます (例:***)。', maxLengthCheck: 'チャンクの最大長は {{limit}} 未満にする必要があります', previewChunkTip: 'プレビューを読み込むには、左側の \'チャンクをプレビュー\' ボタンをクリックしてください', - previewChunkCount: '推定チャンク数: {{count}}', + previewChunkCount: '推定チャンク数:{{count}}', switch: '切り替え', - qaSwitchHighQualityTipTitle: 'Q&A形式には高品質なインデックスが必要です', - qaSwitchHighQualityTipContent: '現在、高品質なインデックス作成のみがQ&A形式の分割をサポートしています。高品質モードに切り替えますか?', + qaSwitchHighQualityTipTitle: 'Q&A 形式には高品質なインデックスが必要です', + qaSwitchHighQualityTipContent: '現在、高品質なインデックス作成のみが Q&A 形式の分割をサポートしています。高品質モードに切り替えますか?', notAvailableForParentChild: '親子インデックスでは利用できません', - notAvailableForQA: 'Q&Aインデックスでは利用できません', + notAvailableForQA: 'Q&A インデックスでは利用できません', parentChildDelimiterTip: '区切り文字とは、テキストを分割するために使用される文字です。\\n\\n は、元のドキュメントを大きな親チャンクに分割する際におすすめです。独自の区切り文字も使用できます。', parentChildChunkDelimiterTip: '区切り文字とは、テキストを分割するために使用される文字です。\\n は、親チャンクを小さな子チャンクに分割する際におすすめです。独自の区切り文字も使用できます。', }, @@ -195,7 +195,7 @@ const translation = { resume: '処理を再開', navTo: 'ドキュメントに移動', sideTipTitle: '次は何ですか', - sideTipContent: 'ドキュメントのインデックスが完了したら、ナレッジベースをアプリケーションのコンテキストとして統合することができます。プロンプトオーケストレーションページでコンテキスト設定を見つけることができます。また、独立したChatGPTインデックスプラグインとしてリリースすることもできます。', + sideTipContent: 'ドキュメントのインデックスが完了したら、ナレッジベースをアプリケーションのコンテキストとして統合することができます。プロンプトオーケストレーションページでコンテキスト設定を見つけることができます。また、独立した ChatGPT インデックスプラグインとしてリリースすることもできます。', modelTitle: '埋め込みを停止してもよろしいですか?', modelContent: '後で処理を再開する必要がある場合は、中断した場所から続行します。', modelButtonConfirm: '確認', @@ -203,13 +203,13 @@ const translation = { }, otherDataSource: { title: '他のデータソースと接続しますか?', - description: '現在、Difyのナレッジベースには利用できるデータソースが限られています。Difyのナレッジベースにデータソースを提供いただくことは、プラットフォームの柔軟性と能力を向上させる上で非常に有益です。貢献ガイドをご用意していますので、ぜひご協力ください。詳細については、以下のリンクをクリックしてください。', + description: '現在、Dify のナレッジベースには利用できるデータソースが限られています。Dify のナレッジベースにデータソースを提供いただくことは、プラットフォームの柔軟性と能力を向上させる上で非常に有益です。貢献ガイドをご用意していますので、ぜひご協力ください。詳細については、以下のリンクをクリックしてください。', learnMore: '詳細はこちら', }, watercrawl: { - getApiKeyLinkText: 'watercrawl.devからAPIキーを取得してください。', + getApiKeyLinkText: 'watercrawl.dev から API キーを取得してください。', configWatercrawl: 'ウォータークローラーを設定する', - apiKeyPlaceholder: 'watercrawl.devからのAPIキー', + apiKeyPlaceholder: 'watercrawl.dev からの API キー', }, } diff --git a/web/i18n/ja-JP/dataset-documents.ts b/web/i18n/ja-JP/dataset-documents.ts index 81047872ad..ecdbbf512c 100644 --- a/web/i18n/ja-JP/dataset-documents.ts +++ b/web/i18n/ja-JP/dataset-documents.ts @@ -1,11 +1,11 @@ const translation = { list: { title: 'ドキュメント', - desc: 'すべてのファイルがここに表示され、ナレッジベース全体がDifyの引用やチャットプラグインを介してリンクされるか、インデックス化されることができます。', + desc: 'すべてのファイルがここに表示され、ナレッジベース全体が Dify の引用やチャットプラグインを介してリンクされるか、インデックス化されることができます。', learnMore: '詳細はこちら', addFile: 'ファイルを追加', addPages: 'ページを追加', - addUrl: 'URLを追加', + addUrl: 'URL を追加', table: { header: { fileName: 'ファイル名', @@ -51,10 +51,10 @@ const translation = { empty: { title: 'まだドキュメントがありません', upload: { - tip: 'ファイルをアップロードしたり、ウェブサイトから同期したり、NotionやGitHubなどのWebアプリから同期することができます。', + tip: 'ファイルをアップロードしたり、ウェブサイトから同期したり、Notion や GitHub などの Web アプリから同期することができます。', }, sync: { - tip: 'Difyは定期的にNotionからファイルをダウンロードし、処理を完了します。', + tip: 'Dify は定期的に Notion からファイルをダウンロードし、処理を完了します。', }, }, delete: { @@ -63,9 +63,9 @@ const translation = { }, batchModal: { title: '一括追加', - csvUploadTitle: 'CSVファイルをここにドラッグアンドドロップするか、', + csvUploadTitle: 'CSV ファイルをここにドラッグアンドドロップするか、', browse: '参照', - tip: 'CSVファイルは次の構造に準拠する必要があります:', + tip: 'CSV ファイルは次の構造に準拠する必要があります:', question: '質問', answer: '回答', contentTitle: 'チャンクの内容', @@ -82,8 +82,8 @@ const translation = { }, metadata: { title: 'メタデータ', - desc: 'ドキュメントのメタデータにラベルを付けることで、AIがタイムリーにアクセスできるようになり、ユーザーに参照元が公開されます。', - dateTimeFormat: 'YYYY年M月D日 hh:mm A', + desc: 'ドキュメントのメタデータにラベルを付けることで、AI がタイムリーにアクセスできるようになり、ユーザーに参照元が公開されます。', + dateTimeFormat: 'YYYY 年 M 月 D 日 hh:mm A', docTypeSelectTitle: 'ドキュメントタイプを選択してください', docTypeChangeTitle: 'ドキュメントタイプを変更', docTypeSelectWarning: @@ -95,8 +95,8 @@ const translation = { }, source: { upload_file: 'ファイルをアップロード', - notion: 'Notionから同期', - github: 'GitHubから同期', + notion: 'Notion から同期', + github: 'GitHub から同期', }, type: { book: '書籍', @@ -105,10 +105,10 @@ const translation = { socialMediaPost: 'ソーシャルメディアの投稿', personalDocument: '個人のドキュメント', businessDocument: 'ビジネスドキュメント', - IMChat: 'IMチャット', - wikipediaEntry: 'Wikipediaのエントリー', - notion: 'Notionから同期', - github: 'GitHubから同期', + IMChat: 'IM チャット', + wikipediaEntry: 'Wikipedia のエントリー', + notion: 'Notion から同期', + github: 'GitHub から同期', technicalParameters: '技術的なパラメータ', }, field: { @@ -151,7 +151,7 @@ const translation = { platform: 'プラットフォーム', authorUsername: '著者/ユーザー名', publishDate: '公開日', - postURL: '投稿URL', + postURL: '投稿 URL', topicsTags: 'トピック/タグ', }, personalDocument: { @@ -182,7 +182,7 @@ const translation = { wikipediaEntry: { title: 'タイトル', language: '言語', - webpageURL: 'ウェブページURL', + webpageURL: 'ウェブページ URL', editorContributor: '編集者/寄稿者', lastEditDate: '最終編集日', summaryIntroduction: '概要/紹介', @@ -355,11 +355,11 @@ const translation = { newChildChunk: '新しい子チャンク', keywords: 'キーワード', addKeyWord: 'キーワードを追加', - keywordError: 'キーワードの最大長は20です', + keywordError: 'キーワードの最大長は 20 です', characters_one: '文字', characters_other: '文字', hitCount: '検索回数', - vectorHash: 'ベクトルハッシュ: ', + vectorHash: 'ベクトルハッシュ:', questionPlaceholder: 'ここに質問を追加', questionEmpty: '質問は空にできません', answerPlaceholder: 'ここに回答を追加', @@ -367,7 +367,7 @@ const translation = { contentPlaceholder: 'ここに内容を追加', contentEmpty: '内容は空にできません', newTextSegment: '新しいテキストチャンク', - newQaSegment: '新しいQ&Aチャンク', + newQaSegment: '新しい Q&A チャンク', addChunk: 'チャンクを追加', addChildChunk: '子チャンクを追加', addAnother: '続けて追加', diff --git a/web/i18n/ja-JP/dataset-hit-testing.ts b/web/i18n/ja-JP/dataset-hit-testing.ts index 7b00455636..9e6f5e6c05 100644 --- a/web/i18n/ja-JP/dataset-hit-testing.ts +++ b/web/i18n/ja-JP/dataset-hit-testing.ts @@ -13,7 +13,7 @@ const translation = { input: { title: 'ソーステキスト', placeholder: 'テキストを入力してください。短い記述文がおすすめです。', - countWarning: '最大200文字まで入力できます。', + countWarning: '最大 200 文字まで入力できます。', indexWarning: '高品質のナレッジのみ。', testing: 'テスト中', }, diff --git a/web/i18n/ja-JP/dataset-settings.ts b/web/i18n/ja-JP/dataset-settings.ts index b0fd2ec55b..87a48dd005 100644 --- a/web/i18n/ja-JP/dataset-settings.ts +++ b/web/i18n/ja-JP/dataset-settings.ts @@ -7,7 +7,7 @@ const translation = { nameError: '名前は空にできません', desc: 'ナレッジベースの説明', descInfo: 'ナレッジベースの内容を概説するための明確なテキストの説明を書いてください。この説明は、複数のナレッジから推論を選択する際の基準として使用されます。', - descPlaceholder: 'このデータセットの内容を記述してください。詳細に記述することで、AIがデータセットの内容に迅速にアクセスできるようになります。空欄の場合、LangGeniusはデフォルトの検索方法を使用します。', + descPlaceholder: 'このデータセットの内容を記述してください。詳細に記述することで、AI がデータセットの内容に迅速にアクセスできるようになります。空欄の場合、LangGenius はデフォルトの検索方法を使用します。', helpText: '適切なデータセットの説明を作成する方法を学びましょう。', descWrite: '良いナレッジベースの説明の書き方を学ぶ。', permissions: '権限', @@ -17,10 +17,10 @@ const translation = { me: '(あなた)', indexMethod: 'インデックス方法', indexMethodHighQuality: '高品質', - indexMethodHighQualityTip: 'より正確な検索のため、埋め込みモデルを呼び出してドキュメントを処理することで、LLMは高品質な回答を生成できます。', + indexMethodHighQualityTip: 'より正確な検索のため、埋め込みモデルを呼び出してドキュメントを処理することで、LLM は高品質な回答を生成できます。', upgradeHighQualityTip: '高品質モードにアップグレードすると、経済的モードには戻せません。', indexMethodEconomy: '経済的', - indexMethodEconomyTip: 'チャンクあたり10個のキーワードを検索に使用します。トークンは消費しませんが、検索精度は低下します。', + indexMethodEconomyTip: 'チャンクあたり 10 個のキーワードを検索に使用します。トークンは消費しませんが、検索精度は低下します。', embeddingModel: '埋め込みモデル', embeddingModelTip: '埋め込みモデルを変更するには、', embeddingModelTipLink: '設定', @@ -32,10 +32,10 @@ const translation = { longDescription: ' 検索方法についての詳細については、いつでもナレッジベースの設定で変更できます。', }, save: '保存', - externalKnowledgeID: '外部ナレッジベースID', + externalKnowledgeID: '外部ナレッジベース ID', retrievalSettings: '取得設定', - externalKnowledgeAPI: '外部ナレッジベースAPI', - indexMethodChangeToEconomyDisabledTip: 'HQからECOへのダウングレードはできません。', + externalKnowledgeAPI: '外部ナレッジベース API', + indexMethodChangeToEconomyDisabledTip: 'HQ から ECO へのダウングレードはできません。', searchModel: 'モデル検索', }, } diff --git a/web/i18n/ja-JP/dataset.ts b/web/i18n/ja-JP/dataset.ts index 1078fee69d..2bdc4a8d28 100644 --- a/web/i18n/ja-JP/dataset.ts +++ b/web/i18n/ja-JP/dataset.ts @@ -9,33 +9,33 @@ const translation = { fullDoc: '全体', }, externalTag: '外部', - externalAPI: '外部API', - externalAPIPanelTitle: '外部ナレッジベース連携API', - externalKnowledgeId: '外部ナレッジベースID', + externalAPI: '外部 API', + externalAPIPanelTitle: '外部ナレッジベース連携 API', + externalKnowledgeId: '外部ナレッジベース ID', externalKnowledgeName: '外部ナレッジベース名', externalKnowledgeDescription: 'ナレッジベースの説明', - externalKnowledgeIdPlaceholder: 'ナレッジベースIDを入力', + externalKnowledgeIdPlaceholder: 'ナレッジベース ID を入力', externalKnowledgeNamePlaceholder: 'ナレッジベース名を入力', externalKnowledgeDescriptionPlaceholder: 'このナレッジベースの説明(任意)', learnHowToWriteGoodKnowledgeDescription: '効果的なナレッジベースの説明の書き方', - externalAPIPanelDescription: '外部ナレッジベース連携APIは、Dify外のナレッジベースと連携し、そこからナレッジベースを取得するために使用します。', - externalAPIPanelDocumentation: '外部ナレッジベース連携APIの作成方法', + externalAPIPanelDescription: '外部ナレッジベース連携 API は、Dify 外のナレッジベースと連携し、そこからナレッジベースを取得するために使用します。', + externalAPIPanelDocumentation: '外部ナレッジベース連携 API の作成方法', localDocs: 'ローカルドキュメント', documentCount: ' ドキュメント', wordCount: ' k 単語', appCount: ' リンクされたアプリ', createDataset: 'ナレッジベースを作成', - createNewExternalAPI: '新しい外部ナレッジベース連携APIを作成', - noExternalKnowledge: '外部ナレッジベース連携APIがありません。ここをクリックして作成してください', - createExternalAPI: '外部ナレッジベース連携APIを追加', - editExternalAPIFormTitle: '外部ナレッジベース連携APIを編集', + createNewExternalAPI: '新しい外部ナレッジベース連携 API を作成', + noExternalKnowledge: '外部ナレッジベース連携 API がありません。ここをクリックして作成してください', + createExternalAPI: '外部ナレッジベース連携 API を追加', + editExternalAPIFormTitle: '外部ナレッジベース連携 API を編集', editExternalAPITooltipTitle: '連携中のナレッジベース', editExternalAPIConfirmWarningContent: { - front: 'この外部ナレッジベース連携APIは', + front: 'この外部ナレッジベース連携 API は', end: '件の外部ナレッジベースと連携しており、この変更はすべてに適用されます。変更を保存しますか?', }, editExternalAPIFormWarning: { - front: 'この外部APIは', + front: 'この外部 API は', end: '件の外部ナレッジベースと連携しています', }, deleteExternalAPIConfirmWarningContent: { @@ -44,32 +44,32 @@ const translation = { end: 'しますか?', }, content: { - front: 'この外部ナレッジベース連携APIは', - end: '件の外部ナレッジベースと連携しています。このAPIを削除すると、すべて無効になります。このAPIを削除しますか?', + front: 'この外部ナレッジベース連携 API は', + end: '件の外部ナレッジベースと連携しています。この API を削除すると、すべて無効になります。この API を削除しますか?', }, - noConnectionContent: 'このAPIを削除しますか?', + noConnectionContent: 'この API を削除しますか?', }, selectExternalKnowledgeAPI: { - placeholder: '外部ナレッジベース連携APIを選択', + placeholder: '外部ナレッジベース連携 API を選択', }, connectDataset: '外部ナレッジベースと連携', connectDatasetIntro: { title: '外部ナレッジベースとの連携方法', content: { - front: '外部ナレッジベースと連携するには、まず外部APIを作成する必要があります。以下の手順を参照し、', - link: '外部APIの作成方法', - end: 'をご確認ください。次に、対応するナレッジベースIDを左側のフォームに入力してください。すべての情報が正しければ、連携ボタンをクリックすると、自動的にナレッジベースの検索テストに移動します。', + front: '外部ナレッジベースと連携するには、まず外部 API を作成する必要があります。以下の手順を参照し、', + link: '外部 API の作成方法', + end: 'をご確認ください。次に、対応するナレッジベース ID を左側のフォームに入力してください。すべての情報が正しければ、連携ボタンをクリックすると、自動的にナレッジベースの検索テストに移動します。', }, learnMore: '詳細はこちら', }, connectHelper: { - helper1: 'APIとナレッジベースIDを使って外部ナレッジベースと連携します。現在、', + helper1: 'API とナレッジベース ID を使って外部ナレッジベースと連携します。現在、', helper2: '検索機能のみがサポートされています。', helper3: 'この機能を使用する前に、', helper4: 'ヘルプドキュメント', helper5: 'をよくお読みください。', }, - createDatasetIntro: '独自のテキストデータをインポートするか、LLMコンテキストの強化のためにWebhookを介してリアルタイムでデータを書き込むことができます。', + createDatasetIntro: '独自のテキストデータをインポートするか、LLM コンテキストの強化のために Webhook を介してリアルタイムでデータを書き込むことができます。', deleteDatasetConfirmTitle: 'このナレッジベースを削除しますか?', deleteDatasetConfirmContent: 'ナレッジベースを削除すると元に戻すことはできません。ユーザーはもはやあなたのナレッジベースにアクセスできず、すべてのプロンプトの設定とログが永久に削除されます。', @@ -77,12 +77,12 @@ const translation = { datasetDeleted: 'ナレッジベースが削除されました', datasetDeleteFailed: 'ナレッジベースの削除に失敗しました', didYouKnow: 'ご存知ですか?', - intro1: 'ナレッジベースはDifyアプリケーションに統合することができます', + intro1: 'ナレッジベースは Dify アプリケーションに統合することができます', intro2: 'コンテキストとして', intro3: '、', intro4: 'または', intro5: '作成することができます', - intro6: '単体のChatGPTインデックスプラグインとして公開するために', + intro6: '単体の ChatGPT インデックスプラグインとして公開するために', unavailable: '利用不可', unavailableTip: '埋め込みモデルが利用できません。デフォルトの埋め込みモデルを設定する必要があります', datasets: 'ナレッジベース', @@ -93,13 +93,13 @@ const translation = { }, externalAPIForm: { name: '名前', - endpoint: 'APIエンドポイント', - apiKey: 'APIキー', + endpoint: 'API エンドポイント', + apiKey: 'API キー', save: '保存', cancel: 'キャンセル', edit: '編集', encrypted: { - front: 'APIトークンは', + front: 'API トークンは', end: '技術で暗号化され、安全に保存されます。', }, }, @@ -114,19 +114,19 @@ const translation = { }, hybrid_search: { title: 'ハイブリッド検索', - description: '全文検索とベクトル検索を同時に実行し、ユーザーのクエリに最適なマッチを選択するためにRerank付けを行います。RerankモデルAPIの設定が必要です。', + description: '全文検索とベクトル検索を同時に実行し、ユーザーのクエリに最適なマッチを選択するために Rerank 付けを行います。Rerank モデル API の設定が必要です。', recommend: '推奨', }, invertedIndex: { title: '転置インデックス', - description: '効率的な検索に使用される構造です。各用語が含まれるドキュメントまたはWebページを指すように、用語ごとに整理されています。', + description: '効率的な検索に使用される構造です。各用語が含まれるドキュメントまたは Web ページを指すように、用語ごとに整理されています。', }, change: '変更', changeRetrievalMethod: '検索方法の変更', }, docsFailedNotice: 'ドキュメントのインデックス作成に失敗しました', retry: '再試行', - documentsDisabled: '{{num}}件のドキュメントが無効 - 30日以上非アクティブ', + documentsDisabled: '{{num}}件のドキュメントが無効 - 30 日以上非アクティブ', enable: '有効化', indexingTechnique: { high_quality: '高品質', @@ -139,12 +139,12 @@ const translation = { invertedIndex: '転置', }, defaultRetrievalTip: 'デフォルトでは、マルチパス検索が使用されます。複数のナレッジベースから情報を取得した後、再ランキングを行います。', - mixtureHighQualityAndEconomicTip: '高品質なナレッジベースとコスト重視のナレッジベースを混在させるには、Rerankモデルが必要です。', - inconsistentEmbeddingModelTip: '選択されたナレッジベースの埋め込みモデルに一貫性がない場合、Rerankモデルが必要です。', - mixtureInternalAndExternalTip: '内部と外部のナレッジベースを混在させる場合、Rerankモデルが必要です。', - allExternalTip: '外部ナレッジベースのみを使用する場合、Rerankモデルを有効にするかを選択できます。有効にしない場合、検索結果はスコアに基づいてソートされます。異なるナレッジベースで検索戦略が一貫していないと、結果が不正確になる可能性があります。', + mixtureHighQualityAndEconomicTip: '高品質なナレッジベースとコスト重視のナレッジベースを混在させるには、Rerank モデルが必要です。', + inconsistentEmbeddingModelTip: '選択されたナレッジベースの埋め込みモデルに一貫性がない場合、Rerank モデルが必要です。', + mixtureInternalAndExternalTip: '内部と外部のナレッジベースを混在させる場合、Rerank モデルが必要です。', + allExternalTip: '外部ナレッジベースのみを使用する場合、Rerank モデルを有効にするかを選択できます。有効にしない場合、検索結果はスコアに基づいてソートされます。異なるナレッジベースで検索戦略が一貫していないと、結果が不正確になる可能性があります。', retrievalSettings: '検索設定', - rerankSettings: 'Rerank設定', + rerankSettings: 'Rerank 設定', weightedScore: { title: 'ウェイト設定', description: '重みを調整することで、並べ替え戦略はセマンティックマッチングとキーワードマッチングのどちらを優先するかを決定します。', @@ -154,9 +154,9 @@ const translation = { semantic: 'セマンティクス', keyword: 'キーワード', }, - nTo1RetrievalLegacy: '製品計画によると、N-to-1 Retrievalは9月に正式に廃止される予定です。それまでは通常通り使用できます。', + nTo1RetrievalLegacy: '製品計画によると、N-to-1 Retrieval は 9 月に正式に廃止される予定です。それまでは通常通り使用できます。', nTo1RetrievalLegacyLink: '詳細はこちら', - nTo1RetrievalLegacyLinkText: ' N-to-1 retrievalは9月に正式に廃止されます。', + nTo1RetrievalLegacyLinkText: ' N-to-1 retrieval は 9 月に正式に廃止されます。', batchAction: { selected: '選択済み', enable: '有効にする', @@ -168,7 +168,7 @@ const translation = { preprocessDocument: '{{num}}件のドキュメントを前処理', allKnowledge: 'ナレッジベース全体', allKnowledgeDescription: 'このワークスペースにナレッジベース全体を表示する場合に選択します。ワークスペースのオーナーのみがすべてのナレッジベースを管理できます。', - embeddingModelNotAvailable: 'Embeddingモデル不可用。', + embeddingModelNotAvailable: 'Embedding モデル不可用。', metadata: { metadata: 'メタデータ', addMetadata: 'メタデータを追加', diff --git a/web/i18n/ja-JP/education.ts b/web/i18n/ja-JP/education.ts index d51bac817d..4fae5ac28e 100644 --- a/web/i18n/ja-JP/education.ts +++ b/web/i18n/ja-JP/education.ts @@ -1,7 +1,7 @@ const translation = { toVerified: '教育認証を取得', toVerifiedTip: { - front: '現在、教育認証ステータスを取得する資格があります。以下に教育情報を入力し、認証プロセスを完了すると、Difyプロフェッショナルプランの', + front: '現在、教育認証ステータスを取得する資格があります。以下に教育情報を入力し、認証プロセスを完了すると、Dify プロフェッショナルプランの', coupon: '50%割引クーポン', end: 'を受け取ることができます。', }, @@ -29,18 +29,18 @@ const translation = { privacyPolicy: 'プライバシーポリシー', }, option: { - age: '18歳以上であることを確認します。', - inSchool: '提供した教育機関に在籍または勤務している ことを確認します。Difyは在籍/雇用証明の提出を求める場合があります。不正な情報を申告した場合、教育認証に基づき免除された費用を支払うことに同意します。', + age: '18 歳以上であることを確認します。', + inSchool: '提供した教育機関に在籍または勤務している ことを確認します。Dify は在籍/雇用証明の提出を求める場合があります。不正な情報を申告した場合、教育認証に基づき免除された費用を支払うことに同意します。', }, }, }, submit: '送信', submitError: 'フォームの送信に失敗しました。しばらくしてから再度ご提出ください。', learn: '教育認証の取得方法はこちら', - successTitle: 'Dify教育認証を取得しました!', - successContent: 'お客様のアカウントに Difyプロフェッショナルプランの50%割引クーポン を発行しました。有効期間は 1年間 ですので、期限内にご利用ください。', - rejectTitle: 'Dify教育認証が拒否されました', - rejectContent: '申し訳ございませんが、このメールアドレスでは 教育認証 の資格を取得できず、Difyプロフェッショナルプランの50%割引クーポン を受け取ることはできません。', + successTitle: 'Dify 教育認証を取得しました!', + successContent: 'お客様のアカウントに Dify プロフェッショナルプランの 50% 割引クーポン を発行しました。有効期間は 1 年間 ですので、期限内にご利用ください。', + rejectTitle: 'Dify 教育認証が拒否されました', + rejectContent: '申し訳ございませんが、このメールアドレスでは 教育認証 の資格を取得できず、Dify プロフェッショナルプランの 50%割引クーポン を受け取ることはできません。', emailLabel: '現在のメールアドレス', } diff --git a/web/i18n/ja-JP/login.ts b/web/i18n/ja-JP/login.ts index 3db651c580..84ab9eecd0 100644 --- a/web/i18n/ja-JP/login.ts +++ b/web/i18n/ja-JP/login.ts @@ -1,6 +1,6 @@ const translation = { pageTitle: 'はじめましょう!👋', - welcome: 'Difyへようこそ。続行するにはログインしてください。', + welcome: 'Dify へようこそ。続行するにはログインしてください。', email: 'メールアドレス', emailPlaceholder: 'メールアドレスを入力してください', password: 'パスワード', @@ -9,10 +9,10 @@ const translation = { namePlaceholder: 'ユーザー名を入力してください', forget: 'パスワードをお忘れですか?', signBtn: 'サインイン', - sso: 'SSOに続ける', + sso: 'SSO に続ける', installBtn: 'セットアップ', setAdminAccount: '管理者アカウントの設定', - setAdminAccountDesc: 'アプリケーションの作成やLLMプロバイダの管理など、管理者アカウントの最大権限を設定します。', + setAdminAccountDesc: 'アプリケーションの作成や LLM プロバイダの管理など、管理者アカウントの最大権限を設定します。', createAndSignIn: '作成してサインイン', oneMoreStep: 'あと一歩', createSample: 'この情報を基に、サンプルアプリケーションを作成します', @@ -20,14 +20,14 @@ const translation = { invitationCodePlaceholder: '招待コードを入力してください', interfaceLanguage: 'インターフェース言語', timezone: 'タイムゾーン', - go: 'Difyへ移動', + go: 'Dify へ移動', sendUsMail: '自己紹介をメールで送信し、招待リクエストを処理します。', acceptPP: 'プライバシーポリシーを読み、同意します', reset: 'パスワードをリセットするには、次のコマンドを実行してください', - withGitHub: 'GitHubで続行', - withGoogle: 'Googleで続行', - rightTitle: 'LLMのフルポテンシャルを解き放つ', - rightDesc: '魅力的で操作可能で改善可能なAIアプリケーションを簡単に構築します。', + withGitHub: 'GitHub で続行', + withGoogle: 'Google で続行', + rightTitle: 'LLM のフルポテンシャルを解き放つ', + rightDesc: '魅力的で操作可能で改善可能な AI アプリケーションを簡単に構築します。', tos: '利用規約', pp: 'プライバシーポリシー', tosDesc: 'サインアップすることで、以下に同意するものとします', @@ -53,19 +53,19 @@ const translation = { emailInValid: '有効なメールアドレスを入力してください', nameEmpty: '名前は必須です', passwordEmpty: 'パスワードは必須です', - passwordLengthInValid: 'パスワードは8文字以上でなければなりません', - passwordInvalid: 'パスワードは文字と数字を含み、長さは8以上である必要があります', + passwordLengthInValid: 'パスワードは 8 文字以上でなければなりません', + passwordInvalid: 'パスワードは文字と数字を含み、長さは 8 以上である必要があります', registrationNotAllowed: 'アカウントが見つかりません。登録するためにシステム管理者に連絡してください。', }, license: { - tip: 'GitHubのオープンソースライセンスを確認してから、Dify Community Editionを開始してください。', + tip: 'GitHub のオープンソースライセンスを確認してから、Dify Community Edition を開始してください。', link: 'オープンソースライセンス', }, join: '参加する', joinTipStart: 'あなたを招待します', joinTipEnd: 'チームに参加する', invalid: 'リンクの有効期限が切れています', - explore: 'Difyを探索する', + explore: 'Dify を探索する', activatedTipStart: 'あなたは', activatedTipEnd: 'チームに参加しました', activated: '今すぐサインイン', @@ -74,13 +74,13 @@ const translation = { checkCode: { invalidCode: '無効なコード', verify: '確かめる', - verificationCodePlaceholder: '6桁のコードを入力してください', + verificationCodePlaceholder: '6 桁のコードを入力してください', useAnotherMethod: '別の方法を使用する', - didNotReceiveCode: 'コードが届きませんか?', + didNotReceiveCode: 'コードが届きませんか?', resend: '再送', verificationCode: '認証コード', tips: '<strong>確認コードを{{email}}に送信します。</strong>', - validTime: 'コードは5分間有効であることに注意してください', + validTime: 'コードは 5 分間有効であることに注意してください', emptyCode: 'コードが必要です', checkYourEmail: 'メールをチェックしてください', }, @@ -90,7 +90,7 @@ const translation = { resetPassword: 'パスワードのリセット', changePasswordBtn: 'パスワードを設定する', setYourAccount: 'アカウントを設定する', - withSSO: 'SSOを続行する', + withSSO: 'SSO を続行する', noLoginMethod: '認証方法が構成されていません', backToLogin: 'ログインに戻る', continueWithCode: 'コードで続行', @@ -98,17 +98,17 @@ const translation = { usePassword: 'パスワードを使用', sendVerificationCode: '確認コードの送信', enterYourName: 'ユーザー名を入力してください', - resetPasswordDesc: 'Difyへのサインアップに使用したメールアドレスを入力すると、パスワードリセットメールが送信されます。', + resetPasswordDesc: 'Dify へのサインアップに使用したメールアドレスを入力すると、パスワードリセットメールが送信されます。', licenseLost: 'ライセンスを失った', - licenseExpiredTip: 'ワークスペースの Dify Enterprise ライセンスの有効期限が切れています。Difyを引き続き使用するには、管理者に連絡してください。', + licenseExpiredTip: 'ワークスペースの Dify Enterprise ライセンスの有効期限が切れています。Dify を引き続き使用するには、管理者に連絡してください。', licenseInactive: 'ライセンスが非アクティブです', - licenseInactiveTip: 'ワークスペースの Dify Enterprise ライセンスが非アクティブです。Difyを引き続き使用するには、管理者に連絡してください。', + licenseInactiveTip: 'ワークスペースの Dify Enterprise ライセンスが非アクティブです。Dify を引き続き使用するには、管理者に連絡してください。', licenseExpired: 'ライセンスの有効期限が切れています', - licenseLostTip: 'Difyライセンスサーバーへの接続に失敗しました。続けてDifyを使用するために管理者に連絡してください。', + licenseLostTip: 'Dify ライセンスサーバーへの接続に失敗しました。続けて Dify を使用するために管理者に連絡してください。', webapp: { - noLoginMethod: 'Webアプリに対して認証方法が構成されていません', + noLoginMethod: 'Web アプリに対して認証方法が構成されていません', noLoginMethodTip: 'システム管理者に連絡して、認証方法を追加してください。', - disabled: 'Webアプリの認証が無効になっています。システム管理者に連絡して有効にしてください。直接アプリを使用してみてください。', + disabled: 'Web アプリの認証が無効になっています。システム管理者に連絡して有効にしてください。直接アプリを使用してみてください。', }, } diff --git a/web/i18n/ja-JP/plugin.ts b/web/i18n/ja-JP/plugin.ts index dfb4efe679..bc976074ea 100644 --- a/web/i18n/ja-JP/plugin.ts +++ b/web/i18n/ja-JP/plugin.ts @@ -17,7 +17,7 @@ const translation = { list: { source: { local: 'ローカルパッケージファイルからインストール', - github: 'GitHubからインストールする', + github: 'GitHub からインストールする', marketplace: 'マーケットプレイスからインストール', }, noInstalled: 'プラグインはインストールされていません', @@ -33,7 +33,7 @@ const translation = { marketplace: 'マーケットプレイスからインストールされました', local: 'ローカルプラグイン', debugging: 'デバッグプラグイン', - github: 'Githubからインストールしました', + github: 'Github からインストールしました', }, operation: { info: 'プラグイン情報', @@ -51,7 +51,7 @@ const translation = { unsupportedContent2: 'バージョンを切り替えるにはクリックしてください。', unsupportedContent: 'インストールされたプラグインのバージョンは、このアクションを提供していません。', title: 'ツールを追加', - uninstalledContent: 'このプラグインはローカル/GitHubリポジトリからインストールされます。インストール後にご利用ください。', + uninstalledContent: 'このプラグインはローカル/GitHub リポジトリからインストールされます。インストール後にご利用ください。', descriptionLabel: 'ツールの説明', auto: '自動', params: '推論設定', @@ -59,13 +59,13 @@ const translation = { placeholder: 'ツールを選択...', uninstalledTitle: 'ツールがインストールされていません', empty: 'ツールを追加するには「+」ボタンをクリックしてください。複数のツールを追加できます。', - paramsTip1: 'LLM推論パラメータを制御します。', + paramsTip1: 'LLM 推論パラメータを制御します。', toolLabel: '道具', unsupportedTitle: 'サポートされていないアクション', toolSetting: 'ツール設定', }, endpointDisableTip: 'エンドポイントを無効にする', - endpointModalDesc: '設定が完了すると、APIエンドポイントを介してプラグインが提供する機能を使用できます。', + endpointModalDesc: '設定が完了すると、API エンドポイントを介してプラグインが提供する機能を使用できます。', endpointDisableContent: '{{name}}を無効にしますか?', endpointModalTitle: 'エンドポイントを設定する', endpointDeleteTip: 'エンドポイントを削除', @@ -141,15 +141,15 @@ const translation = { installFromGitHub: { installedSuccessfully: 'インストールに成功しました', installNote: '信頼できるソースからのみプラグインをインストールするようにしてください。', - updatePlugin: 'GitHubからプラグインを更新する', + updatePlugin: 'GitHub からプラグインを更新する', selectPackage: 'パッケージを選択', installFailed: 'インストールに失敗しました', selectPackagePlaceholder: 'パッケージを選択してください', - gitHubRepo: 'GitHubリポジトリ', + gitHubRepo: 'GitHub リポジトリ', selectVersionPlaceholder: 'バージョンを選択してください', uploadFailed: 'アップロードに失敗しました', selectVersion: 'バージョンを選択', - installPlugin: 'GitHubからプラグインをインストールする', + installPlugin: 'GitHub からプラグインをインストールする', }, upgrade: { title: 'プラグインをインストールする', @@ -162,14 +162,14 @@ const translation = { }, error: { fetchReleasesError: 'リリースを取得できません。後でもう一度お試しください。', - inValidGitHubUrl: '無効なGitHub URLです。有効なURLを次の形式で入力してください: https://github.com/owner/repo', - noReleasesFound: 'リリースは見つかりません。GitHubリポジトリまたは入力URLを確認してください。', + inValidGitHubUrl: '無効な GitHub URL です。有効な URL を次の形式で入力してください:https://github.com/owner/repo', + noReleasesFound: 'リリースは見つかりません。GitHub リポジトリまたは入力 URL を確認してください。', }, marketplace: { - empower: 'AI開発をサポートする', + empower: 'AI 開発をサポートする', discover: '探索', and: 'と', - difyMarketplace: 'Difyマーケットプレイス', + difyMarketplace: 'Dify マーケットプレイス', moreFrom: 'マーケットプレイスからのさらなる情報', noPluginFound: 'プラグインが見つかりません', pluginsResult: '{{num}} 件の結果', @@ -181,8 +181,8 @@ const translation = { firstReleased: 'リリース順', }, viewMore: 'もっと見る', - verifiedTip: 'このプラグインはDifyによって認証されています', - partnerTip: 'このプラグインはDifyのパートナーによって認証されています', + verifiedTip: 'このプラグインは Dify によって認証されています', + partnerTip: 'このプラグインは Dify のパートナーによって認証されています', }, task: { installError: '{{errorLength}} プラグインのインストールに失敗しました。表示するにはクリックしてください。', @@ -190,7 +190,7 @@ const translation = { clearAll: 'すべてクリア', installedError: '{{errorLength}} プラグインのインストールに失敗しました', installingWithError: '{{installingLength}}個のプラグインをインストール中、{{successLength}}件成功、{{errorLength}}件失敗', - installing: '{{installingLength}}個のプラグインをインストール中、0個完了。', + installing: '{{installingLength}}個のプラグインをインストール中、0 個完了。', }, from: 'インストール元', install: '{{num}} インストール', @@ -207,7 +207,7 @@ const translation = { installPlugin: 'プラグインをインストールする', searchInMarketplace: 'マーケットプレイスで検索', submitPlugin: 'プラグインを提出する', - difyVersionNotCompatible: '現在のDifyバージョンはこのプラグインと互換性がありません。最小バージョンは{{minimalDifyVersion}}です。', + difyVersionNotCompatible: '現在の Dify バージョンはこのプラグインと互換性がありません。最小バージョンは{{minimalDifyVersion}}です。', metadata: { title: 'プラグイン', }, diff --git a/web/i18n/ja-JP/run-log.ts b/web/i18n/ja-JP/run-log.ts index 758e37c7de..2c4bc46331 100644 --- a/web/i18n/ja-JP/run-log.ts +++ b/web/i18n/ja-JP/run-log.ts @@ -19,7 +19,7 @@ const translation = { steps: '処理ステップ数', }, resultEmpty: { - title: '今回の実行ではJSON形式のみが出力されました', + title: '今回の実行では JSON 形式のみが出力されました', tipLeft: '詳細を確認するには', link: '詳細情報パネル', tipRight: 'へ移動してください', diff --git a/web/i18n/ja-JP/share-app.ts b/web/i18n/ja-JP/share-app.ts index 9db8926394..20dad7faec 100644 --- a/web/i18n/ja-JP/share-app.ts +++ b/web/i18n/ja-JP/share-app.ts @@ -35,7 +35,7 @@ const translation = { }, generation: { tabs: { - create: '1回実行', + create: '1 回実行', batch: '一括実行', saved: '保存済み', }, @@ -44,7 +44,7 @@ const translation = { description: 'コンテンツ生成後に結果がここに表示されます', startCreateContent: '生成を開始', }, - title: 'AI文章作成', + title: 'AI 文章作成', queryTitle: '入力内容', completionResult: '生成結果', queryPlaceholder: '入力してください', @@ -52,11 +52,11 @@ const translation = { execution: '処理中', executions: '{{num}}回実行', copy: 'コピー', - resultTitle: 'AI生成結果', - noData: 'AIがコンテンツを生成します', - csvUploadTitle: 'CSVファイルをドロップするか', + resultTitle: 'AI 生成結果', + noData: 'AI がコンテンツを生成します', + csvUploadTitle: 'CSV ファイルをドロップするか', browse: 'ファイルを選択', - csvStructureTitle: 'CSV形式要件:', + csvStructureTitle: 'CSV 形式要件:', downloadTemplate: 'テンプレートを取得', field: '', batchFailed: { @@ -67,12 +67,15 @@ const translation = { errorMsg: { empty: 'ファイル内容が空です', fileStructNotMatch: 'ファイル形式が不正です', - emptyLine: '{{rowIndex}}行目: 内容が空です', - invalidLine: '{{rowIndex}}行目: {{varName}}の入力が必要です', - moreThanMaxLengthLine: '{{rowIndex}}行目: {{varName}}が制限長({{maxLength}})を超過', - atLeastOne: '1行以上のデータが必要です', + emptyLine: '{{rowIndex}}行目:内容が空です', + invalidLine: '{{rowIndex}}行目:{{varName}}の入力が必要です', + moreThanMaxLengthLine: '{{rowIndex}}行目:{{varName}}が制限長({{maxLength}})を超過', + atLeastOne: '1 行以上のデータが必要です', }, }, + login: { + backToHome: 'ホームに戻る', + }, } export default translation diff --git a/web/i18n/ja-JP/time.ts b/web/i18n/ja-JP/time.ts index 09203a0cc2..6594533b2b 100644 --- a/web/i18n/ja-JP/time.ts +++ b/web/i18n/ja-JP/time.ts @@ -9,18 +9,18 @@ const translation = { Sun: '日曜日', }, months: { - November: '11月', - December: '12月', - March: '3月', - September: '9月', - July: '7月', - April: '4月', - February: '2月', - June: '6月', - January: '1月', - May: '5月', - August: '8月', - October: '10月', + November: '11 月', + December: '12 月', + March: '3 月', + September: '9 月', + July: '7 月', + April: '4 月', + February: '2 月', + June: '6 月', + January: '1 月', + May: '5 月', + August: '8 月', + October: '10 月', }, operation: { now: '今', diff --git a/web/i18n/ja-JP/tools.ts b/web/i18n/ja-JP/tools.ts index d8262a4a19..cf9dad95b3 100644 --- a/web/i18n/ja-JP/tools.ts +++ b/web/i18n/ja-JP/tools.ts @@ -1,7 +1,7 @@ const translation = { title: 'ツール', createCustomTool: 'カスタムツールを作成する', - customToolTip: 'Difyカスタムツールの詳細', + customToolTip: 'Dify カスタムツールの詳細', type: { all: 'すべて', builtIn: 'ツール', @@ -10,7 +10,7 @@ const translation = { }, contribute: { line1: '私は', - line2: 'Difyへのツールの貢献に興味があります。', + line2: 'Dify へのツールの貢献に興味があります。', viewGuide: 'ガイドを見る', }, author: '著者:', @@ -29,7 +29,7 @@ const translation = { added: '追加済', manageInTools: 'ツールリストに移動して管理する', emptyTitle: '利用可能なワークフローツールはありません', - emptyTip: '追加するには、「ワークフロー -> ツールとして公開 」に移動する', + emptyTip: '追加するには、「ワークフロー -> ツールとして公開」に移動する', emptyTitleCustom: 'カスタムツールはありません', emptyTipCustom: 'カスタムツールの作成', }, @@ -40,20 +40,20 @@ const translation = { name: '名前', toolNamePlaceHolder: 'ツール名を入力してください', nameForToolCall: 'ツールコールの名前', - nameForToolCallPlaceHolder: '機械認識に使用される名前, 例えば、getCurrentWeather、list_pets', + nameForToolCallPlaceHolder: '機械認識に使用される名前,例えば、getCurrentWeather、list_pets', nameForToolCallTip: '数字、文字、アンダースコアのみがサポートされます。', description: 'ツールの説明', descriptionPlaceholder: 'ツールの使い方の簡単な説明。例えば、特定の場所の温度を知るためなど。', schema: 'スキーマ', - schemaPlaceHolder: 'ここにOpenAPIスキーマを入力してください', - viewSchemaSpec: 'OpenAPI-Swagger仕様を表示する', - importFromUrl: 'URLからインポートする', + schemaPlaceHolder: 'ここに OpenAPI スキーマを入力してください', + viewSchemaSpec: 'OpenAPI-Swagger 仕様を表示する', + importFromUrl: 'URL からインポートする', importFromUrlPlaceHolder: 'https://...', - urlError: '有効なURLを入力してください', + urlError: '有効な URL を入力してください', examples: '例', exampleOptions: { - json: '天気(JSON)', - yaml: 'ペットストア(YAML)', + json: '天気 (JSON)', + yaml: 'ペットストア (YAML)', blankTemplate: '空白テンプレート', }, availableTools: { @@ -68,12 +68,12 @@ const translation = { authMethod: { title: '認証方法', type: '認証タイプ', - keyTooltip: 'HTTPヘッダーキー。アイデアがない場合は "Authorization" として残しておいてもかまいません。またはカスタム値に設定できます。', + keyTooltip: 'HTTP ヘッダーキー。アイデアがない場合は "Authorization" として残しておいてもかまいません。またはカスタム値に設定できます。', types: { none: 'なし', - api_key: 'APIキー', - apiKeyPlaceholder: 'APIキーのHTTPヘッダー名', - apiValuePlaceholder: 'APIキーを入力してください', + api_key: 'API キー', + apiKeyPlaceholder: 'API キーの HTTP ヘッダー名', + apiValuePlaceholder: 'API キーを入力してください', }, key: 'キー', value: '値', @@ -95,10 +95,10 @@ const translation = { method: 'メソッド', methodSetting: '設定', methodSettingTip: 'ユーザーがツール設定を入力する', - methodParameter: 'LLM入力', + methodParameter: 'LLM 入力', methodParameterTip: 'LLM は推論中に入力されます', label: 'ラベル', - labelPlaceholder: 'ラベルを選択します(オプション)', + labelPlaceholder: 'ラベルを選択します (オプション)', description: '説明', descriptionPlaceholder: 'パラメータの意味の説明', }, @@ -136,7 +136,7 @@ const translation = { }, noCustomTool: { title: 'カスタムツールがありません!', - content: 'AIアプリを構築するためのカスタムツールをここで追加および管理します。', + content: 'AI アプリを構築するためのカスタムツールをここで追加および管理します。', createTool: 'ツールを作成する', }, noSearchRes: { diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index 8495a7dfd5..1320e7f3b6 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -20,7 +20,7 @@ const translation = { goBackToEdit: '編集に戻る', conversationLog: '会話ログ', features: '機能', - featuresDescription: 'Webアプリの操作性を向上させる機能', + featuresDescription: 'Web アプリの操作性を向上させる機能', ImageUploadLegacyTip: '開始フォームでファイル型変数が作成可能になりました。画像アップロード機能は今後サポート終了となります。', fileUploadTip: '画像アップロード機能がファイルアップロードに拡張されました', featuresDocLink: '詳細を見る', @@ -36,7 +36,7 @@ const translation = { runApp: 'アプリを実行', batchRunApp: 'アプリを一括実行', openInExplore: '探索ページで開く', - accessAPIReference: 'APIリファレンス', + accessAPIReference: 'API リファレンス', embedIntoSite: 'サイトに埋め込む', addTitle: 'タイトルを追加...', addDescription: '説明を追加...', @@ -45,7 +45,7 @@ const translation = { variableNamePlaceholder: '変数名を入力', setVarValuePlaceholder: '変数値を設定', needConnectTip: '接続されていないステップがあります', - maxTreeDepth: '1ブランチあたりの最大ノード数:{{depth}}', + maxTreeDepth: '1 ブランチあたりの最大ノード数:{{depth}}', needEndNode: '終了ブロックを追加する必要があります', needAnswerNode: '回答ブロックを追加する必要があります', workflowProcess: 'ワークフロー処理', @@ -59,10 +59,10 @@ const translation = { processData: 'データ処理', input: '入力', output: '出力', - jinjaEditorPlaceholder: '「/」または 「{」で変数挿入', + jinjaEditorPlaceholder: '「/」または「{」で変数挿入', viewOnly: '閲覧のみ', showRunHistory: '実行履歴を表示', - enableJinja: 'Jinjaテンプレートを有効化', + enableJinja: 'Jinja テンプレートを有効化', learnMore: '詳細を見る', copy: 'コピー', duplicate: '複製', @@ -71,9 +71,9 @@ const translation = { pointerMode: 'ポインターモード', handMode: 'ハンドモード', exportImage: '画像を出力', - exportPNG: 'PNGで出力', - exportJPEG: 'JPEGで出力', - exportSVG: 'SVGで出力', + exportPNG: 'PNG で出力', + exportJPEG: 'JPEG で出力', + exportSVG: 'SVG で出力', model: 'モデル', workflowAsTool: 'ワークフローをツールとして公開する', configureRequired: '設定が必要', @@ -82,14 +82,14 @@ const translation = { workflowAsToolTip: 'ワークフロー更新後はツールの再設定が必要です', viewDetailInTracingPanel: '詳細を表示', syncingData: 'データ同期中。。。', - importDSL: 'DSLをインポート', + importDSL: 'DSL をインポート', importDSLTip: '現在の下書きは上書きされます。インポート前にワークフローをエクスポートしてバックアップしてください', backupCurrentDraft: '現在の下書きをバックアップ', - chooseDSL: 'DSL(yml)ファイルを選択', + chooseDSL: 'DSL(yml) ファイルを選択', overwriteAndImport: '上書きしてインポート', importFailure: 'インポート失敗', importWarning: '注意事項', - importWarningDetails: 'DSLバージョンの違いにより機能に影響が出る可能性があります', + importWarningDetails: 'DSL バージョンの違いにより機能に影響が出る可能性があります', importSuccess: 'インポート成功', parallelRun: '並列実行', parallelTip: { @@ -116,7 +116,7 @@ const translation = { }, env: { envPanelTitle: '環境変数', - envDescription: '環境変数は、個人情報や認証情報を格納するために使用することができます。これらは読み取り専用であり、DSLファイルからエクスポートする際には分離されます。', + envDescription: '環境変数は、個人情報や認証情報を格納するために使用することができます。これらは読み取り専用であり、DSL ファイルからエクスポートする際には分離されます。', envPanelButton: '環境変数を追加', modal: { title: '環境変数を追加', @@ -131,7 +131,7 @@ const translation = { export: { title: 'シークレット環境変数をエクスポートしますか?', checkbox: 'シークレット値を含む', - ignore: 'DSLをエクスポート', + ignore: 'DSL をエクスポート', export: 'シークレット値付きでエクスポート', }, }, @@ -150,7 +150,7 @@ const translation = { valuePlaceholder: 'デフォルト値、設定しない場合は空白にしてください', description: '説明', descriptionPlaceholder: '変数の説明を入力', - editInJSON: 'JSONで編集', + editInJSON: 'JSON で編集', oneByOne: '個別追加', editInForm: 'フォームで編集', arrayValue: '値', @@ -198,7 +198,7 @@ const translation = { variableValue: '変数値', code: 'コード', model: 'モデル', - rerankModel: 'Rerankモデル', + rerankModel: 'Rerank モデル', visionVariable: 'ビジョン変数', }, invalidVariable: '無効な変数です', @@ -241,7 +241,7 @@ const translation = { 'if-else': 'IF/ELSE', 'code': 'コード実行', 'template-transform': 'テンプレート', - 'http-request': 'HTTPリクエスト', + 'http-request': 'HTTP リクエスト', 'variable-assigner': '変数代入器', 'variable-aggregator': '変数集約器', 'assigner': '変数代入', @@ -261,11 +261,11 @@ const translation = { 'answer': 'チャットダイアログの返答内容を定義します。', 'llm': '大規模言語モデルを呼び出して質問回答や自然言語処理を実行します。', 'knowledge-retrieval': 'ナレッジベースからユーザー質問に関連するテキストを検索します。', - 'question-classifier': '質問の分類条件を定義し、LLMが分類に基づいて対話フローを制御します。', - 'if-else': 'if/else条件でワークフローを2つの分岐に分割します。', - 'code': 'Python/NodeJSコードを実行してカスタムロジックを実装します。', - 'template-transform': 'Jinjaテンプレート構文でデータを文字列に変換します。', - 'http-request': 'HTTPリクエストを送信できます。', + 'question-classifier': '質問の分類条件を定義し、LLM が分類に基づいて対話フローを制御します。', + 'if-else': 'if/else 条件でワークフローを 2 つの分岐に分割します。', + 'code': 'Python/NodeJS コードを実行してカスタムロジックを実装します。', + 'template-transform': 'Jinja テンプレート構文でデータを文字列に変換します。', + 'http-request': 'HTTP リクエストを送信できます。', 'variable-assigner': '複数分岐の変数を集約し、下流ノードの設定を統一します。', 'assigner': '書き込み可能な変数(例:会話変数)への値の割り当てを行います。', 'variable-aggregator': '複数分岐の変数を集約し、下流ノードの設定を統一します。', @@ -273,14 +273,14 @@ const translation = { 'loop': '終了条件達成まで、または最大反復回数までロジックを繰り返します。', 'loop-end': '「break」相当の機能です。このノードに設定項目はなく、ループ処理中にこのノードに到達すると即時終了します。', 'parameter-extractor': '自然言語から構造化パラメータを抽出し、後続処理で利用します。', - 'document-extractor': 'アップロード文書をLLM処理用に最適化されたテキストに変換します。', + 'document-extractor': 'アップロード文書を LLM 処理用に最適化されたテキストに変換します。', 'list-operator': '配列のフィルタリングやソート処理を行います。', 'agent': '大規模言語モデルを活用した質問応答や自然言語処理を実行します。', }, operator: { zoomIn: '拡大', zoomOut: '縮小', - zoomTo50: '50%サイズ', + zoomTo50: '50% サイズ', zoomTo100: '等倍表示', zoomToFit: '画面に合わせる', }, @@ -336,7 +336,7 @@ const translation = { defaultValue: { title: 'デフォルト値', desc: '例外発生時のデフォルト出力', - tip: '例外発生時に返される値:', + tip: '例外発生時に返される値:', inLog: 'ノード例外 - デフォルト値を出力', output: 'デフォルト値出力', }, @@ -363,7 +363,7 @@ const translation = { retryFailedTimes: '{{times}}回再試行失敗', times: '回', ms: 'ミリ秒', - retries: '再試行回数: {{num}}', + retries: '再試行回数:{{num}}', }, }, start: { @@ -398,7 +398,7 @@ const translation = { outputVars: '出力変数', }, llm: { - model: 'AIモデル', + model: 'AI モデル', variables: '変数', context: 'コンテキスト', contextTooltip: 'ナレッジベースをコンテキストとして利用', @@ -424,17 +424,17 @@ const translation = { singleRun: { variable: '変数', }, - sysQueryInUser: 'ユーザーメッセージにsys.queryを含めてください', + sysQueryInUser: 'ユーザーメッセージに sys.query を含めてください', jsonSchema: { title: '構造化データスキーマ', instruction: '指示', - promptTooltip: 'テキスト説明から標準JSONスキーマを自動生成できます。', - promptPlaceholder: 'JSONスキーマを入力...', + promptTooltip: 'テキスト説明から標準 JSON スキーマを自動生成できます。', + promptPlaceholder: 'JSON スキーマを入力...', generate: '生成', - import: 'JSONインポート', + import: 'JSON インポート', generateJsonSchema: 'スキーマ生成', - generationTip: '自然言語で簡単にJSONスキーマを作成可能です。', - generating: 'JSONスキーマを生成中...', + generationTip: '自然言語で簡単に JSON スキーマを作成可能です。', + generating: 'JSON スキーマを生成中...', generatedResult: '生成結果', resultTip: 'こちらが生成された結果です。ご満足いただけない場合は、前の画面に戻ってプロンプトを修正できます。', back: '前に戻る', @@ -462,7 +462,7 @@ const translation = { content: 'セグメント内容', title: 'セグメントタイトル', icon: 'セグメントアイコン', - url: 'セグメントURL', + url: 'セグメント URL', metadata: 'メタデータ', }, metadata: { @@ -497,9 +497,9 @@ const translation = { http: { inputVars: '入力変数', api: 'API', - apiPlaceholder: 'URLを入力(変数使用時は"/"を入力)', + apiPlaceholder: 'URL を入力(変数使用時は"/"を入力)', extractListPlaceholder: 'リスト番号を入力(変数使用時は"/"を入力)', - notStartWithHttp: 'APIは http:// または https:// で始まってください', + notStartWithHttp: 'API は http:// または https:// で始まってください', key: 'キー', type: 'タイプ', value: '値', @@ -519,12 +519,12 @@ const translation = { 'authorization': '認証', 'authorizationType': '認証タイプ', 'no-auth': 'なし', - 'api-key': 'APIキー', - 'auth-type': 'API認証タイプ', + 'api-key': 'API キー', + 'auth-type': 'API 認証タイプ', 'basic': 'ベーシック', 'bearer': 'Bearer', 'custom': 'カスタム', - 'api-key-title': 'APIキー', + 'api-key-title': 'API キー', 'header': 'ヘッダー', }, insertVarPlaceholder: '変数を挿入するには\'/\'を入力してください', @@ -538,8 +538,8 @@ const translation = { writePlaceholder: '書き込みタイムアウト(秒)', }, curl: { - title: 'cURLからインポート', - placeholder: 'ここにcURL文字列を貼り付けます', + title: 'cURL からインポート', + placeholder: 'ここに cURL 文字列を貼り付けます', }, }, code: { @@ -552,7 +552,7 @@ const translation = { templateTransform: { inputVars: '入力変数', code: 'コード', - codeSupportTip: 'Jinja2のみをサポートしています', + codeSupportTip: 'Jinja2 のみをサポートしています', outputVars: { output: '変換されたコンテンツ', }, @@ -560,7 +560,7 @@ const translation = { ifElse: { if: 'もし', else: 'それ以外', - elseDescription: 'IF条件が満たされない場合に実行するロジックを定義します。', + elseDescription: 'IF 条件が満たされない場合に実行するロジックを定義します。', and: 'かつ', or: 'または', operator: '演算子', @@ -575,7 +575,7 @@ const translation = { 'empty': '空', 'not empty': '空でない', 'null': 'null', - 'not null': 'nullでない', + 'not null': 'null でない', 'regex match': '正規表現マッチ', 'in': '含まれている', 'not in': '含まれていない', @@ -623,7 +623,7 @@ const translation = { assigner: { 'assignedVariable': '代入された変数', 'writeMode': '書き込みモード', - 'writeModeTip': '代入された変数が配列の場合, 末尾に追記モードを追加する。', + 'writeModeTip': '代入された変数が配列の場合,末尾に追記モードを追加する。', 'over-write': '上書き', 'append': '追記', 'plus': 'プラス', @@ -660,11 +660,11 @@ const translation = { files: { title: 'ツールが生成したファイル', type: 'サポートタイプ。現在は画像のみサポートされています', - transfer_method: '転送方法。値はremote_urlまたはlocal_fileです', - url: '画像URL', - upload_file_id: 'アップロードファイルID', + transfer_method: '転送方法。値は remote_url または local_file です', + url: '画像 URL', + upload_file_id: 'アップロードファイル ID', }, - json: 'ツールで生成されたJSON', + json: 'ツールで生成された JSON', }, authorize: '認証する', }, @@ -705,7 +705,7 @@ const translation = { advancedSetting: '高度な設定', reasoningMode: '推論モード', reasoningModeTip: '関数呼び出しやプロンプトの指示に応答するモデルの能力に基づいて、適切な推論モードを選択できます。', - isSuccess: '成功。成功した場合の値は1、失敗した場合の値は0です。', + isSuccess: '成功。成功した場合の値は 1、失敗した場合の値は 0 です。', errorReason: 'エラーの理由', }, iteration: { @@ -732,7 +732,7 @@ const translation = { parallelModeEnableDesc: '並列モードでは、イテレーション内のタスクは並列実行をサポートします。これは、右側のプロパティパネルで構成できます。', parallelModeEnableTitle: 'パラレルモード有効', MaxParallelismDesc: '最大並列処理は、1 回の反復で同時に実行されるタスクの数を制御するために使用されます。', - answerNodeWarningDesc: '並列モードの警告: 応答ノード、会話変数の割り当て、およびイテレーション内の永続的な読み取り/書き込み操作により、例外が発生する可能性があります。', + answerNodeWarningDesc: '並列モードの警告:応答ノード、会話変数の割り当て、およびイテレーション内の永続的な読み取り/書き込み操作により、例外が発生する可能性があります。', }, loop: { deleteTitle: 'ループノードを削除しますか?', @@ -745,7 +745,7 @@ const translation = { breakCondition: 'ループ終了条件', breakConditionTip: 'ループ内の変数やセッション変数を参照し、終了条件を設定できます。', loopMaxCount: '最大ループ回数', - loopMaxCountError: '最大ループ回数は1から{{maxCount}}の範囲で正しく入力してください。', + loopMaxCountError: '最大ループ回数は 1 から{{maxCount}}の範囲で正しく入力してください。', errorResponseMethod: 'エラー対応方法', ErrorMethod: { operationTerminated: 'エラー時に処理を終了', @@ -758,10 +758,10 @@ const translation = { setLoopVariables: 'ループスコープ内で変数を設定', variableName: '変数名', inputMode: '入力モード', - exitConditionTip: 'ループノードには少なくとも1つの終了条件が必要です', + exitConditionTip: 'ループノードには少なくとも 1 つの終了条件が必要です', loopNode: 'ループノード', - currentLoopCount: '現在のループ回数: {{count}}', - totalLoopCount: '総ループ回数: {{count}}', + currentLoopCount: '現在のループ回数:{{count}}', + totalLoopCount: '総ループ回数:{{count}}', error_other: '{{count}} エラー', error_one: '{{count}} エラー', comma: ',', @@ -791,7 +791,7 @@ const translation = { }, inputVar: '入力変数', learnMore: '詳細はこちら', - supportFileTypes: 'サポートするファイルタイプ: {{types}}。', + supportFileTypes: 'サポートするファイルタイプ:{{types}}。', }, listFilter: { outputVars: { @@ -799,7 +799,7 @@ const translation = { first_record: '最初のレコード', result: 'フィルター結果', }, - limit: 'トップN', + limit: 'トップ N', asc: 'ASC', filterCondition: 'フィルター条件', filterConditionKey: 'フィルター条件キー', @@ -809,7 +809,7 @@ const translation = { filterConditionComparisonOperator: 'フィルター条件を比較オペレーター', inputVar: '入力変数', desc: 'DESC', - extractsCondition: 'N個のアイテムを抽出します', + extractsCondition: 'N 個のアイテムを抽出します', }, agent: { strategy: { @@ -828,7 +828,7 @@ const translation = { modelNotInMarketplace: { manageInPlugins: 'プラグインを管理する', title: 'モデルがインストールされていません', - desc: 'このモデルはローカルまたはGitHubリポジトリからインストールされます。インストール後にご利用ください。', + desc: 'このモデルはローカルまたは GitHub リポジトリからインストールされます。インストール後にご利用ください。', }, modelNotSupport: { title: 'サポートされていないモデル', @@ -840,14 +840,14 @@ const translation = { }, outputVars: { files: { - url: '画像のURL', + url: '画像の URL', type: 'サポートタイプ。現在はサポート画像のみ', - upload_file_id: 'ファイルIDをアップロード', - transfer_method: '転送方法。値はremote_urlまたはlocal_fileです。', + upload_file_id: 'ファイル ID をアップロード', + transfer_method: '転送方法。値は remote_url または local_file です。', title: 'エージェント生成ファイル', }, text: 'エージェント生成コンテンツ', - json: 'エージェント生成のJSON', + json: 'エージェント生成の JSON', }, checkList: { strategyNotSelected: '戦略が選択されていません', @@ -875,9 +875,9 @@ const translation = { toolbox: 'ツールボックス', pluginNotInstalled: 'このプラグインはインストールされていません', strategyNotFoundDescAndSwitchVersion: 'インストールされたプラグインのバージョンはこの戦略を提供していません。バージョンを切り替えるにはクリックしてください。', - pluginNotInstalledDesc: 'このプラグインはGitHubからインストールされています。再インストールするにはプラグインに移動してください。', + pluginNotInstalledDesc: 'このプラグインは GitHub からインストールされています。再インストールするにはプラグインに移動してください。', unsupportedStrategy: 'サポートされていない戦略', - pluginNotFoundDesc: 'このプラグインはGitHubからインストールされています。再インストールするにはプラグインに移動してください。', + pluginNotFoundDesc: 'このプラグインは GitHub からインストールされています。再インストールするにはプラグインに移動してください。', strategyNotFoundDesc: 'インストールされたプラグインのバージョンは、この戦略を提供していません。', }, }, diff --git a/web/i18n/ko-KR/app-api.ts b/web/i18n/ko-KR/app-api.ts index 810e67a875..4f8ac14d86 100644 --- a/web/i18n/ko-KR/app-api.ts +++ b/web/i18n/ko-KR/app-api.ts @@ -16,7 +16,7 @@ const translation = { never: '없음', apiKeyModal: { apiSecretKey: 'API 비밀 키', - apiSecretKeyTips: 'API 키를 보호하여 API의 남용을 방지하십시오. 프런트엔드 코드에서 평문으로 사용하지 마세요. :)', + apiSecretKeyTips: 'API 키를 보호하여 API 의 남용을 방지하십시오. 프런트엔드 코드에서 평문으로 사용하지 마세요. :)', createNewSecretKey: '새로운 비밀 키 생성', secretKey: '비밀 키', created: '생성 날짜', @@ -30,43 +30,43 @@ const translation = { }, completionMode: { title: '완성 모드 API', - info: '문서, 요약, 번역 등 고품질 텍스트 생성을 위해 사용자 입력을 사용하는 완성 메시지 API를 사용합니다. 텍스트 생성은 Dify Prompt Engineering에서 설정한 모델 매개변수와 프롬프트 템플릿에 의존합니다.', + info: '문서, 요약, 번역 등 고품질 텍스트 생성을 위해 사용자 입력을 사용하는 완성 메시지 API 를 사용합니다. 텍스트 생성은 Dify Prompt Engineering 에서 설정한 모델 매개변수와 프롬프트 템플릿에 의존합니다.', createCompletionApi: '완성 메시지 생성', createCompletionApiTip: '질의 응답 모드를 지원하기 위해 완성 메시지를 생성합니다.', inputsTips: - '(선택 사항) Prompt Eng의 변수에 해당하는 키-값 쌍으로 사용자 입력 필드를 제공합니다. 키는 변수 이름이고 값은 매개변수 값입니다. 필드 유형이 Select인 경우 전송되는 값은 미리 설정된 선택 사항 중 하나여야 합니다.', + '(선택 사항) Prompt Eng 의 변수에 해당하는 키 - 값 쌍으로 사용자 입력 필드를 제공합니다. 키는 변수 이름이고 값은 매개변수 값입니다. 필드 유형이 Select 인 경우 전송되는 값은 미리 설정된 선택 사항 중 하나여야 합니다.', queryTips: '사용자 입력 텍스트 내용.', blocking: '블로킹 유형으로 실행이 완료되고 결과가 반환될 때까지 대기합니다. (처리가 오래 걸리면 요청이 중단될 수 있습니다)', - streaming: '스트리밍 반환. SSE(Server-Sent Events)를 기반으로 하는 스트리밍 반환 구현.', - messageFeedbackApi: '메시지 피드백(좋아요)', + streaming: '스트리밍 반환. SSE(Server-Sent Events) 를 기반으로 하는 스트리밍 반환 구현.', + messageFeedbackApi: '메시지 피드백 (좋아요)', messageFeedbackApiTip: '엔드 사용자 대신 수신된 메시지를 "좋아요" 또는 "좋아요"로 평가합니다. 이 데이터는 로그 및 주석 페이지에 표시되며 향후 모델 세부 조정에 사용됩니다.', messageIDTip: '메시지 ID', - ratingTip: '좋아요 또는 좋아요, null은 취소', + ratingTip: '좋아요 또는 좋아요, null 은 취소', parametersApi: '애플리케이션 매개변수 정보 가져오기', parametersApiTip: '변수 이름, 필드 이름, 유형, 기본값을 포함한 설정된 입력 매개변수를 가져옵니다. 일반적으로 이러한 필드는 양식에 표시하거나 클라이언트 로드 후에 기본값을 입력하는 데 사용됩니다.', }, chatMode: { title: '채팅 모드 API', - info: '질의 응답 형식을 사용하는 다목적 대화형 응용 프로그램에는 채팅 메시지 API를 호출하여 대화를 시작합니다. 반환된 conversation_id를 전달하여 계속된 대화를 유지합니다. 응답 매개변수 및 템플릿은 Dify Prompt Eng의 설정에 의존합니다.', + info: '질의 응답 형식을 사용하는 다목적 대화형 응용 프로그램에는 채팅 메시지 API 를 호출하여 대화를 시작합니다. 반환된 conversation_id 를 전달하여 계속된 대화를 유지합니다. 응답 매개변수 및 템플릿은 Dify Prompt Eng 의 설정에 의존합니다.', createChatApi: '채팅 메시지 생성', createChatApiTip: '새로운 대화 메시지를 생성하거나 기존 대화를 계속합니다.', inputsTips: - '(선택 사항) Prompt Eng의 변수에 해당하는 키-값 쌍으로 사용자 입력 필드를 제공합니다. 키는 변수 이름이고 값은 매개변수 값입니다. 필드 유형이 Select인 경우 전송되는 값은 미리 설정된 선택 사항 중 하나여야 합니다.', + '(선택 사항) Prompt Eng 의 변수에 해당하는 키 - 값 쌍으로 사용자 입력 필드를 제공합니다. 키는 변수 이름이고 값은 매개변수 값입니다. 필드 유형이 Select 인 경우 전송되는 값은 미리 설정된 선택 사항 중 하나여야 합니다.', queryTips: '사용자 입력/질문 내용', blocking: '블로킹 유형으로 실행이 완료되고 결과가 반환될 때까지 대기합니다. (처리가 오래 걸리면 요청이 중단될 수 있습니다)', - streaming: '스트리밍 반환. SSE(Server-Sent Events)를 기반으로 하는 스트리밍 반환 구현.', - conversationIdTip: '(선택 사항) 대화 ID: 처음 대화의 경우 비워두고, 계속된 경우 컨텍스트에서 conversation_id를 전달합니다.', - messageFeedbackApi: '메시지 피드백(좋아요)', + streaming: '스트리밍 반환. SSE(Server-Sent Events) 를 기반으로 하는 스트리밍 반환 구현.', + conversationIdTip: '(선택 사항) 대화 ID: 처음 대화의 경우 비워두고, 계속된 경우 컨텍스트에서 conversation_id 를 전달합니다.', + messageFeedbackApi: '메시지 피드백 (좋아요)', messageFeedbackApiTip: '엔드 사용자 대신 수신된 메시지를 "좋아요" 또는 "좋아요"로 평가합니다. 이 데이터는 로그 및 주석 페이지에 표시되며 향후 모델 세부 조정에 사용됩니다.', messageIDTip: '메시지 ID', - ratingTip: '좋아요 또는 좋아요, null은 취소', + ratingTip: '좋아요 또는 좋아요, null 은 취소', chatMsgHistoryApi: '채팅 메시지 기록 가져오기', chatMsgHistoryApiTip: '첫 번째 페이지는 최신의 "limit" 바를 반환합니다. 역순입니다.', chatMsgHistoryConversationIdTip: '대화 ID', chatMsgHistoryFirstId: '현재 페이지의 첫 번째 채팅 레코드의 ID. 기본값은 없음입니다.', chatMsgHistoryLimit: '한 번에 반환되는 채팅 수', conversationsListApi: '대화 목록 가져오기', - conversationsListApiTip: '현재 사용자의 세션 목록을 가져옵니다. 기본적으로 최근 20개의 세션이 반환됩니다.', + conversationsListApiTip: '현재 사용자의 세션 목록을 가져옵니다. 기본적으로 최근 20 개의 세션이 반환됩니다.', conversationsListFirstIdTip: '현재 페이지의 마지막 레코드의 ID, 기본값은 없음입니다.', conversationsListLimitTip: '한 번에 반환되는 채팅 수', conversationRenamingApi: '대화 이름 변경', diff --git a/web/i18n/ko-KR/app-debug.ts b/web/i18n/ko-KR/app-debug.ts index bafe0bf8d8..3c5a3f4b1f 100644 --- a/web/i18n/ko-KR/app-debug.ts +++ b/web/i18n/ko-KR/app-debug.ts @@ -39,8 +39,8 @@ const translation = { settingBtn: '설정으로 이동', }, trailUseGPT4Info: { - title: '현재 gpt-4는 지원되지 않습니다', - description: 'gpt-4를 사용하려면 API 키를 설정해야 합니다.', + title: '현재 gpt-4 는 지원되지 않습니다', + description: 'gpt-4 를 사용하려면 API 키를 설정해야 합니다.', }, feature: { groupChat: { @@ -52,12 +52,12 @@ const translation = { }, conversationOpener: { title: '대화 시작', - description: '채팅 앱에서 AI가 사용자에게 처음으로 적극적으로 말을 건다면 일반적으로 환영 메시지로 사용됩니다.', + description: '채팅 앱에서 AI 가 사용자에게 처음으로 적극적으로 말을 건다면 일반적으로 환영 메시지로 사용됩니다.', }, suggestedQuestionsAfterAnswer: { title: '팔로우업', description: '다음 질문 제안을 설정하면 사용자에게 더 나은 채팅이 제공됩니다.', - resDes: '사용자의 다음 질문에 대한 3가지 제안.', + resDes: '사용자의 다음 질문에 대한 3 가지 제안.', tryToAsk: '질문해보세요', }, moreLikeThis: { @@ -162,7 +162,7 @@ const translation = { }, moderation: { title: '콘텐츠 모더레이션', - description: '모더레이션 API를 사용하거나 기밀 단어 목록을 유지함으로써 모델 출력을 안전하게 합니다.', + description: '모더레이션 API 를 사용하거나 기밀 단어 목록을 유지함으로써 모델 출력을 안전하게 합니다.', allEnabled: '입력/출력 콘텐츠가 모두 활성화되어 있습니다', inputEnabled: '입력 콘텐츠가 활성화되어 있습니다', outputEnabled: '출력 콘텐츠가 활성화되어 있습니다', @@ -178,7 +178,7 @@ const translation = { keywords: '키워드', }, keywords: { - tip: '한 줄에 하나씩, 줄 바꿈으로 입력하세요. 한 줄 당 최대 100자.', + tip: '한 줄에 하나씩, 줄 바꿈으로 입력하세요. 한 줄 당 최대 100 자.', placeholder: '한 줄씩 입력하세요', line: '줄', }, @@ -188,7 +188,7 @@ const translation = { preset: '프리셋 응답', placeholder: '프리셋 응답 내용을 입력하세요', condition: '최소한 하나의 입력 및 출력 콘텐츠를 모더레이션합니다', - fromApi: '프리셋 응답은 API에서 반환됩니다', + fromApi: '프리셋 응답은 API 에서 반환됩니다', errorMessage: '프리셋 응답은 비워둘 수 없습니다', supportMarkdown: '마크다운이 지원됩니다', }, @@ -201,10 +201,10 @@ const translation = { }, automatic: { title: '자동 어플리케이션 오케스트레이션', - description: '시나리오를 설명하세요. Dify가 어플리케이션을 자동으로 오케스트레이션 합니다.', + description: '시나리오를 설명하세요. Dify 가 어플리케이션을 자동으로 오케스트레이션 합니다.', intendedAudience: '누가 대상이 되는지 설명하세요.', intendedAudiencePlaceHolder: '예: 학생', - solveProblem: '어떤 문제를 AI가 해결할 것으로 예상하나요?', + solveProblem: '어떤 문제를 AI 가 해결할 것으로 예상하나요?', solveProblemPlaceHolder: '예: 학업 성적 평가', generate: '생성', audiencesRequired: '대상이 필요합니다', @@ -231,7 +231,7 @@ const translation = { }, chatSubTitle: '단계', completionSubTitle: '접두사 프롬프트', - promptTip: '프롬프트는 AI의 응답을 지시하고 제한하여 유도합니다. {{input}}과 같은 변수를 삽입하세요. 이 프롬프트는 사용자에게 표시되지 않습니다.', + promptTip: '프롬프트는 AI 의 응답을 지시하고 제한하여 유도합니다. {{input}}과 같은 변수를 삽입하세요. 이 프롬프트는 사용자에게 표시되지 않습니다.', formattingChangedTitle: '포맷이 변경되었습니다', formattingChangedText: '포맷을 변경하면 디버그 영역이 재설정됩니다. 계속하시겠습니까?', variableTitle: '변수', @@ -249,7 +249,7 @@ const translation = { }, varKeyError: { canNoBeEmpty: '{{key}}가 필요합니다', - tooLong: '{{key}}가 너무 깁니다. 30자를 넘을 수 없습니다', + tooLong: '{{key}}가 너무 깁니다. 30 자를 넘을 수 없습니다', notValid: '{{key}}가 유효하지 않습니다. 문자, 숫자, 밑줄만 포함할 수 있습니다', notStartWithNumber: '{{key}}는 숫자로 시작할 수 없습니다', keyAlreadyExists: '{{key}}는 이미 존재합니다', @@ -279,6 +279,7 @@ const translation = { 'labelName': '레이블명', 'inputPlaceholder': '입력하세요', 'required': '필수', + 'hide': '숨기기', 'errorMsg': { varNameRequired: '변수명은 필수입니다', labelNameRequired: '레이블명은 필수입니다', @@ -294,9 +295,9 @@ const translation = { visionSettings: { title: '비전 설정', resolution: '해상도', - resolutionTooltip: `저해상도는 모델에게 512 x 512 해상도의 저해상도 이미지를 제공하여 65 토큰의 예산으로 이미지를 표현합니다. 이로 인해 API는 더 빠른 응답을 제공하며 높은 세부 정보가 필요한 경우 토큰 소모를 늘립니다. + resolutionTooltip: `저해상도는 모델에게 512 x 512 해상도의 저해상도 이미지를 제공하여 65 토큰의 예산으로 이미지를 표현합니다. 이로 인해 API 는 더 빠른 응답을 제공하며 높은 세부 정보가 필요한 경우 토큰 소모를 늘립니다. \n - 고해상도는 먼저 모델에게 저해상도 이미지를 보여주고, 그 후 입력 이미지 크기에 따라 512px의 정사각형 세부 사진을 만듭니다. 각 세부 사진에 대해 129 토큰의 예산을 사용합니다.`, + 고해상도는 먼저 모델에게 저해상도 이미지를 보여주고, 그 후 입력 이미지 크기에 따라 512px 의 정사각형 세부 사진을 만듭니다. 각 세부 사진에 대해 129 토큰의 예산을 사용합니다.`, high: '고', low: '저', uploadMethod: '업로드 방식', @@ -363,12 +364,12 @@ const translation = { }, retrieveMultiWay: { title: '멀티패스 리트리벌', - description: '사용자 의도에 따라 모든 지식을 쿼리하고, 관련 텍스트를 여러 소스에서 가져와 다시 순위를 매긴 후 사용자 쿼리에 가장 적합한 결과를 선택합니다. 재순위 모델 API의 구성이 필요합니다.', + description: '사용자 의도에 따라 모든 지식을 쿼리하고, 관련 텍스트를 여러 소스에서 가져와 다시 순위를 매긴 후 사용자 쿼리에 가장 적합한 결과를 선택합니다. 재순위 모델 API 의 구성이 필요합니다.', }, rerankModelRequired: '재순위 모델이 필요합니다', params: '매개변수', top_k: '상위 K', - top_kTip: '사용자 질문에 가장 유사한 청크를 필터링하는 데 사용됩니다. 시스템은 선택한 모델의 max_tokens에 따라 동적으로 상위 K 값을 조정합니다.', + top_kTip: '사용자 질문에 가장 유사한 청크를 필터링하는 데 사용됩니다. 시스템은 선택한 모델의 max_tokens 에 따라 동적으로 상위 K 값을 조정합니다.', score_threshold: '점수 임계값', score_thresholdTip: '청크 필터링의 유사성 임계값을 설정하는 데 사용됩니다.', retrieveChangeTip: '인덱스 모드 및 리트리벌 모드를 변경하면 이 지식과 관련된 애플리케이션에 영향을 줄 수 있습니다.', @@ -409,7 +410,7 @@ const translation = { promptPlaceholder: '여기에 프롬프트를 입력하세요', tools: { name: '도구', - description: '도구를 사용하여 인터넷 검색이나 과학적 계산 등 LLM의 기능을 확장할 수 있습니다', + description: '도구를 사용하여 인터넷 검색이나 과학적 계산 등 LLM 의 기능을 확장할 수 있습니다', enabled: '활성화됨', }, }, diff --git a/web/i18n/ko-KR/app-log.ts b/web/i18n/ko-KR/app-log.ts index 4017692f5c..6ed33b3c1c 100644 --- a/web/i18n/ko-KR/app-log.ts +++ b/web/i18n/ko-KR/app-log.ts @@ -49,7 +49,7 @@ const translation = { dislike: '좋아요 취소', addAnnotation: '향상 추가', editAnnotation: '향상 편집', - annotationPlaceholder: 'AI가 응답할 것으로 예상하는 답변을 입력하여 향후 모델 세부 조정 및 텍스트 생성 품질 지속적 향상을 위해 개선할 수 있습니다.', + annotationPlaceholder: 'AI 가 응답할 것으로 예상하는 답변을 입력하여 향후 모델 세부 조정 및 텍스트 생성 품질 지속적 향상을 위해 개선할 수 있습니다.', }, variables: '변수', uploadImages: '업로드된 이미지', @@ -58,10 +58,10 @@ const translation = { filter: { period: { today: '오늘', - last7days: '지난 7일', - last4weeks: '지난 4주', - last3months: '지난 3개월', - last12months: '지난 12개월', + last7days: '지난 7 일', + last4weeks: '지난 4 주', + last3months: '지난 3 개월', + last12months: '지난 12 개월', monthToDate: '월 초부터 오늘까지', quarterToDate: '분기 초부터 오늘까지', yearToDate: '연 초부터 오늘까지', @@ -77,7 +77,7 @@ const translation = { ascending: '오름차순', }, workflowTitle: '워크플로우 로그', - workflowSubtitle: '이 로그는 Automate의 작업을 기록했습니다.', + workflowSubtitle: '이 로그는 Automate 의 작업을 기록했습니다.', runDetail: { title: '대화 로그', workflowTitle: '로그 세부 정보', diff --git a/web/i18n/ko-KR/app-overview.ts b/web/i18n/ko-KR/app-overview.ts index 3a9c56367c..136e472a24 100644 --- a/web/i18n/ko-KR/app-overview.ts +++ b/web/i18n/ko-KR/app-overview.ts @@ -25,7 +25,7 @@ const translation = { callTimes: '요청 횟수', usedToken: '사용된 토큰', setAPIBtn: '모델 제공자 설정으로 이동', - tryCloud: '또는 Dify의 클라우드 버전을 무료로 체험해보세요', + tryCloud: '또는 Dify 의 클라우드 버전을 무료로 체험해보세요', }, overview: { title: '개요', @@ -34,7 +34,7 @@ const translation = { accessibleAddress: '공개 URL', preview: '미리보기', regenerate: '재생성', - regenerateNotice: '공개 URL을 재생성하시겠습니까?', + regenerateNotice: '공개 URL 을 재생성하시겠습니까?', preUseReminder: '계속하기 전에 웹앱을 활성화하세요.', settings: { entry: '설정', @@ -48,21 +48,21 @@ const translation = { title: '워크플로 단계', show: '표시', hide: '숨기기', - showDesc: 'WebApp에서 워크플로 세부 정보 표시 또는 숨기기', + showDesc: 'WebApp 에서 워크플로 세부 정보 표시 또는 숨기기', subTitle: '워크플로우 세부 정보', }, chatColorTheme: '챗봇 색상 테마', chatColorThemeDesc: '챗봇의 색상 테마를 설정하세요', chatColorThemeInverted: '반전', - invalidHexMessage: '잘못된 16진수 값', - invalidPrivacyPolicy: '유효하지 않은 개인정보처리방침 링크입니다. http 또는 https로 시작하는 유효한 링크를 사용해 주세요', + invalidHexMessage: '잘못된 16 진수 값', + invalidPrivacyPolicy: '유효하지 않은 개인정보처리방침 링크입니다. http 또는 https 로 시작하는 유효한 링크를 사용해 주세요', more: { entry: '추가 설정 보기', copyright: '저작권', copyRightPlaceholder: '저작권자 또는 조직 이름을 입력하세요', privacyPolicy: '개인정보 처리방침', privacyPolicyPlaceholder: '개인정보 처리방침 링크를 입력하세요', - privacyPolicyTip: '방문자가 애플리케이션이 수집하는 데이터를 이해하고, Dify의 <privacyPolicyLink>개인정보 처리방침</privacyPolicyLink>을 참조할 수 있도록 합니다.', + privacyPolicyTip: '방문자가 애플리케이션이 수집하는 데이터를 이해하고, Dify 의 <privacyPolicyLink>개인정보 처리방침</privacyPolicyLink>을 참조할 수 있도록 합니다.', customDisclaimer: '사용자 지정 면책 조항', customDisclaimerPlaceholder: '사용자 지정 면책 조항 텍스트를 입력합니다.', customDisclaimerTip: '사용자 지정 고지 사항 텍스트는 클라이언트 쪽에 표시되어 응용 프로그램에 대한 추가 정보를 제공합니다', @@ -72,8 +72,8 @@ const translation = { sso: { label: 'SSO 인증', title: '웹앱 SSO', - tooltip: '관리자에게 문의하여 web app SSO를 사용하도록 설정합니다.', - description: '모든 사용자는 WebApp을 사용하기 전에 SSO로 로그인해야 합니다.', + tooltip: '관리자에게 문의하여 web app SSO 를 사용하도록 설정합니다.', + description: '모든 사용자는 WebApp 을 사용하기 전에 SSO 로 로그인해야 합니다.', }, modalTip: '클라이언트 쪽 웹앱 설정.', }, @@ -81,8 +81,8 @@ const translation = { entry: '임베드', title: '웹사이트에 임베드하기', explanation: '챗봇 앱을 웹사이트에 임베드하는 방법을 선택하세요.', - iframe: '웹사이트의 원하는 위치에 챗봇 앱을 추가하려면 이 iframe을 HTML 코드에 추가하세요.', - scripts: '웹사이트의 우측 하단에 챗봇 앱을 추가하려면 이 코드를 HTML에 추가하세요.', + iframe: '웹사이트의 원하는 위치에 챗봇 앱을 추가하려면 이 iframe 을 HTML 코드에 추가하세요.', + scripts: '웹사이트의 우측 하단에 챗봇 앱을 추가하려면 이 코드를 HTML 에 추가하세요.', chromePlugin: 'Dify Chatbot Chrome 확장 프로그램 설치', copied: '복사되었습니다', copy: '복사', @@ -98,18 +98,18 @@ const translation = { title: 'AI 웹앱 사용자화', explanation: '시나리오와 스타일 요구에 따라 웹앱의 프론트엔드를 사용자화할 수 있습니다.', way1: { - name: '클라이언트 코드를 포크하여 수정하고 Vercel에 배포하기 (권장)', + name: '클라이언트 코드를 포크하여 수정하고 Vercel 에 배포하기 (권장)', step1: '클라이언트 코드를 포크하여 수정합니다', step1Tip: '여기를 클릭하여 소스 코드를 GitHub 계정에 포크하고 코드를 수정하세요', step1Operation: 'Dify-WebClient', - step2: 'Vercel에 배포합니다', - step2Tip: '여기를 클릭하여 리포지토리를 Vercel에 임포트하고 배포하세요', + step2: 'Vercel 에 배포합니다', + step2Tip: '여기를 클릭하여 리포지토리를 Vercel 에 임포트하고 배포하세요', step2Operation: '리포지토리 임포트', step3: '환경 변수를 설정합니다', - step3Tip: 'Vercel에 다음 환경 변수를 추가하세요', + step3Tip: 'Vercel 에 다음 환경 변수를 추가하세요', }, way2: { - name: '클라이언트 측 코드를 작성하여 API를 호출하고 서버에 배포합니다', + name: '클라이언트 측 코드를 작성하여 API 를 호출하고 서버에 배포합니다', operation: '문서', }, }, @@ -140,7 +140,7 @@ const translation = { }, activeUsers: { title: '활성 사용자 수', - explanation: 'AI와의 Q&A에 참여하는 고유 사용자 수; 엔지니어링/디버깅 목적의 프롬프트는 제외됩니다.', + explanation: 'AI 와의 Q&A 에 참여하는 고유 사용자 수; 엔지니어링/디버깅 목적의 프롬프트는 제외됩니다.', }, tokenUsage: { title: '토큰 사용량', @@ -149,7 +149,7 @@ const translation = { }, avgSessionInteractions: { title: '평균 세션 상호작용 수', - explanation: '사용자와 AI의 연속적인 커뮤니케이션 수; 대화형 애플리케이션을 위한 것입니다.', + explanation: '사용자와 AI 의 연속적인 커뮤니케이션 수; 대화형 애플리케이션을 위한 것입니다.', }, avgUserInteractions: { title: '평균 사용자 상호작용 수', @@ -157,15 +157,15 @@ const translation = { }, userSatisfactionRate: { title: '사용자 만족도율', - explanation: '1,000개의 메시지 당 "좋아요" 수입니다. 이는 사용자가 매우 만족한 응답의 비율을 나타냅니다.', + explanation: '1,000 개의 메시지 당 "좋아요" 수입니다. 이는 사용자가 매우 만족한 응답의 비율을 나타냅니다.', }, avgResponseTime: { title: '평균 응답 시간', - explanation: 'AI가 처리/응답하는 시간(밀리초); 텍스트 기반 애플리케이션을 위한 것입니다.', + explanation: 'AI 가 처리/응답하는 시간 (밀리초); 텍스트 기반 애플리케이션을 위한 것입니다.', }, tps: { title: '토큰 출력 속도', - explanation: 'LLM의 성능을 측정합니다. 요청 시작부터 출력 완료까지의 LLM의 토큰 출력 속도를 계산합니다.', + explanation: 'LLM 의 성능을 측정합니다. 요청 시작부터 출력 완료까지의 LLM 의 토큰 출력 속도를 계산합니다.', }, }, } diff --git a/web/i18n/ko-KR/app.ts b/web/i18n/ko-KR/app.ts index d800968eaa..7227fd3171 100644 --- a/web/i18n/ko-KR/app.ts +++ b/web/i18n/ko-KR/app.ts @@ -34,7 +34,7 @@ const translation = { workflowWarning: '현재 베타 버전입니다.', chatbotType: '챗봇 오케스트레이션 방식', basic: '기본', - basicTip: '초보자용. 나중에 Chatflow로 전환할 수 있습니다.', + basicTip: '초보자용. 나중에 Chatflow 로 전환할 수 있습니다.', basicFor: '초보자용', basicDescription: '기본 오케스트레이션은 내장된 프롬프트를 수정할 수 없고 간단한 설정을 사용하여 챗봇 앱을 오케스트레이션합니다. 초보자용입니다.', advanced: 'Chatflow', @@ -85,7 +85,7 @@ const translation = { noAppsFound: '앱을 찾을 수 없습니다.', foundResult: '{{개수}} 결과', completionUserDescription: '간단한 구성으로 텍스트 생성 작업을 위한 AI 도우미를 빠르게 구축합니다.', - chatbotUserDescription: '간단한 구성으로 LLM 기반 챗봇을 빠르게 구축할 수 있습니다. 나중에 Chatflow로 전환할 수 있습니다.', + chatbotUserDescription: '간단한 구성으로 LLM 기반 챗봇을 빠르게 구축할 수 있습니다. 나중에 Chatflow 로 전환할 수 있습니다.', workflowShortDescription: '지능형 자동화를 위한 에이전트 플로우', agentUserDescription: '작업 목표를 달성하기 위해 반복적인 추론과 자율적인 도구를 사용할 수 있는 지능형 에이전트입니다.', advancedUserDescription: '메모리 기능과 챗봇 인터페이스를 갖춘 워크플로우', @@ -117,7 +117,7 @@ const translation = { }, tracing: { title: '앱 성능 추적', - description: '제3자 LLMOps 제공업체 구성 및 앱 성능 추적.', + description: '제 3 자 LLMOps 제공업체 구성 및 앱 성능 추적.', config: '구성', collapse: '접기', expand: '펼치기', @@ -125,7 +125,7 @@ const translation = { disabled: '비활성화됨', disabledTip: '먼저 제공업체를 구성해 주세요', enabled: '서비스 중', - tracingDescription: 'LLM 호출, 컨텍스트, 프롬프트, HTTP 요청 등 앱 실행의 전체 컨텍스트를 제3자 추적 플랫폼에 캡처합니다.', + tracingDescription: 'LLM 호출, 컨텍스트, 프롬프트, HTTP 요청 등 앱 실행의 전체 컨텍스트를 제 3 자 추적 플랫폼에 캡처합니다.', configProviderTitle: { configured: '구성됨', notConfigured: '추적을 활성화하려면 제공업체를 구성하세요', @@ -153,27 +153,27 @@ const translation = { view: '보기', opik: { title: '오픽', - description: 'Opik은 LLM 애플리케이션을 평가, 테스트 및 모니터링하기 위한 오픈 소스 플랫폼입니다.', + description: 'Opik 은 LLM 애플리케이션을 평가, 테스트 및 모니터링하기 위한 오픈 소스 플랫폼입니다.', }, weave: { title: '직조하다', - description: 'Weave는 LLM 애플리케이션을 평가하고 테스트하며 모니터링하기 위한 오픈 소스 플랫폼입니다.', + description: 'Weave 는 LLM 애플리케이션을 평가하고 테스트하며 모니터링하기 위한 오픈 소스 플랫폼입니다.', }, }, answerIcon: { description: 'web app 아이콘을 사용하여 공유 응용 프로그램에서 바꿀🤖지 여부', title: 'web app 아이콘을 사용하여 🤖', - descriptionInExplore: 'Explore에서 web app 아이콘을 사용하여 바꿀🤖지 여부', + descriptionInExplore: 'Explore 에서 web app 아이콘을 사용하여 바꿀🤖지 여부', }, - importFromDSL: 'DSL에서 가져오기', + importFromDSL: 'DSL 에서 가져오기', importFromDSLFile: 'DSL 파일에서', - importFromDSLUrl: 'URL에서', + importFromDSLUrl: 'URL 에서', importFromDSLUrlPlaceholder: '여기에 DSL 링크 붙여 넣기', mermaid: { handDrawn: '손으로 그린', classic: '고전', }, - openInExplore: 'Explore에서 열기', + openInExplore: 'Explore 에서 열기', newAppFromTemplate: { sidebar: { Agent: '대리인', @@ -201,7 +201,7 @@ const translation = { notConfiguredTip: '구성이 아직 설정되지 않았습니다.', structured: '구조화된', configure: '설정하다', - moreFillTip: '최대 10단계 중첩을 표시합니다.', + moreFillTip: '최대 10 단계 중첩을 표시합니다.', modelNotSupportedTip: '현재 모델은 이 기능을 지원하지 않으며 자동으로 프롬프트 주입으로 다운그레이드됩니다.', structuredTip: '구조화된 출력은 모델이 제공한 JSON 스키마를 항상 준수하는 응답을 생성하도록 보장하는 기능입니다.', }, @@ -209,12 +209,14 @@ const translation = { anyone: '누구나 웹 앱에 접근할 수 있습니다.', specific: '특정 그룹이나 회원만 웹 앱에 접근할 수 있습니다.', organization: '조직 내 모든 사람이 웹 애플리케이션에 접근할 수 있습니다.', + external: '인증된 외부 사용자만 웹 애플리케이션에 접근할 수 있습니다.', }, accessControlDialog: { accessItems: { anyone: '링크가 있는 누구나', specific: '특정 그룹 또는 구성원', organization: '기업 내의 회원만', + external: '인증된 외부 사용자', }, operateGroupAndMember: { searchPlaceholder: '그룹 및 구성원 검색', diff --git a/web/i18n/ko-KR/billing.ts b/web/i18n/ko-KR/billing.ts index dfb9f6abb2..87ccf27fe0 100644 --- a/web/i18n/ko-KR/billing.ts +++ b/web/i18n/ko-KR/billing.ts @@ -9,7 +9,7 @@ const translation = { buyPermissionDeniedTip: '구독하려면 엔터프라이즈 관리자에게 문의하세요', plansCommon: { title: '당신에게 맞는 요금제를 선택하세요', - yearlyTip: '연간 구독 시 2개월 무료!', + yearlyTip: '연간 구독 시 2 개월 무료!', mostPopular: '가장 인기 있는', planRange: { monthly: '월간', @@ -30,8 +30,8 @@ const translation = { teamMembers: '팀 멤버', buildApps: '앱 만들기', vectorSpace: '벡터 공간', - vectorSpaceBillingTooltip: '1MB당 약 120만 글자의 벡터화된 데이터를 저장할 수 있습니다 (OpenAI Embeddings을 기반으로 추정되며 모델에 따라 다릅니다).', - vectorSpaceTooltip: '벡터 공간은 LLM이 데이터를 이해하는 데 필요한 장기 기억 시스템입니다.', + vectorSpaceBillingTooltip: '1MB 당 약 120 만 글자의 벡터화된 데이터를 저장할 수 있습니다 (OpenAI Embeddings 을 기반으로 추정되며 모델에 따라 다릅니다).', + vectorSpaceTooltip: '벡터 공간은 LLM 이 데이터를 이해하는 데 필요한 장기 기억 시스템입니다.', documentProcessingPriority: '문서 처리 우선순위', documentProcessingPriorityTip: '더 높은 문서 처리 우선순위를 원하시면 요금제를 업그레이드하세요.', documentProcessingPriorityUpgrade: '더 높은 정확성과 빠른 속도로 데이터를 처리합니다.', @@ -74,7 +74,7 @@ const translation = { title: '주석 응답 쿼터', tooltip: '수동으로 편집 및 응답 주석 달기로 앱의 사용자 정의 가능한 고품질 질의응답 기능을 제공합니다 (채팅 앱에만 해당).', }, - ragAPIRequestTooltip: 'Dify의 지식베이스 처리 기능을 호출하는 API 호출 수를 나타냅니다.', + ragAPIRequestTooltip: 'Dify 의 지식베이스 처리 기능을 호출하는 API 호출 수를 나타냅니다.', receiptInfo: '팀 소유자 및 팀 관리자만 구독 및 청구 정보를 볼 수 있습니다', annotationQuota: 'Annotation Quota(주석 할당량)', documentsUploadQuota: '문서 업로드 할당량', @@ -84,7 +84,7 @@ const translation = { apiRateLimit: 'API 요금 한도', cloud: '클라우드 서비스', unlimitedApiRate: 'API 호출 속도 제한 없음', - freeTrialTip: '200회의 OpenAI 호출에 대한 무료 체험.', + freeTrialTip: '200 회의 OpenAI 호출에 대한 무료 체험.', annualBilling: '연간 청구', getStarted: '시작하기', apiRateLimitUnit: '{{count,number}}/일', @@ -94,15 +94,15 @@ const translation = { teamMember_other: '{{count,number}} 팀원', teamMember_one: '{{count,number}} 팀원', priceTip: '작업 공간당/', - apiRateLimitTooltip: 'Dify API를 통한 모든 요청에는 API 요금 한도가 적용되며, 여기에는 텍스트 생성, 채팅 대화, 워크플로 실행 및 문서 처리가 포함됩니다.', + apiRateLimitTooltip: 'Dify API 를 통한 모든 요청에는 API 요금 한도가 적용되며, 여기에는 텍스트 생성, 채팅 대화, 워크플로 실행 및 문서 처리가 포함됩니다.', documentsRequestQuota: '{{count,number}}/분 지식 요청 비율 제한', documentsTooltip: '지식 데이터 소스에서 가져올 수 있는 문서 수에 대한 쿼터.', - documentsRequestQuotaTooltip: '지식 기반 내에서 작업 공간이 분당 수행할 수 있는 총 작업 수를 지정합니다. 여기에는 데이터 세트 생성, 삭제, 업데이트, 문서 업로드, 수정, 보관 및 지식 기반 쿼리가 포함됩니다. 이 지표는 지식 기반 요청의 성능을 평가하는 데 사용됩니다. 예를 들어, 샌드박스 사용자가 1분 이내에 10회의 연속 히트 테스트를 수행하면, 해당 작업 공간은 다음 1분 동안 데이터 세트 생성, 삭제, 업데이트 및 문서 업로드 또는 수정과 같은 작업을 수행하는 것이 일시적으로 제한됩니다.', + documentsRequestQuotaTooltip: '지식 기반 내에서 작업 공간이 분당 수행할 수 있는 총 작업 수를 지정합니다. 여기에는 데이터 세트 생성, 삭제, 업데이트, 문서 업로드, 수정, 보관 및 지식 기반 쿼리가 포함됩니다. 이 지표는 지식 기반 요청의 성능을 평가하는 데 사용됩니다. 예를 들어, 샌드박스 사용자가 1 분 이내에 10 회의 연속 히트 테스트를 수행하면, 해당 작업 공간은 다음 1 분 동안 데이터 세트 생성, 삭제, 업데이트 및 문서 업로드 또는 수정과 같은 작업을 수행하는 것이 일시적으로 제한됩니다.', }, plans: { sandbox: { name: '샌드박스', - description: 'GPT 무료 체험 200회', + description: 'GPT 무료 체험 200 회', includesTitle: '포함된 항목:', for: '핵심 기능 무료 체험', }, diff --git a/web/i18n/ko-KR/common.ts b/web/i18n/ko-KR/common.ts index 4fa8ec74e4..c0d64e71e0 100644 --- a/web/i18n/ko-KR/common.ts +++ b/web/i18n/ko-KR/common.ts @@ -103,20 +103,20 @@ const translation = { model: { params: { temperature: '온도', - temperatureTip: '랜덤성을 제어합니다. 온도를 낮추면 더 랜덤한 결과물을 얻을 수 있습니다. 온도가 0에 가까워질수록 모델은 결정적이고 반복적으로 작동합니다.', - top_p: '상위P', - top_pTip: '뉴클리어스 샘플링에 의한 다양성 제어: 0.5는 모든 확률 가중 옵션의 절반을 고려함을 의미합니다.', + temperatureTip: '랜덤성을 제어합니다. 온도를 낮추면 더 랜덤한 결과물을 얻을 수 있습니다. 온도가 0 에 가까워질수록 모델은 결정적이고 반복적으로 작동합니다.', + top_p: '상위 P', + top_pTip: '뉴클리어스 샘플링에 의한 다양성 제어: 0.5 는 모든 확률 가중 옵션의 절반을 고려함을 의미합니다.', presence_penalty: '존재 페널티', presence_penaltyTip: '이전 텍스트에서 토큰이 나타나는지 여부에 따라 새로운 토큰에 얼마나 많은 페널티를 부여할지 제어합니다. 모델이 새로운 주제에 대해 말할 가능성이 높아집니다.', frequency_penalty: '빈도 페널티', frequency_penaltyTip: '이전 텍스트 내 토큰의 기존 빈도에 따라 새로운 토큰에 얼마나 많은 페널티를 부여할지 제어합니다. 모델이 같은 문구를 글자 그대로 반복할 가능성이 줄어듭니다.', max_tokens: '최대 토큰', max_tokensTip: - '응답의 최대 길이를 토큰 단위로 제한하는 데 사용됩니다. 큰 값은 프롬프트, 채팅 로그 및 남은 공간에 대한 제한을 가질 수 있습니다. 2/3 이하로 설정하는 것이 좋습니다. gpt-4-1106-preview, gpt-4-vision-preview의 최대 토큰 (입력 128k 출력 4k)보다 작게 설정하는 것이 좋습니다.', + '응답의 최대 길이를 토큰 단위로 제한하는 데 사용됩니다. 큰 값은 프롬프트, 채팅 로그 및 남은 공간에 대한 제한을 가질 수 있습니다. 2/3 이하로 설정하는 것이 좋습니다. gpt-4-1106-preview, gpt-4-vision-preview 의 최대 토큰 (입력 128k 출력 4k) 보다 작게 설정하는 것이 좋습니다.', maxTokenSettingTip: '최대 토큰 설정이 높아서 프롬프트, 쿼리 및 데이터 공간에 제한이 생길 수 있습니다. 현재 모델의 최대 토큰의 80% 이하로 설정해주세요.', - setToCurrentModelMaxTokenTip: '최대 토큰이 현재 모델의 최대 토큰의 80%로 업데이트되었습니다 {{maxToken}}.', + setToCurrentModelMaxTokenTip: '최대 토큰이 현재 모델의 최대 토큰의 80% 로 업데이트되었습니다 {{maxToken}}.', stop_sequences: '중단 시퀀스', - stop_sequencesTip: 'API가 진행 중인 토큰 생성을 중단하는 최대 4개의 시퀀스입니다. 반환된 텍스트에는 중단 시퀀스가 포함되지 않습니다.', + stop_sequencesTip: 'API 가 진행 중인 토큰 생성을 중단하는 최대 4 개의 시퀀스입니다. 반환된 텍스트에는 중단 시퀀스가 포함되지 않습니다.', stop_sequencesPlaceholder: '시퀀스를 입력하고 탭 키를 누르세요', }, tone: { @@ -201,7 +201,7 @@ const translation = { deletePlaceholder: '이메일을 입력해 주세요', sendVerificationButton: '인증 코드 보내기', verificationLabel: '인증 코드', - verificationPlaceholder: '6자리 코드를 붙여넣습니다.', + verificationPlaceholder: '6 자리 코드를 붙여넣습니다.', permanentlyDeleteButton: '계정 영구 삭제', feedbackTitle: '피드백', feedbackLabel: '계정을 삭제한 이유를 알려주시겠습니까?', @@ -234,7 +234,7 @@ const translation = { sendInvite: '초대 보내기', invitedAsRole: '{{role}} 사용자로 초대되었습니다', invitationSent: '초대가 전송되었습니다', - invitationSentTip: '초대가 전송되었으며, 그들은 Dify에 로그인하여 당신의 팀 데이터에 액세스할 수 있습니다.', + invitationSentTip: '초대가 전송되었으며, 그들은 Dify 에 로그인하여 당신의 팀 데이터에 액세스할 수 있습니다.', invitationLink: '초대 링크', failedInvitationEmails: '다음 사용자들은 성공적으로 초대되지 않았습니다', ok: '확인', @@ -271,7 +271,7 @@ const translation = { validatedError: '검증 실패:', validating: '키를 확인하는 중...', saveFailed: 'API 키 저장 실패', - apiKeyExceedBill: '이 API KEY에는 사용 가능한 할당량이 없습니다. 자세한 내용은', + apiKeyExceedBill: '이 API KEY 에는 사용 가능한 할당량이 없습니다. 자세한 내용은', addKey: '키 추가', comingSoon: '곧 출시됨', editKey: '편집', @@ -287,7 +287,7 @@ const translation = { openaiHosted: '호스팅된 OpenAI', onTrial: '트라이얼 중', exhausted: '할당량이 다 사용되었습니다', - desc: 'Dify가 제공하는 OpenAI 호스팅 서비스를 사용하면 GPT-3.5와 같은 모델을 사용할 수 있습니다. 트라이얼 할당량이 다 사용되기 전에 다른 모델 제공자를 설정해야 합니다.', + desc: 'Dify 가 제공하는 OpenAI 호스팅 서비스를 사용하면 GPT-3.5 와 같은 모델을 사용할 수 있습니다. 트라이얼 할당량이 다 사용되기 전에 다른 모델 제공자를 설정해야 합니다.', callTimes: '호출 횟수', usedUp: '트라이얼 할당량이 다 사용되었습니다. 다른 모델 제공자를 추가하세요.', useYourModel: '현재 사용자 정의 모델 제공자를 사용 중입니다.', @@ -308,10 +308,10 @@ const translation = { using: '임베드 기능을 사용 중입니다', enableTip: 'Anthropic 모델을 활성화하려면 먼저 OpenAI 또는 Azure OpenAI 서비스에 바인딩해야 합니다.', notEnabled: '비활성화됨', - keyFrom: 'Anthropic에서 API 키를 받으세요', + keyFrom: 'Anthropic 에서 API 키를 받으세요', }, encrypted: { - front: 'API KEY는', + front: 'API KEY 는', back: '기술을 사용하여 암호화 및 저장됩니다.', }, }, @@ -366,7 +366,7 @@ const translation = { tip: '지불된 할당량에 우선순위가 부여됩니다. 평가판 할당량은 유료 할당량이 소진된 후 사용됩니다.', }, item: { - deleteDesc: '{{modelName}}은(는) 시스템 추론 모델로 사용 중입니다. 제거 후 일부 기능을 사용할 수 없습니다. 확인하시겠습니까?', + deleteDesc: '{{modelName}}은 (는) 시스템 추론 모델로 사용 중입니다. 제거 후 일부 기능을 사용할 수 없습니다. 확인하시겠습니까?', freeQuota: '무료 할당량', }, addApiKey: 'API 키 추가', @@ -401,7 +401,7 @@ const translation = { apiKey: 'API 키', defaultConfig: '기본 구성', providerManaged: '제공자 관리', - loadBalancing: '부하 분산Load balancing', + loadBalancing: '부하 분산 Load balancing', addConfig: '구성 추가', apiKeyStatusNormal: 'APIKey 상태는 정상입니다.', configLoadBalancing: 'Config 로드 밸런싱', @@ -411,8 +411,8 @@ const translation = { loadBalancingDescription: '여러 자격 증명 세트로 부담을 줄입니다.', upgradeForLoadBalancing: '로드 밸런싱을 사용하도록 계획을 업그레이드합니다.', apiKeyRateLimit: '속도 제한에 도달했으며, {{seconds}}s 후에 사용할 수 있습니다.', - loadBalancingInfo: '기본적으로 부하 분산은 라운드 로빈 전략을 사용합니다. 속도 제한이 트리거되면 1분의 휴지 기간이 적용됩니다.', - loadBalancingLeastKeyWarning: '로드 밸런싱을 사용하려면 최소 2개의 키를 사용하도록 설정해야 합니다.', + loadBalancingInfo: '기본적으로 부하 분산은 라운드 로빈 전략을 사용합니다. 속도 제한이 트리거되면 1 분의 휴지 기간이 적용됩니다.', + loadBalancingLeastKeyWarning: '로드 밸런싱을 사용하려면 최소 2 개의 키를 사용하도록 설정해야 합니다.', providerManagedDescription: '모델 공급자가 제공하는 단일 자격 증명 집합을 사용합니다.', installProvider: '모델 공급자 설치', discoverMore: '더 알아보기', @@ -484,7 +484,7 @@ const translation = { apiKey: { title: 'API 키', placeholder: 'API 키를 입력하세요', - lengthError: 'API 키는 5자 미만이어야 합니다', + lengthError: 'API 키는 5 자 미만이어야 합니다', }, }, type: '유형', @@ -604,7 +604,7 @@ const translation = { uploadFromComputer: '컴퓨터에서 업로드', uploadFromComputerReadError: '이미지 읽기 실패. 다시 시도하세요.', uploadFromComputerUploadError: '이미지 업로드 실패. 다시 업로드하세요.', - uploadFromComputerLimit: '업로드 이미지 크기는 {{size}} MB를 초과할 수 없습니다', + uploadFromComputerLimit: '업로드 이미지 크기는 {{size}} MB 를 초과할 수 없습니다', pasteImageLink: '이미지 링크 붙여넣기', pasteImageLinkInputPlaceholder: '여기에 이미지 링크를 붙여넣으세요', pasteImageLinkInvalid: '유효하지 않은 이미지 링크', @@ -626,7 +626,7 @@ const translation = { failed: '태그 생성에 실패했습니다', }, errorMsg: { - urlError: 'URL은 http:// 또는 https:// 로 시작해야 합니다.', + urlError: 'URL 은 http:// 또는 https:// 로 시작해야 합니다.', fieldRequired: '{{field}}는 필수입니다.', }, fileUploader: { @@ -662,7 +662,7 @@ const translation = { sandboxUpgradeTooltip: '전문가 또는 팀 플랜에서만 사용할 수 있습니다.', }, imageInput: { - supportedFormats: 'PNG, JPG, JPEG, WEBP 및 GIF를 지원합니다.', + supportedFormats: 'PNG, JPG, JPEG, WEBP 및 GIF 를 지원합니다.', browse: '브라우즈', dropImageHere: '여기에 이미지를 드롭하거나', }, diff --git a/web/i18n/ko-KR/custom.ts b/web/i18n/ko-KR/custom.ts index f5bb34008d..9b70e7326a 100644 --- a/web/i18n/ko-KR/custom.ts +++ b/web/i18n/ko-KR/custom.ts @@ -10,11 +10,11 @@ const translation = { title: 'web app 브랜드 사용자 정의', removeBrand: 'Powered by Dify 삭제', changeLogo: 'Powered by 브랜드 이미지 변경', - changeLogoTip: '최소 크기 40x40px의 SVG 또는 PNG 형식', + changeLogoTip: '최소 크기 40x40px 의 SVG 또는 PNG 형식', }, app: { title: '앱 헤더 브랜드 사용자 정의', - changeLogoTip: '최소 크기 80x80px의 SVG 또는 PNG 형식', + changeLogoTip: '최소 크기 80x80px 의 SVG 또는 PNG 형식', }, upload: '업로드', uploading: '업로드 중', diff --git a/web/i18n/ko-KR/dataset-creation.ts b/web/i18n/ko-KR/dataset-creation.ts index 4b5ee3f03f..33f6e332a0 100644 --- a/web/i18n/ko-KR/dataset-creation.ts +++ b/web/i18n/ko-KR/dataset-creation.ts @@ -24,19 +24,19 @@ const translation = { title: '텍스트 파일 업로드', button: '파일이나 폴더를 끌어서 놓기', browse: '찾아보기', - tip: '{{supportTypes}}을(를) 지원합니다. 파일당 최대 크기는 {{size}}MB입니다.', + tip: '{{supportTypes}}을 (를) 지원합니다. 파일당 최대 크기는 {{size}}MB 입니다.', validation: { typeError: '지원되지 않는 파일 유형입니다', - size: '파일 크기가 너무 큽니다. 최대 크기는 {{size}}MB입니다', + size: '파일 크기가 너무 큽니다. 최대 크기는 {{size}}MB 입니다', count: '여러 파일은 지원되지 않습니다', - filesNumber: '일괄 업로드 제한({{filesNumber}}개)에 도달했습니다.', + filesNumber: '일괄 업로드 제한 ({{filesNumber}}개) 에 도달했습니다.', }, cancel: '취소', change: '변경', failed: '업로드에 실패했습니다', }, - notionSyncTitle: 'Notion에 연결되지 않았습니다', - notionSyncTip: 'Notion과 동기화하려면 먼저 Notion에 연결해야 합니다.', + notionSyncTitle: 'Notion 에 연결되지 않았습니다', + notionSyncTip: 'Notion 과 동기화하려면 먼저 Notion 에 연결해야 합니다.', connect: '연결하기', button: '다음', emptyDatasetCreation: '비어있는 지식 생성', @@ -46,7 +46,7 @@ const translation = { input: '지식 이름', placeholder: '입력하세요', nameNotEmpty: '이름은 비워둘 수 없습니다', - nameLengthInvalid: '이름은 1~40자여야 합니다', + nameLengthInvalid: '이름은 1~40 자여야 합니다', cancelButton: '취소', confirmButton: '생성', failed: '생성에 실패했습니다', @@ -62,8 +62,8 @@ const translation = { excludePaths: '경로 제외', preview: '미리 보기', run: '달리다', - fireCrawlNotConfigured: 'Firecrawl이 구성되지 않았습니다.', - firecrawlTitle: 'Firecrawl로 🔥웹 콘텐츠 추출', + fireCrawlNotConfigured: 'Firecrawl 이 구성되지 않았습니다.', + firecrawlTitle: 'Firecrawl 로 🔥웹 콘텐츠 추출', configure: '구성', resetAll: '모두 재설정', crawlSubPage: '하위 페이지 크롤링', @@ -71,24 +71,24 @@ const translation = { scrapTimeInfo: '{{time}}s 내에 총 {{total}} 페이지를 스크랩했습니다.', unknownError: '알 수 없는 오류', totalPageScraped: '스크랩한 총 페이지 수:', - fireCrawlNotConfiguredDescription: 'API 키로 Firecrawl을 구성하여 사용합니다.', - extractOnlyMainContent: '기본 콘텐츠만 추출합니다(머리글, 탐색, 바닥글 등 없음).', - maxDepthTooltip: '입력한 URL을 기준으로 크롤링할 최대 수준입니다. 깊이 0은 입력 된 url의 페이지를 긁어 내고, 깊이 1은 url과 enteredURL + one / 이후의 모든 것을 긁어 모으는 식입니다.', + fireCrawlNotConfiguredDescription: 'API 키로 Firecrawl 을 구성하여 사용합니다.', + extractOnlyMainContent: '기본 콘텐츠만 추출합니다 (머리글, 탐색, 바닥글 등 없음).', + maxDepthTooltip: '입력한 URL 을 기준으로 크롤링할 최대 수준입니다. 깊이 0 은 입력 된 url 의 페이지를 긁어 내고, 깊이 1 은 url 과 enteredURL + one / 이후의 모든 것을 긁어 모으는 식입니다.', chooseProvider: '제공자 선택', jinaReaderDocLink: 'https://jina.ai/reader', useSitemap: '사이트맵 사용', - jinaReaderNotConfiguredDescription: '액세스를 위해 무료 API 키를 입력하여 Jina Reader를 설정합니다.', - jinaReaderDoc: 'Jina Reader에 대해 자세히 알아보기', - jinaReaderTitle: '전체 사이트를 Markdown으로 변환', - jinaReaderNotConfigured: 'Jina Reader가 구성되지 않았습니다.', - useSitemapTooltip: '사이트맵을 따라 사이트를 크롤링합니다. 그렇지 않은 경우 Jina Reader는 페이지 관련성에 따라 반복적으로 크롤링하여 더 적지만 더 높은 품질의 페이지를 생성합니다.', + jinaReaderNotConfiguredDescription: '액세스를 위해 무료 API 키를 입력하여 Jina Reader 를 설정합니다.', + jinaReaderDoc: 'Jina Reader 에 대해 자세히 알아보기', + jinaReaderTitle: '전체 사이트를 Markdown 으로 변환', + jinaReaderNotConfigured: 'Jina Reader 가 구성되지 않았습니다.', + useSitemapTooltip: '사이트맵을 따라 사이트를 크롤링합니다. 그렇지 않은 경우 Jina Reader 는 페이지 관련성에 따라 반복적으로 크롤링하여 더 적지만 더 높은 품질의 페이지를 생성합니다.', watercrawlDoc: '워터크롤 문서', - waterCrawlNotConfiguredDescription: 'API 키로 Watercrawl을 구성하여 사용하십시오.', - watercrawlTitle: 'Watercrawl로 웹 콘텐츠 추출하기', + waterCrawlNotConfiguredDescription: 'API 키로 Watercrawl 을 구성하여 사용하십시오.', + watercrawlTitle: 'Watercrawl 로 웹 콘텐츠 추출하기', configureFirecrawl: '파이어크롤 구성하기', watercrawlDocLink: '웹사이트에서 동기화하기', configureJinaReader: '지나 리더 설정하기', - waterCrawlNotConfigured: 'Watercrawl이 설정되어 있지 않습니다.', + waterCrawlNotConfigured: 'Watercrawl 이 설정되어 있지 않습니다.', configureWatercrawl: '워터크롤 구성하기', }, cancel: '취소', @@ -100,15 +100,15 @@ const translation = { custom: '사용자 설정', customDescription: '청크 규칙, 청크 길이, 전처리 규칙 등을 사용자 정의합니다.', separator: '세그먼트 식별자', - separatorPlaceholder: '예: 줄바꿈(\\\\n) 또는 특수 구분자(예: "***")', + separatorPlaceholder: '예: 줄바꿈 (\\\\n) 또는 특수 구분자 (예: "***")', maxLength: '최대 청크 길이', overlap: '청크 중첩', - overlapTip: '청크 중첩을 설정하여 그 사이의 의미적 연관성을 유지하고 검색 효과를 향상시킬 수 있습니다. 최대 청크 크기의 10%~25%로 설정하는 것이 좋습니다.', + overlapTip: '청크 중첩을 설정하여 그 사이의 의미적 연관성을 유지하고 검색 효과를 향상시킬 수 있습니다. 최대 청크 크기의 10%~25% 로 설정하는 것이 좋습니다.', overlapCheck: '청크 중첩은 최대 청크 길이를 초과할 수 없습니다', rules: '텍스트 전처리 규칙', removeExtraSpaces: '연속된 공백, 줄바꿈, 탭을 대체합니다', - removeUrlEmails: '모든 URL과 이메일 주소를 제거합니다', - removeStopwords: '일반적인 불용어(예: "a", "an", "the" 등)를 제거합니다', + removeUrlEmails: '모든 URL 과 이메일 주소를 제거합니다', + removeStopwords: '일반적인 불용어 (예: "a", "an", "the" 등) 를 제거합니다', preview: '미리보기', reset: '초기화', indexMode: '인덱스 모드', @@ -142,7 +142,7 @@ const translation = { sideTipP4: '적절한 청크와 클리닝은 모델의 성능을 향상시키고 정확하고 가치 있는 결과를 제공합니다.', previewTitle: '미리보기', previewTitleButton: '미리보기', - previewButton: '질문-답변 형식으로 전환', + previewButton: '질문 - 답변 형식으로 전환', previewSwitchTipStart: '현재 청크 미리보기는 텍스트 형식입니다. 질문과 답변 형식 미리보기로 전환하면', previewSwitchTipEnd: ' 추가 토큰이 소비됩니다', characters: '문자', @@ -151,25 +151,25 @@ const translation = { datasetSettingLink: '지식 설정', webpageUnit: '페이지', websiteSource: '웹 사이트 전처리', - separatorTip: '구분 기호는 텍스트를 구분하는 데 사용되는 문자입니다. \\n\\n 및 \\n은 단락과 줄을 구분하는 데 일반적으로 사용되는 구분 기호입니다. 쉼표(\\n\\n,\\n)와 함께 사용하면 최대 청크 길이를 초과할 경우 단락이 줄로 분할됩니다. 직접 정의한 특수 구분 기호(예: ***)를 사용할 수도 있습니다.', + separatorTip: '구분 기호는 텍스트를 구분하는 데 사용되는 문자입니다. \\n\\n 및 \\n은 단락과 줄을 구분하는 데 일반적으로 사용되는 구분 기호입니다. 쉼표 (\\n\\n,\\n) 와 함께 사용하면 최대 청크 길이를 초과할 경우 단락이 줄로 분할됩니다. 직접 정의한 특수 구분 기호 (예: ***) 를 사용할 수도 있습니다.', maxLengthCheck: '최대 청크 길이는 {{limit}} 미만이어야 합니다.', childChunkForRetrieval: '검색을 위한 자식 청크', qaSwitchHighQualityTipContent: '현재 고품질 인덱스 방법만 Q&A 형식 청크를 지원합니다. 고화질 모드로 전환하시겠습니까?', previewChunkTip: '왼쪽의 \'Preview Chunk\' 버튼을 클릭하여 프리뷰를 로드합니다', general: '일반', fullDoc: '전체 문서', - previewChunk: '프리뷰 청크(Preview Chunk)', + previewChunk: '프리뷰 청크 (Preview Chunk)', parentChunkForContext: '컨텍스트에 대한 Parent-chunk', parentChildDelimiterTip: '구분 기호는 텍스트를 구분하는 데 사용되는 문자입니다. \\n\\n은 원본 문서를 큰 부모 청크로 분할하는 데 권장됩니다. 직접 정의한 특수 구분 기호를 사용할 수도 있습니다.', paragraph: '단락', - parentChild: '부모-자식', + parentChild: '부모 - 자식', useQALanguage: 'Q&A 형식을 사용하는 청크', highQualityTip: '고품질 모드에서 삽입을 마치면 경제적 모드로 되돌릴 수 없습니다.', notAvailableForQA: 'Q&A 인덱스에는 사용할 수 없습니다.', qaSwitchHighQualityTipTitle: 'Q&A 형식에는 고품질 인덱싱 방법이 필요합니다.', - notAvailableForParentChild: '부모-자식 인덱스에는 사용할 수 없습니다.', + notAvailableForParentChild: '부모 - 자식 인덱스에는 사용할 수 없습니다.', previewChunkCount: '{{개수}} 추정된 청크', - parentChildTip: '부모-자식 모드를 사용할 때 자식 청크는 검색에 사용되고 부모 청크는 컨텍스트로 회수에 사용됩니다.', + parentChildTip: '부모 - 자식 모드를 사용할 때 자식 청크는 검색에 사용되고 부모 청크는 컨텍스트로 회수에 사용됩니다.', generalTip: '일반적인 텍스트 청크 모드에서는 검색된 청크와 회수된 청크가 동일합니다.', fullDocTip: '전체 문서가 상위 청크로 사용되며 직접 검색됩니다. 성능상의 이유로 10000 토큰을 초과하는 텍스트는 자동으로 잘립니다.', parentChildChunkDelimiterTip: '구분 기호는 텍스트를 구분하는 데 사용되는 문자입니다. \\n 은 부모 청크를 작은 자식 청크로 분할하는 데 권장됩니다. 직접 정의한 특수 구분 기호를 사용할 수도 있습니다.', @@ -207,12 +207,12 @@ const translation = { otherDataSource: { learnMore: '더 알아보세요', title: '다른 데이터 소스에 연결하시겠습니까?', - description: '현재 Dify의 기술 자료에는 제한된 데이터 소스만 있습니다. Dify 기술 자료에 데이터 소스를 제공하는 것은 모든 사용자를 위해 플랫폼의 유연성과 기능을 향상시키는 데 도움이 되는 환상적인 방법입니다. 기여 가이드를 통해 쉽게 시작할 수 있습니다. 자세한 내용은 아래 링크를 클릭하십시오.', + description: '현재 Dify 의 기술 자료에는 제한된 데이터 소스만 있습니다. Dify 기술 자료에 데이터 소스를 제공하는 것은 모든 사용자를 위해 플랫폼의 유연성과 기능을 향상시키는 데 도움이 되는 환상적인 방법입니다. 기여 가이드를 통해 쉽게 시작할 수 있습니다. 자세한 내용은 아래 링크를 클릭하십시오.', }, watercrawl: { - getApiKeyLinkText: 'watercrawl.dev에서 API 키를 얻으세요.', + getApiKeyLinkText: 'watercrawl.dev 에서 API 키를 얻으세요.', configWatercrawl: '워터크롤 구성하기', - apiKeyPlaceholder: 'watercrawl.dev의 API 키', + apiKeyPlaceholder: 'watercrawl.dev 의 API 키', }, } diff --git a/web/i18n/ko-KR/dataset-documents.ts b/web/i18n/ko-KR/dataset-documents.ts index a00d3a7c25..a379318959 100644 --- a/web/i18n/ko-KR/dataset-documents.ts +++ b/web/i18n/ko-KR/dataset-documents.ts @@ -1,7 +1,7 @@ const translation = { list: { title: '문서', - desc: '지식의 모든 파일이 여기에 표시되며, 전체 지식이 Dify의 인용문이나 챗 플러그인을 통해 링크되거나 색인화될 수 있습니다.', + desc: '지식의 모든 파일이 여기에 표시되며, 전체 지식이 Dify 의 인용문이나 챗 플러그인을 통해 링크되거나 색인화될 수 있습니다.', addFile: '파일 추가', addPages: '페이지 추가', table: { @@ -49,10 +49,10 @@ const translation = { empty: { title: '아직 문서가 없습니다', upload: { - tip: '파일을 업로드하거나 웹 사이트에서 동기화하거나 Notion이나 GitHub 같은 웹 앱에서 동기화할 수 있습니다.', + tip: '파일을 업로드하거나 웹 사이트에서 동기화하거나 Notion 이나 GitHub 같은 웹 앱에서 동기화할 수 있습니다.', }, sync: { - tip: 'Dify는 정기적으로 Notion에서 파일을 다운로드하고 처리합니다.', + tip: 'Dify 는 정기적으로 Notion 에서 파일을 다운로드하고 처리합니다.', }, }, delete: { @@ -82,8 +82,8 @@ const translation = { }, metadata: { title: '메타데이터', - desc: '문서 메타데이터에 레이블을 붙여 AI가 신속하게 접근할 수 있고 사용자에게 출처가 공개됩니다.', - dateTimeFormat: 'YYYY년 M월 D일 hh:mm A', + desc: '문서 메타데이터에 레이블을 붙여 AI 가 신속하게 접근할 수 있고 사용자에게 출처가 공개됩니다.', + dateTimeFormat: 'YYYY 년 M 월 D 일 hh:mm A', docTypeSelectTitle: '문서 유형을 선택하세요', docTypeChangeTitle: '문서 유형 변경', docTypeSelectWarning: '문서 유형을 변경하면 현재 입력된 메타데이터가 유지되지 않습니다.', @@ -94,8 +94,8 @@ const translation = { }, source: { upload_file: '파일 업로드', - notion: 'Notion에서 동기화', - github: 'GitHub에서 동기화', + notion: 'Notion 에서 동기화', + github: 'GitHub 에서 동기화', }, type: { book: '도서', @@ -106,8 +106,8 @@ const translation = { businessDocument: '비즈니스 문서', IMChat: 'IM 채팅', wikipediaEntry: '위키피디아 항목', - notion: 'Notion에서 동기화', - github: 'GitHub에서 동기화', + notion: 'Notion 에서 동기화', + github: 'GitHub 에서 동기화', technicalParameters: '기술적 매개변수', }, field: { @@ -332,13 +332,13 @@ const translation = { childMaxTokens: '아이', parentMaxTokens: '부모', pause: '일시 중지', - hierarchical: '부모-자식', + hierarchical: '부모 - 자식', }, segment: { paragraphs: '단락', keywords: '키워드', addKeyWord: '키워드 추가', - keywordError: '키워드 최대 길이는 20자입니다', + keywordError: '키워드 최대 길이는 20 자입니다', characters: '문자', hitCount: '검색 횟수', vectorHash: '벡터 해시: ', @@ -351,41 +351,41 @@ const translation = { newTextSegment: '새로운 텍스트 세그먼트', newQaSegment: '새로운 Q&A 세그먼트', delete: '이 청크를 삭제하시겠습니까?', - parentChunks_one: '부모 청크(PARENT CHUNK)', + parentChunks_one: '부모 청크 (PARENT CHUNK)', newChunk: '새 청크', - addChildChunk: '자손 청크 추가(Add Child Chunk)', - editChildChunk: '자손 청크 편집(Edit Child Chunk)', - chunkDetail: '청크 디테일(Chunk Detail)', - editChunk: '청크 편집(Edit Chunk)', + addChildChunk: '자손 청크 추가 (Add Child Chunk)', + editChildChunk: '자손 청크 편집 (Edit Child Chunk)', + chunkDetail: '청크 디테일 (Chunk Detail)', + editChunk: '청크 편집 (Edit Chunk)', regeneratingTitle: '자식 청크 재생성', - newChildChunk: '새 자손 청크(New Child Chunk)', - childChunkAdded: '자식 청크 1개 추가됨', + newChildChunk: '새 자손 청크 (New Child Chunk)', + childChunkAdded: '자식 청크 1 개 추가됨', chunk: '덩어리', searchResults_zero: '결과', empty: '청크를 찾을 수 없습니다.', - editParentChunk: '부모 청크 편집(Edit Parent Chunk)', + editParentChunk: '부모 청크 편집 (Edit Parent Chunk)', chunks_one: '덩어리', regenerationSuccessMessage: '이 창을 닫을 수 있습니다.', - childChunks_one: '자식 청크(CHILD CHUNK)', + childChunks_one: '자식 청크 (CHILD CHUNK)', regenerationSuccessTitle: '재생이 완료되었습니다.', editedAt: '편집 위치', addAnother: '다른 항목 추가', - chunkAdded: '청크 1개 추가됨', + chunkAdded: '청크 1 개 추가됨', searchResults_one: '결과', searchResults_other: '결과', regenerationConfirmMessage: '자식 청크를 다시 생성하면 편집된 청크와 새로 추가된 청크를 포함하여 현재 자식 청크를 덮어씁니다. 재생성은 취소할 수 없습니다.', regenerationConfirmTitle: '자식 청크를 다시 생성하시겠습니까?', clearFilter: '필터 지우기', characters_one: '문자', - parentChunk: '부모-청크', + parentChunk: '부모 - 청크', expandChunks: '청크 확장', collapseChunks: '청크 축소', - parentChunks_other: '부모 청크(PARENT CHUNKS)', + parentChunks_other: '부모 청크 (PARENT CHUNKS)', childChunk: '자식 청크', childChunks_other: '자식 청크', chunks_other: '청크', edited: '편집', - addChunk: '청크 추가(Add Chunk)', + addChunk: '청크 추가 (Add Chunk)', characters_other: '문자', regeneratingMessage: '시간이 걸릴 수 있으니 잠시만 기다려 주십시오...', }, diff --git a/web/i18n/ko-KR/dataset-hit-testing.ts b/web/i18n/ko-KR/dataset-hit-testing.ts index a5329fbdb5..17ab7db08a 100644 --- a/web/i18n/ko-KR/dataset-hit-testing.ts +++ b/web/i18n/ko-KR/dataset-hit-testing.ts @@ -13,7 +13,7 @@ const translation = { input: { title: '소스 텍스트', placeholder: '텍스트를 입력하세요. 간결한 설명문이 좋습니다.', - countWarning: '최대 200자까지 입력할 수 있습니다.', + countWarning: '최대 200 자까지 입력할 수 있습니다.', indexWarning: '고품질 지식만.', testing: '테스트 중', }, @@ -29,7 +29,7 @@ const translation = { records: '레코드', hitChunks: '{{num}}개의 자식 청크를 히트했습니다.', keyword: '키워드', - chunkDetail: '청크 디테일(Chunk Detail)', + chunkDetail: '청크 디테일 (Chunk Detail)', } export default translation diff --git a/web/i18n/ko-KR/dataset-settings.ts b/web/i18n/ko-KR/dataset-settings.ts index b008e37ccd..272cd4b9f9 100644 --- a/web/i18n/ko-KR/dataset-settings.ts +++ b/web/i18n/ko-KR/dataset-settings.ts @@ -7,7 +7,7 @@ const translation = { nameError: '이름은 비워둘 수 없습니다', desc: '지식 설명', descInfo: '지식 내용을 개괄하는 명확한 텍스트 설명을 작성하세요. 이 설명은 여러 지식 중에서 선택하는 기준으로 사용됩니다.', - descPlaceholder: '이 지식에 포함된 내용을 설명하세요. 자세한 설명은 AI가 지식 내용에 빠르게 접근할 수 있도록 합니다. 비어 있으면 Dify가 기본 검색 전략을 사용합니다.', + descPlaceholder: '이 지식에 포함된 내용을 설명하세요. 자세한 설명은 AI 가 지식 내용에 빠르게 접근할 수 있도록 합니다. 비어 있으면 Dify 가 기본 검색 전략을 사용합니다.', descWrite: '좋은 지식 설명 작성 방법 배우기', permissions: '권한', permissionsOnlyMe: '나만', @@ -34,7 +34,7 @@ const translation = { externalKnowledgeID: '외부 지식 ID', retrievalSettings: '검색 설정', upgradeHighQualityTip: '고품질 모드로 업그레이드한 후에는 경제적 모드로 되돌릴 수 없습니다.', - indexMethodChangeToEconomyDisabledTip: 'HQ에서 ECO로 다운그레이드할 수 없습니다.', + indexMethodChangeToEconomyDisabledTip: 'HQ 에서 ECO 로 다운그레이드할 수 없습니다.', helpText: '좋은 데이터 세트 설명을 작성하는 방법을 알아보세요.', searchModel: '모델 검색', }, diff --git a/web/i18n/ko-KR/dataset.ts b/web/i18n/ko-KR/dataset.ts index a95e039932..3eb4634194 100644 --- a/web/i18n/ko-KR/dataset.ts +++ b/web/i18n/ko-KR/dataset.ts @@ -67,17 +67,17 @@ const translation = { semantic: '의미론적', keyword: '키워드', }, - nTo1RetrievalLegacy: 'N-대-1 검색은 9월부터 공식적으로 더 이상 사용되지 않습니다. 더 나은 결과를 얻으려면 최신 다중 경로 검색을 사용하는 것이 좋습니다.', + nTo1RetrievalLegacy: 'N-대 -1 검색은 9 월부터 공식적으로 더 이상 사용되지 않습니다. 더 나은 결과를 얻으려면 최신 다중 경로 검색을 사용하는 것이 좋습니다.', nTo1RetrievalLegacyLink: '자세히 알아보기', - nTo1RetrievalLegacyLinkText: 'N-대-1 검색은 9월에 공식적으로 더 이상 사용되지 않습니다.', + nTo1RetrievalLegacyLinkText: 'N-대 -1 검색은 9 월에 공식적으로 더 이상 사용되지 않습니다.', defaultRetrievalTip: '다중 경로 검색이 기본적으로 사용됩니다. 지식은 여러 기술 자료에서 검색된 다음 순위가 다시 매겨집니다.', editExternalAPIConfirmWarningContent: { - front: '이 외부 지식 API는 다음에 연결됩니다.', + front: '이 외부 지식 API 는 다음에 연결됩니다.', end: '외부 지식, 그리고 이 수정 사항은 그들 모두에게 적용될 것입니다. 이 변경 사항을 저장하시겠습니까?', }, editExternalAPIFormWarning: { end: '외부 지식', - front: '이 외부 API는 다음에 연결됩니다.', + front: '이 외부 API 는 다음에 연결됩니다.', }, deleteExternalAPIConfirmWarningContent: { title: { @@ -85,25 +85,25 @@ const translation = { end: '?', }, content: { - front: '이 외부 지식 API는 다음에 연결됩니다.', - end: '외부 지식. 이 API를 삭제하면 모두 무효화됩니다. 이 API를 삭제하시겠습니까?', + front: '이 외부 지식 API 는 다음에 연결됩니다.', + end: '외부 지식. 이 API 를 삭제하면 모두 무효화됩니다. 이 API 를 삭제하시겠습니까?', }, - noConnectionContent: '이 API를 삭제하시겠습니까?', + noConnectionContent: '이 API 를 삭제하시겠습니까?', }, selectExternalKnowledgeAPI: { placeholder: '외부 지식 API 선택', }, connectDatasetIntro: { content: { - link: '외부 API를 만드는 방법 알아보기', - end: '. 그런 다음 해당 기술 ID를 찾아 왼쪽 양식에 입력합니다. 모든 정보가 올바르면 연결 단추를 클릭한 후 기술 자료의 검색 테스트로 자동으로 이동합니다.', - front: '외부 기술 자료에 연결하려면 먼저 외부 API를 만들어야 합니다. 주의 깊게 읽고 참조하십시오.', + link: '외부 API 를 만드는 방법 알아보기', + end: '. 그런 다음 해당 기술 ID 를 찾아 왼쪽 양식에 입력합니다. 모든 정보가 올바르면 연결 단추를 클릭한 후 기술 자료의 검색 테스트로 자동으로 이동합니다.', + front: '외부 기술 자료에 연결하려면 먼저 외부 API 를 만들어야 합니다. 주의 깊게 읽고 참조하십시오.', }, learnMore: '더 알아보세요', title: '외부 기술 자료에 연결하는 방법', }, connectHelper: { - helper1: 'API 및 기술 자료 ID를 통해 외부 기술 자료에 연결합니다. 현재,', + helper1: 'API 및 기술 자료 ID 를 통해 외부 기술 자료에 연결합니다. 현재,', helper4: '도움말 문서 읽기', helper2: '검색 기능만 지원됩니다', helper5: '이 기능을 사용하기 전에 주의하십시오.', @@ -134,19 +134,19 @@ const translation = { externalTag: '외부', editExternalAPIFormTitle: '외부 지식 API 편집', externalKnowledgeNamePlaceholder: '기술 자료의 이름을 입력하십시오.', - externalAPIPanelDocumentation: '외부 지식 API를 만드는 방법 알아보기', + externalAPIPanelDocumentation: '외부 지식 API 를 만드는 방법 알아보기', createNewExternalAPI: '새 외부 지식 API 만들기', mixtureInternalAndExternalTip: '리랭크 모델은 내부 및 외부 지식의 혼합에 필요합니다.', connectDataset: '외부 기술 자료에 연결', learnHowToWriteGoodKnowledgeDescription: '적절한 지식 설명을 작성하는 방법 알아보기', - externalKnowledgeDescriptionPlaceholder: '이 기술 자료의 내용 설명(선택 사항)', + externalKnowledgeDescriptionPlaceholder: '이 기술 자료의 내용 설명 (선택 사항)', externalKnowledgeId: '외부 지식 ID', - externalKnowledgeIdPlaceholder: '지식 ID를 입력하십시오.', + externalKnowledgeIdPlaceholder: '지식 ID 를 입력하십시오.', allExternalTip: '외부 지식만 사용하는 경우 사용자는 리랭크 모델을 사용할지 여부를 선택할 수 있습니다. 활성화하지 않으면 검색된 청크가 점수에 따라 정렬됩니다. 서로 다른 기술 자료의 검색 전략이 일관되지 않으면 부정확합니다.', - externalAPIPanelDescription: '외부 지식 API는 Dify 외부의 기술 자료에 연결하고 해당 기술 자료에서 지식을 검색하는 데 사용됩니다.', - noExternalKnowledge: '아직 외부 지식 API가 없으므로 여기를 클릭하여 생성하십시오.', + externalAPIPanelDescription: '외부 지식 API 는 Dify 외부의 기술 자료에 연결하고 해당 기술 자료에서 지식을 검색하는 데 사용됩니다.', + noExternalKnowledge: '아직 외부 지식 API 가 없으므로 여기를 클릭하여 생성하십시오.', chunkingMode: { - parentChild: '부모-자식', + parentChild: '부모 - 자식', general: '일반', }, parentMode: { @@ -164,7 +164,7 @@ const translation = { localDocs: '로컬 문서', preprocessDocument: '{{숫자}} 문서 전처리', enable: '사용', - documentsDisabled: '{{num}} 문서 사용 안 함 - 30일 이상 비활성 상태', + documentsDisabled: '{{num}} 문서 사용 안 함 - 30 일 이상 비활성 상태', allKnowledge: '모든 지식', allKnowledgeDescription: '이 작업 영역의 모든 정보를 표시하려면 선택합니다. 워크스페이스 소유자만 모든 기술 자료를 관리할 수 있습니다.', metadata: { diff --git a/web/i18n/ko-KR/education.ts b/web/i18n/ko-KR/education.ts index 78e4be7052..eba00b0f9f 100644 --- a/web/i18n/ko-KR/education.ts +++ b/web/i18n/ko-KR/education.ts @@ -26,15 +26,15 @@ const translation = { privacyPolicy: '개인정보 보호정책', }, option: { - inSchool: '나는 제공된 기관에 재학 중이거나 고용되어 있음을 확인합니다. Dify는 재학증명서나 고용증명서를 요청할 수 있습니다. 만약 내가 자격을 허위로 진술하면, 나는 내 교육 상태에 따라 처음 면제된 수수료를 지불하기로 동의합니다.', - age: '나는 최소한 18세 이상임을 확인합니다.', + inSchool: '나는 제공된 기관에 재학 중이거나 고용되어 있음을 확인합니다. Dify 는 재학증명서나 고용증명서를 요청할 수 있습니다. 만약 내가 자격을 허위로 진술하면, 나는 내 교육 상태에 따라 처음 면제된 수수료를 지불하기로 동의합니다.', + age: '나는 최소한 18 세 이상임을 확인합니다.', }, title: '약관 및 동의사항', }, }, submit: '제출', - rejectContent: '안타깝게도, 귀하는 교육 인증 상태에 적합하지 않으므로 이 이메일 주소를 사용할 경우 Dify Professional Plan의 독점 100% 쿠폰을 받을 수 없습니다.', - successContent: '귀하의 계정에 Dify Professional 플랜을 위한 100% 할인 쿠폰을 발급했습니다. 이 쿠폰은 1년간 유효하므로 유효 기간 내에 사용해 주시기 바랍니다.', + rejectContent: '안타깝게도, 귀하는 교육 인증 상태에 적합하지 않으므로 이 이메일 주소를 사용할 경우 Dify Professional Plan 의 독점 100% 쿠폰을 받을 수 없습니다.', + successContent: '귀하의 계정에 Dify Professional 플랜을 위한 100% 할인 쿠폰을 발급했습니다. 이 쿠폰은 1 년간 유효하므로 유효 기간 내에 사용해 주시기 바랍니다.', currentSigned: '현재 로그인 중입니다', toVerified: '교육 인증 받기', rejectTitle: '귀하의 Dify 교육 인증이 거부되었습니다.', diff --git a/web/i18n/ko-KR/explore.ts b/web/i18n/ko-KR/explore.ts index 5ae1a34643..bc6438af2b 100644 --- a/web/i18n/ko-KR/explore.ts +++ b/web/i18n/ko-KR/explore.ts @@ -16,7 +16,7 @@ const translation = { }, }, apps: { - title: 'Dify로 앱 탐색', + title: 'Dify 로 앱 탐색', description: '이 템플릿 앱을 즉시 사용하거나 템플릿을 기반으로 고유한 앱을 사용자 정의하세요.', allCategories: '모든 카테고리', }, diff --git a/web/i18n/ko-KR/login.ts b/web/i18n/ko-KR/login.ts index 4fbd5f5522..da044554bc 100644 --- a/web/i18n/ko-KR/login.ts +++ b/web/i18n/ko-KR/login.ts @@ -1,6 +1,6 @@ const translation = { pageTitle: '시작하기 🎉', - welcome: 'Dify에 오신 것을 환영합니다. 계속하려면 로그인하세요.', + welcome: 'Dify 에 오신 것을 환영합니다. 계속하려면 로그인하세요.', email: '이메일 주소', emailPlaceholder: '이메일 주소를 입력하세요', password: '비밀번호', @@ -19,13 +19,13 @@ const translation = { invitationCodePlaceholder: '초대 코드를 입력하세요', interfaceLanguage: '인터페이스 언어', timezone: '시간대', - go: 'Dify로 이동', + go: 'Dify 로 이동', sendUsMail: '간단한 소개를 메일로 보내주시면 초대 요청을 처리해드립니다.', acceptPP: '개인정보 처리 방침에 동의합니다.', reset: '비밀번호를 재설정하려면 다음 명령을 실행하세요:', - withGitHub: 'GitHub로 계속', - withGoogle: 'Google로 계속', - rightTitle: 'LLM의 최대 잠재력을 발휘하세요', + withGitHub: 'GitHub 로 계속', + withGoogle: 'Google 로 계속', + rightTitle: 'LLM 의 최대 잠재력을 발휘하세요', rightDesc: '매력적이고 조작 가능하며 개선 가능한 AI 애플리케이션을 쉽게 구축하세요.', tos: '이용약관', pp: '개인정보 처리 방침', @@ -52,33 +52,33 @@ const translation = { emailInValid: '유효한 이메일 주소를 입력하세요.', nameEmpty: '사용자 이름을 입력하세요.', passwordEmpty: '비밀번호를 입력하세요.', - passwordInvalid: '비밀번호는 문자와 숫자를 포함하고 8자 이상이어야 합니다.', - passwordLengthInValid: '비밀번호는 8자 이상이어야 합니다.', + passwordInvalid: '비밀번호는 문자와 숫자를 포함하고 8 자 이상이어야 합니다.', + passwordLengthInValid: '비밀번호는 8 자 이상이어야 합니다.', registrationNotAllowed: '계정을 찾을 수 없습니다. 등록하려면 시스템 관리자에게 문의하십시오.', }, license: { - tip: 'Dify Community Edition을 시작하기 전에 GitHub의', + tip: 'Dify Community Edition 을 시작하기 전에 GitHub 의', link: '오픈 소스 라이선스', }, join: '가입하기', joinTipStart: '당신을 초대합니다.', joinTipEnd: '팀에 가입하세요.', invalid: '링크의 유효 기간이 만료되었습니다.', - explore: 'Dify를 탐색하세요', + explore: 'Dify 를 탐색하세요', activatedTipStart: '이제', activatedTipEnd: '팀에 가입되었습니다.', activated: '지금 로그인하세요', adminInitPassword: '관리자 초기화 비밀번호', validate: '확인', - sso: 'SSO로 계속하기', + sso: 'SSO 로 계속하기', checkCode: { verify: '확인', verificationCode: '인증 코드', tips: '<strong>{{email}}</strong>로 인증 코드를 보내드립니다.', - validTime: '코드는 5분 동안 유효합니다', + validTime: '코드는 5 분 동안 유효합니다', checkYourEmail: '이메일 주소 확인', invalidCode: '유효하지 않은 코드', - verificationCodePlaceholder: '6자리 코드 입력', + verificationCodePlaceholder: '6 자리 코드 입력', emptyCode: '코드가 필요합니다.', useAnotherMethod: '다른 방법 사용', didNotReceiveCode: '코드를 받지 못하셨나요?', @@ -89,7 +89,7 @@ const translation = { useVerificationCode: '인증 코드 사용', continueWithCode: '코드로 계속하기', usePassword: '비밀번호 사용', - withSSO: 'SSO로 계속하기', + withSSO: 'SSO 로 계속하기', backToLogin: '로그인으로 돌아가기', resetPassword: '비밀번호 재설정', setYourAccount: '계정 설정', @@ -98,13 +98,13 @@ const translation = { changePasswordBtn: '비밀번호 설정', enterYourName: '사용자 이름을 입력해 주세요', noLoginMethodTip: '인증 방법을 추가하려면 시스템 관리자에게 문의하십시오.', - resetPasswordDesc: 'Dify에 가입할 때 사용한 이메일을 입력하면 비밀번호 재설정 이메일을 보내드립니다.', - licenseInactiveTip: '작업 영역에 대한 Dify Enterprise 라이선스가 비활성 상태입니다. Dify를 계속 사용하려면 관리자에게 문의하십시오.', + resetPasswordDesc: 'Dify 에 가입할 때 사용한 이메일을 입력하면 비밀번호 재설정 이메일을 보내드립니다.', + licenseInactiveTip: '작업 영역에 대한 Dify Enterprise 라이선스가 비활성 상태입니다. Dify 를 계속 사용하려면 관리자에게 문의하십시오.', licenseLost: '라이센스 분실', - licenseLostTip: 'Dify 라이선스 서버에 연결하지 못했습니다. Dify를 계속 사용하려면 관리자에게 문의하십시오.', + licenseLostTip: 'Dify 라이선스 서버에 연결하지 못했습니다. Dify 를 계속 사용하려면 관리자에게 문의하십시오.', licenseInactive: 'License Inactive(라이선스 비활성)', licenseExpired: '라이센스가 만료되었습니다.', - licenseExpiredTip: '작업 영역에 대한 Dify Enterprise 라이선스가 만료되었습니다. Dify를 계속 사용하려면 관리자에게 문의하십시오.', + licenseExpiredTip: '작업 영역에 대한 Dify Enterprise 라이선스가 만료되었습니다. Dify 를 계속 사용하려면 관리자에게 문의하십시오.', webapp: { noLoginMethod: '웹 애플리케이션에 대한 인증 방법이 구성되어 있지 않습니다.', disabled: '웹앱 인증이 비활성화되었습니다. 이를 활성화하려면 시스템 관리자에게 문의하십시오. 앱을 직접 사용해 볼 수 있습니다.', diff --git a/web/i18n/ko-KR/plugin.ts b/web/i18n/ko-KR/plugin.ts index 8d823136d0..9fae9a71ac 100644 --- a/web/i18n/ko-KR/plugin.ts +++ b/web/i18n/ko-KR/plugin.ts @@ -18,7 +18,7 @@ const translation = { source: { marketplace: '마켓플레이스에서 설치', local: '로컬 패키지 파일에서 설치', - github: 'GitHub에서 설치', + github: 'GitHub 에서 설치', }, noInstalled: '설치된 플러그인이 없습니다.', notFound: '플러그인을 찾을 수 없습니다.', @@ -32,7 +32,7 @@ const translation = { categoryTip: { marketplace: '마켓플레이스에서 설치됨', debugging: '디버깅 플러그인', - github: 'Github에서 설치됨', + github: 'Github 에서 설치됨', local: '로컬 플러그인', }, operation: { @@ -56,7 +56,7 @@ const translation = { settings: '사용자 설정', unsupportedContent2: '버전을 전환하려면 클릭합니다.', uninstalledTitle: '도구가 설치되지 않음', - descriptionPlaceholder: '도구의 용도에 대한 간략한 설명(예: 특정 위치의 온도 가져오기).', + descriptionPlaceholder: '도구의 용도에 대한 간략한 설명 (예: 특정 위치의 온도 가져오기).', title: '추가 도구', toolLabel: '도구', placeholder: '도구 선택...', @@ -141,9 +141,9 @@ const translation = { installFromGitHub: { uploadFailed: '업로드 실패', selectVersionPlaceholder: '버전을 선택하세요.', - installPlugin: 'GitHub에서 플러그인 설치', + installPlugin: 'GitHub 에서 플러그인 설치', installFailed: '설치 실패', - updatePlugin: 'GitHub에서 플러그인 업데이트', + updatePlugin: 'GitHub 에서 플러그인 업데이트', selectPackage: '패키지 선택', gitHubRepo: 'GitHub 리포지토리', selectPackagePlaceholder: '패키지를 선택하세요.', @@ -161,9 +161,9 @@ const translation = { title: '플러그인 설치', }, error: { - noReleasesFound: '릴리스를 찾을 수 없습니다. GitHub 리포지토리 또는 입력 URL을 확인하세요.', + noReleasesFound: '릴리스를 찾을 수 없습니다. GitHub 리포지토리 또는 입력 URL 을 확인하세요.', fetchReleasesError: '릴리스를 검색할 수 없습니다. 나중에 다시 시도하십시오.', - inValidGitHubUrl: '잘못된 GitHub URL입니다. 유효한 URL을 https://github.com/owner/repo 형식으로 입력하십시오.', + inValidGitHubUrl: '잘못된 GitHub URL 입니다. 유효한 URL 을 https://github.com/owner/repo 형식으로 입력하십시오.', }, marketplace: { sortOption: { @@ -178,10 +178,10 @@ const translation = { difyMarketplace: 'Dify 마켓플레이스', pluginsResult: '{{num}} 결과', discover: '발견하다', - moreFrom: 'Marketplace에서 더 보기', + moreFrom: 'Marketplace 에서 더 보기', sortBy: '정렬', and: '그리고', - verifiedTip: 'Dify에 의해 확인됨', + verifiedTip: 'Dify 에 의해 확인됨', partnerTip: 'Dify 파트너에 의해 확인됨', }, task: { @@ -199,14 +199,14 @@ const translation = { installFrom: '에서 설치', allCategories: '모든 카테고리', submitPlugin: '플러그인 제출', - findMoreInMarketplace: 'Marketplace에서 더 알아보기', + findMoreInMarketplace: 'Marketplace 에서 더 알아보기', searchCategories: '검색 카테고리', search: '검색', - searchInMarketplace: 'Marketplace에서 검색', + searchInMarketplace: 'Marketplace 에서 검색', from: '보낸 사람', searchPlugins: '검색 플러그인', install: '{{num}} 설치', - fromMarketplace: 'Marketplace에서', + fromMarketplace: 'Marketplace 에서', metadata: { title: '플러그인', }, diff --git a/web/i18n/ko-KR/share-app.ts b/web/i18n/ko-KR/share-app.ts index 8474bc554e..3958b4f93e 100644 --- a/web/i18n/ko-KR/share-app.ts +++ b/web/i18n/ko-KR/share-app.ts @@ -51,7 +51,7 @@ const translation = { run: '실행', copy: '복사', resultTitle: 'AI 완성', - noData: 'AI가 필요한 내용을 제공할 것입니다.', + noData: 'AI 가 필요한 내용을 제공할 것입니다.', csvUploadTitle: 'CSV 파일을 여기로 끌어다 놓거나', browse: '찾아보기', csvStructureTitle: 'CSV 파일은 다음 구조를 따라야 합니다:', @@ -65,7 +65,7 @@ const translation = { errorMsg: { empty: '업로드된 파일에 컨텐츠를 입력해주세요.', fileStructNotMatch: '업로드된 CSV 파일이 구조와 일치하지 않습니다.', - emptyLine: '줄 {{rowIndex}}이(가) 비어 있습니다.', + emptyLine: '줄 {{rowIndex}}이 (가) 비어 있습니다.', invalidLine: '줄 {{rowIndex}}: {{varName}}의 값은 비워둘 수 없습니다.', moreThanMaxLengthLine: '줄 {{rowIndex}}: {{varName}}의 값은 {{maxLength}}자를 초과할 수 없습니다.', atLeastOne: '업로드된 파일에는 적어도 한 줄의 입력이 필요합니다.', @@ -73,6 +73,9 @@ const translation = { execution: '실행', executions: '{{num}} 처형', }, + login: { + backToHome: '홈으로 돌아가기', + }, } export default translation diff --git a/web/i18n/ko-KR/time.ts b/web/i18n/ko-KR/time.ts index 78e825d1ba..172bb78bd6 100644 --- a/web/i18n/ko-KR/time.ts +++ b/web/i18n/ko-KR/time.ts @@ -9,18 +9,18 @@ const translation = { Mon: '몬', }, months: { - May: '5월', - January: '1월', - August: '8월', - July: '7월', - April: '4월', - October: '10월', - December: '12월', - February: '2월', - June: '6월', - November: '11월', - March: '3월', - September: '9월', + May: '5 월', + January: '1 월', + August: '8 월', + July: '7 월', + April: '4 월', + October: '10 월', + December: '12 월', + February: '2 월', + June: '6 월', + November: '11 월', + March: '3 월', + September: '9 월', }, operation: { pickDate: '날짜 선택', diff --git a/web/i18n/ko-KR/tools.ts b/web/i18n/ko-KR/tools.ts index b526de6c06..45c63b5f80 100644 --- a/web/i18n/ko-KR/tools.ts +++ b/web/i18n/ko-KR/tools.ts @@ -9,7 +9,7 @@ const translation = { workflow: '워크플로우', }, contribute: { - line1: '저는 Dify에', + line1: '저는 Dify 에', line2: '도구를 기여하는데 관심이 있습니다.', viewGuide: '가이드 보기', }, @@ -47,9 +47,9 @@ const translation = { schema: '스키마', schemaPlaceHolder: '여기에 OpenAPI 스키마를 입력하세요', viewSchemaSpec: 'OpenAPI-Swagger 명세 보기', - importFromUrl: 'URL에서 가져오기', + importFromUrl: 'URL 에서 가져오기', importFromUrlPlaceHolder: 'https://...', - urlError: '유효한 URL을 입력하세요', + urlError: '유효한 URL 을 입력하세요', examples: '예시', exampleOptions: { json: '날씨 (JSON)', @@ -96,7 +96,7 @@ const translation = { methodSetting: '설정', methodSettingTip: '도구 설정에서 사용자가 기입', methodParameter: '파라미터', - methodParameterTip: '추론 중에 LLM이 기입', + methodParameterTip: '추론 중에 LLM 이 기입', label: '태그', labelPlaceholder: '태그를 선택하세요.(선택사항)', description: '설명', diff --git a/web/i18n/ko-KR/workflow.ts b/web/i18n/ko-KR/workflow.ts index 3cf22dfe13..a1c08eef4d 100644 --- a/web/i18n/ko-KR/workflow.ts +++ b/web/i18n/ko-KR/workflow.ts @@ -101,19 +101,19 @@ const translation = { ImageUploadLegacyTip: '이제 시작 양식에서 파일 형식 변수를 만들 수 있습니다. 앞으로 이미지 업로드 기능은 더 이상 지원되지 않습니다.', importWarning: '주의', importWarningDetails: 'DSL 버전 차이는 특정 기능에 영향을 미칠 수 있습니다.', - openInExplore: 'Explore에서 열기', + openInExplore: 'Explore 에서 열기', onFailure: '실패 시', addFailureBranch: '실패 분기 추가', noHistory: '이력 없음', loadMore: '더 많은 워크플로우 로드', publishUpdate: '업데이트 게시', - exportJPEG: 'JPEG로 내보내기', + exportJPEG: 'JPEG 로 내보내기', exitVersions: '종료 버전', exportImage: '이미지 내보내기', noExist: '해당 변수가 없습니다.', - exportSVG: 'SVG로 내보내기', + exportSVG: 'SVG 로 내보내기', versionHistory: '버전 기록', - exportPNG: 'PNG로 내보내기', + exportPNG: 'PNG 로 내보내기', referenceVar: '참조 변수', }, env: { @@ -139,7 +139,7 @@ const translation = { }, chatVariable: { panelTitle: '대화 변수', - panelDescription: '대화 변수는 LLM이 기억해야 할 대화 기록, 업로드된 파일, 사용자 선호도 등의 상호작용 정보를 저장하는 데 사용됩니다. 이들은 읽기 및 쓰기가 가능합니다.', + panelDescription: '대화 변수는 LLM 이 기억해야 할 대화 기록, 업로드된 파일, 사용자 선호도 등의 상호작용 정보를 저장하는 데 사용됩니다. 이들은 읽기 및 쓰기가 가능합니다.', docLink: '자세한 내용은 문서를 참조하세요.', button: '변수 추가', modal: { @@ -152,7 +152,7 @@ const translation = { valuePlaceholder: '기본값, 설정하지 않으려면 비워두세요', description: '설명', descriptionPlaceholder: '변수에 대해 설명하세요', - editInJSON: 'JSON으로 편집', + editInJSON: 'JSON 으로 편집', oneByOne: '하나씩 추가', editInForm: '양식에서 편집', arrayValue: '값', @@ -193,7 +193,7 @@ const translation = { errorMsg: { fieldRequired: '{{field}}가 필요합니다', authRequired: '인증이 필요합니다', - invalidJson: '{{field}}는 잘못된 JSON입니다', + invalidJson: '{{field}}는 잘못된 JSON 입니다', fields: { variable: '변수 이름', variableValue: '변수 값', @@ -203,9 +203,9 @@ const translation = { visionVariable: '비전 변수', }, invalidVariable: '잘못된 변수', - rerankModelRequired: 'Rerank Model을 켜기 전에 설정에서 모델이 성공적으로 구성되었는지 확인하십시오.', + rerankModelRequired: 'Rerank Model 을 켜기 전에 설정에서 모델이 성공적으로 구성되었는지 확인하십시오.', noValidTool: '{{field}} 유효한 도구가 선택되지 않았습니다.', - toolParameterRequired: '{{field}}: 매개변수 [{{param}}]이 필요합니다.', + toolParameterRequired: '{{field}}: 매개변수 [{{param}}] 이 필요합니다.', }, singleRun: { testRun: '테스트 실행', @@ -263,17 +263,17 @@ const translation = { 'answer': '대화의 답변 내용을 정의합니다', 'llm': '질문에 답하거나 자연어를 처리하기 위해 대형 언어 모델을 호출합니다', 'knowledge-retrieval': '사용자 질문과 관련된 텍스트 콘텐츠를 지식 베이스에서 쿼리할 수 있습니다', - 'question-classifier': '사용자 질문의 분류 조건을 정의합니다. LLM은 분류 설명을 기반으로 대화의 진행 방식을 정의할 수 있습니다', + 'question-classifier': '사용자 질문의 분류 조건을 정의합니다. LLM 은 분류 설명을 기반으로 대화의 진행 방식을 정의할 수 있습니다', 'if-else': 'if/else 조건을 기반으로 워크플로우를 두 가지 분기로 나눌 수 있습니다', 'code': '사용자 정의 논리를 구현하기 위해 Python 또는 NodeJS 코드를 실행합니다', 'template-transform': 'Jinja 템플릿 구문을 사용하여 데이터를 문자열로 변환합니다', 'http-request': 'HTTP 프로토콜을 통해 서버 요청을 보낼 수 있습니다', 'variable-assigner': '다중 분기 변수들을 하나의 변수로 집계하여 다운스트림 노드의 통합 구성을 가능하게 합니다.', - 'assigner': '변수 할당 노드는 쓰기 가능한 변수(대화 변수 등)에 값을 할당하는 데 사용됩니다.', + 'assigner': '변수 할당 노드는 쓰기 가능한 변수 (대화 변수 등) 에 값을 할당하는 데 사용됩니다.', 'variable-aggregator': '다중 분기 변수들을 하나의 변수로 집계하여 다운스트림 노드의 통합 구성을 가능하게 합니다.', 'iteration': '목록 객체에서 여러 단계를 수행하여 모든 결과가 출력될 때까지 반복합니다.', - 'parameter-extractor': '도구 호출 또는 HTTP 요청을 위해 자연어에서 구조화된 매개변수를 추출하기 위해 LLM을 사용합니다.', - 'document-extractor': '업로드된 문서를 LLM에서 쉽게 이해할 수 있는 텍스트 콘텐츠로 구문 분석하는 데 사용됩니다.', + 'parameter-extractor': '도구 호출 또는 HTTP 요청을 위해 자연어에서 구조화된 매개변수를 추출하기 위해 LLM 을 사용합니다.', + 'document-extractor': '업로드된 문서를 LLM 에서 쉽게 이해할 수 있는 텍스트 콘텐츠로 구문 분석하는 데 사용됩니다.', 'list-operator': '배열 내용을 필터링하거나 정렬하는 데 사용됩니다.', 'agent': '질문에 답하거나 자연어를 처리하기 위해 대규모 언어 모델을 호출하는 경우', 'loop': '종료 조건이 충족되거나 최대 반복 횟수에 도달할 때까지 논리 루프를 실행합니다.', @@ -282,8 +282,8 @@ const translation = { operator: { zoomIn: '확대', zoomOut: '축소', - zoomTo50: '50%로 확대', - zoomTo100: '100%로 확대', + zoomTo50: '50% 로 확대', + zoomTo100: '100% 로 확대', zoomToFit: '화면에 맞게 확대', }, panel: { @@ -336,9 +336,9 @@ const translation = { failBranch: { title: '실패 분기', desc: '오류가 발생하면 예외 분기를 실행합니다', - customize: '캔버스로 이동하여 fail branch logic를 사용자 지정합니다.', + customize: '캔버스로 이동하여 fail branch logic 를 사용자 지정합니다.', inLog: '노드 예외는 실패 분기를 자동으로 실행합니다. 노드 출력은 오류 유형 및 오류 메시지를 반환하고 다운스트림으로 전달합니다.', - customizeTip: 'fail 분기가 활성화되면 노드에서 throw된 예외가 프로세스를 종료하지 않습니다. 대신 미리 정의된 실패 분기를 자동으로 실행하여 오류 메시지, 보고서, 수정 사항을 유연하게 제공하거나 작업을 건너뛸 수 있습니다.', + customizeTip: 'fail 분기가 활성화되면 노드에서 throw 된 예외가 프로세스를 종료하지 않습니다. 대신 미리 정의된 실패 분기를 자동으로 실행하여 오류 메시지, 보고서, 수정 사항을 유연하게 제공하거나 작업을 건너뛸 수 있습니다.', }, partialSucceeded: { tip: '프로세스에 {{num}} 노드가 비정상적으로 실행 중입니다. 추적으로 이동하여 로그를 확인하십시오.', @@ -397,7 +397,7 @@ const translation = { variables: '변수', context: '컨텍스트', contextTooltip: '컨텍스트로 지식을 가져올 수 있습니다', - notSetContextInPromptTip: '컨텍스트 기능을 활성화하려면 PROMPT에 컨텍스트 변수를 입력하세요.', + notSetContextInPromptTip: '컨텍스트 기능을 활성화하려면 PROMPT 에 컨텍스트 변수를 입력하세요.', prompt: '프롬프트', roleDescription: { system: '대화를 위한 고급 지침 제공', @@ -419,7 +419,7 @@ const translation = { singleRun: { variable: '변수', }, - sysQueryInUser: '사용자 메시지에 sys.query가 필요합니다', + sysQueryInUser: '사용자 메시지에 sys.query 가 필요합니다', jsonSchema: { warningTips: { saveSchema: '현재 필드의 편집을 완료한 후 스키마를 저장하세요.', @@ -446,7 +446,7 @@ const translation = { regenerate: '재생하다', required: '필수', doc: '구조화된 출력에 대해 더 알아보세요.', - import: 'JSON에서 가져오기', + import: 'JSON 에서 가져오기', }, }, knowledgeRetrieval: { @@ -491,12 +491,12 @@ const translation = { http: { inputVars: '입력 변수', api: 'API', - apiPlaceholder: 'URL을 입력하세요, 변수를 삽입하려면 ‘/’를 입력하세요', - notStartWithHttp: 'API는 http:// 또는 https://로 시작해야 합니다', + apiPlaceholder: 'URL 을 입력하세요, 변수를 삽입하려면‘/’를 입력하세요', + notStartWithHttp: 'API 는 http:// 또는 https://로 시작해야 합니다', key: '키', value: '값', bulkEdit: '일괄 편집', - keyValueEdit: '키-값 편집', + keyValueEdit: '키 - 값 편집', headers: '헤더', params: '매개변수', body: '본문', @@ -532,7 +532,7 @@ const translation = { binaryFileVariable: '바이너리 파일 변수', extractListPlaceholder: '목록 항목 인덱스 입력, \'/\' 변수 삽입', curl: { - title: 'cURL에서 가져오기', + title: 'cURL 에서 가져오기', placeholder: '여기에 cURL 문자열 붙여 넣기', }, }, @@ -546,7 +546,7 @@ const translation = { templateTransform: { inputVars: '입력 변수', code: '코드', - codeSupportTip: 'Jinja2만 지원합니다', + codeSupportTip: 'Jinja2 만 지원합니다', outputVars: { output: '변환된 내용', }, @@ -568,8 +568,8 @@ const translation = { 'is not': '아니다', 'empty': '비어 있음', 'not empty': '비어 있지 않음', - 'null': 'null임', - 'not null': 'null이 아님', + 'null': 'null 임', + 'not null': 'null 이 아님', 'regex match': '정규식 일치', 'in': '안으로', 'exists': '존재', @@ -699,7 +699,7 @@ const translation = { advancedSetting: '고급 설정', reasoningMode: '추론 모드', reasoningModeTip: '모델의 함수 호출 또는 프롬프트에 대한 지시 응답 능력을 기반으로 적절한 추론 모드를 선택할 수 있습니다.', - isSuccess: '성공 여부. 성공 시 값은 1이고, 실패 시 값은 0입니다.', + isSuccess: '성공 여부. 성공 시 값은 1 이고, 실패 시 값은 0 입니다.', errorReason: '오류 원인', }, iteration: { @@ -804,7 +804,7 @@ const translation = { files: { url: '이미지 URL', upload_file_id: '파일 ID 업로드', - transfer_method: '전송 방법. 값이 remote_url 또는 local_file입니다.', + transfer_method: '전송 방법. 값이 remote_url 또는 local_file 입니다.', type: '지원 유형. 이제 이미지만 지원합니다.', title: '에이전트 생성 파일', }, @@ -826,8 +826,8 @@ const translation = { toolNotAuthorizedTooltip: '{{도구}} 권한이 부여되지 않음', strategyNotFoundDesc: '설치된 플러그인 버전은 이 전략을 제공하지 않습니다.', maxIterations: '최대 반복 횟수', - pluginNotFoundDesc: '이 플러그인은 GitHub에서 설치됩니다. 플러그인으로 이동하여 다시 설치하십시오.', - pluginNotInstalledDesc: '이 플러그인은 GitHub에서 설치됩니다. 플러그인으로 이동하여 다시 설치하십시오.', + pluginNotFoundDesc: '이 플러그인은 GitHub 에서 설치됩니다. 플러그인으로 이동하여 다시 설치하십시오.', + pluginNotInstalledDesc: '이 플러그인은 GitHub 에서 설치됩니다. 플러그인으로 이동하여 다시 설치하십시오.', strategyNotInstallTooltip: '{{strategy}}가 설치되지 않았습니다.', tools: '도구', unsupportedStrategy: '지원되지 않는 전략', @@ -868,7 +868,7 @@ const translation = { initialLoopVariables: '초기 루프 변수', breakConditionTip: '종료 조건과 대화 변수가 있는 루프 내에서만 변수를 참조할 수 있습니다.', currentLoopCount: '현재 루프 카운트: {{count}}', - loopMaxCountError: '유효한 최대 루프 수를 입력하십시오. 범위는 1에서 {{maxCount}}입니다.', + loopMaxCountError: '유효한 최대 루프 수를 입력하십시오. 범위는 1 에서 {{maxCount}}입니다.', totalLoopCount: '총 루프 횟수: {{count}}', variableName: '변수 이름', loopNode: '루프 노드', diff --git a/web/i18n/language.ts b/web/i18n/language.ts index 87027a7951..a31f9e9c4b 100644 --- a/web/i18n/language.ts +++ b/web/i18n/language.ts @@ -92,7 +92,7 @@ export const NOTICE_I18N = { ja_JP: 'Our system will be unavailable from 19:00 to 24:00 UTC on August 28 for an upgrade. For questions, kindly contact our support team (support@dify.ai). We value your patience.', ko_KR: - '시스템이 업그레이드를 위해 UTC 시간대로 8월 28일 19:00 ~ 24:00에 사용 불가될 예정입니다. 질문이 있으시면 지원 팀에 연락주세요 (support@dify.ai). 최선을 다해 답변해드리겠습니다.', + '시스템이 업그레이드를 위해 UTC 시간대로 8 월 28 일 19:00 ~ 24:00 에 사용 불가될 예정입니다. 질문이 있으시면 지원 팀에 연락주세요 (support@dify.ai). 최선을 다해 답변해드리겠습니다.', pl_PL: 'Nasz system będzie niedostępny od 19:00 do 24:00 UTC 28 sierpnia w celu aktualizacji. W przypadku pytań prosimy o kontakt z naszym zespołem wsparcia (support@dify.ai). Doceniamy Twoją cierpliwość.', uk_UA: diff --git a/web/i18n/pl-PL/app-debug.ts b/web/i18n/pl-PL/app-debug.ts index cf7232e563..48b44c0cbb 100644 --- a/web/i18n/pl-PL/app-debug.ts +++ b/web/i18n/pl-PL/app-debug.ts @@ -309,6 +309,7 @@ const translation = { 'labelName': 'Nazwa etykiety', 'inputPlaceholder': 'Proszę wpisać', 'required': 'Wymagane', + 'hide': 'Ukryj', 'errorMsg': { varNameRequired: 'Wymagana nazwa zmiennej', labelNameRequired: 'Wymagana nazwa etykiety', diff --git a/web/i18n/pl-PL/app.ts b/web/i18n/pl-PL/app.ts index 54759154ca..856a64c868 100644 --- a/web/i18n/pl-PL/app.ts +++ b/web/i18n/pl-PL/app.ts @@ -220,12 +220,14 @@ const translation = { anyone: 'Każdy może uzyskać dostęp do aplikacji webowej', specific: 'Tylko określone grupy lub członkowie mogą uzyskać dostęp do aplikacji internetowej', organization: 'Każdy w organizacji ma dostęp do aplikacji internetowej.', + external: 'Tylko uwierzytelnieni zewnętrzni użytkownicy mogą uzyskać dostęp do aplikacji internetowej.', }, accessControlDialog: { accessItems: { anyone: 'Każdy z linkiem', specific: 'Specyficzne grupy lub członkowie', organization: 'Tylko członkowie w obrębie przedsiębiorstwa', + external: 'Uwierzytelnieni użytkownicy zewnętrzni', }, operateGroupAndMember: { searchPlaceholder: 'Szukaj grup i członków', diff --git a/web/i18n/pl-PL/share-app.ts b/web/i18n/pl-PL/share-app.ts index 80619cf4fc..617f66d994 100644 --- a/web/i18n/pl-PL/share-app.ts +++ b/web/i18n/pl-PL/share-app.ts @@ -78,6 +78,9 @@ const translation = { executions: '{{num}} EGZEKUCJI', execution: 'WYKONANIE', }, + login: { + backToHome: 'Powrót do strony głównej', + }, } export default translation diff --git a/web/i18n/pt-BR/app-debug.ts b/web/i18n/pt-BR/app-debug.ts index df4312f887..64f7a85fe7 100644 --- a/web/i18n/pt-BR/app-debug.ts +++ b/web/i18n/pt-BR/app-debug.ts @@ -285,6 +285,7 @@ const translation = { 'labelName': 'Nome do Rótulo', 'inputPlaceholder': 'Por favor, insira', 'required': 'Obrigatório', + 'hide': 'Ocultar', 'errorMsg': { varNameRequired: 'O nome da variável é obrigatório', labelNameRequired: 'O nome do rótulo é obrigatório', diff --git a/web/i18n/pt-BR/app.ts b/web/i18n/pt-BR/app.ts index 5dd1753cac..766053456a 100644 --- a/web/i18n/pt-BR/app.ts +++ b/web/i18n/pt-BR/app.ts @@ -213,12 +213,14 @@ const translation = { anyone: 'Qualquer pessoa pode acessar o aplicativo web', specific: 'Apenas grupos ou membros específicos podem acessar o aplicativo web', organization: 'Qualquer pessoa na organização pode acessar o aplicativo web', + external: 'Apenas usuários externos autenticados podem acessar o aplicativo Web.', }, accessControlDialog: { accessItems: { anyone: 'Qualquer pessoa com o link', specific: 'Grupos específicos ou membros', organization: 'Apenas membros dentro da empresa', + external: 'Usuários externos autenticados', }, operateGroupAndMember: { searchPlaceholder: 'Pesquisar grupos e membros', diff --git a/web/i18n/pt-BR/share-app.ts b/web/i18n/pt-BR/share-app.ts index d8bca03089..9a9d7db632 100644 --- a/web/i18n/pt-BR/share-app.ts +++ b/web/i18n/pt-BR/share-app.ts @@ -77,6 +77,9 @@ const translation = { executions: '{{num}} EXECUÇÕES', execution: 'EXECUÇÃO', }, + login: { + backToHome: 'Voltar para a página inicial', + }, } export default translation diff --git a/web/i18n/ro-RO/app-debug.ts b/web/i18n/ro-RO/app-debug.ts index bafeee8bb0..f7240055e3 100644 --- a/web/i18n/ro-RO/app-debug.ts +++ b/web/i18n/ro-RO/app-debug.ts @@ -285,6 +285,7 @@ const translation = { 'labelName': 'Nume etichetă', 'inputPlaceholder': 'Vă rugăm să introduceți', 'required': 'Obligatoriu', + 'hide': 'Ascundeți', 'errorMsg': { varNameRequired: 'Numele variabilei este obligatoriu', labelNameRequired: 'Numele etichetei este obligatoriu', diff --git a/web/i18n/ro-RO/app.ts b/web/i18n/ro-RO/app.ts index adf82aa38e..cd267e7b66 100644 --- a/web/i18n/ro-RO/app.ts +++ b/web/i18n/ro-RO/app.ts @@ -213,12 +213,14 @@ const translation = { specific: 'Numai grupuri sau membri specifici pot accesa aplicația web.', organization: 'Oricine din organizație poate accesa aplicația web', anyone: 'Oricine poate accesa aplicația web', + external: 'Numai utilizatorii externi autentificați pot accesa aplicația web', }, accessControlDialog: { accessItems: { anyone: 'Oricine are linkul', specific: 'Grupuri sau membri specifici', organization: 'Numai membrii din cadrul întreprinderii', + external: 'Utilizatori extern autentificați', }, operateGroupAndMember: { searchPlaceholder: 'Caută grupuri și membri', diff --git a/web/i18n/ro-RO/share-app.ts b/web/i18n/ro-RO/share-app.ts index 2cb39a0485..41e38812c5 100644 --- a/web/i18n/ro-RO/share-app.ts +++ b/web/i18n/ro-RO/share-app.ts @@ -77,6 +77,9 @@ const translation = { execution: 'EXECUȚIE', executions: '{{num}} EXECUȚII', }, + login: { + backToHome: 'Înapoi la Acasă', + }, } export default translation diff --git a/web/i18n/ru-RU/app-debug.ts b/web/i18n/ru-RU/app-debug.ts index 038165301e..00cd6e8a75 100644 --- a/web/i18n/ru-RU/app-debug.ts +++ b/web/i18n/ru-RU/app-debug.ts @@ -322,6 +322,7 @@ const translation = { 'inputPlaceholder': 'Пожалуйста, введите', 'content': 'Содержимое', 'required': 'Обязательно', + 'hide': 'Скрыть', 'errorMsg': { labelNameRequired: 'Имя метки обязательно', varNameCanBeRepeat: 'Имя переменной не может повторяться', diff --git a/web/i18n/ru-RU/app.ts b/web/i18n/ru-RU/app.ts index fa73e33197..428d4c4e57 100644 --- a/web/i18n/ru-RU/app.ts +++ b/web/i18n/ru-RU/app.ts @@ -213,12 +213,14 @@ const translation = { anyone: 'Любой может получить доступ к веб-приложению', specific: 'Только определенные группы или участники могут получить доступ к веб-приложению.', organization: 'Любой в организации может получить доступ к веб-приложению', + external: 'Только аутентифицированные внешние пользователи могут получить доступ к веб-приложению.', }, accessControlDialog: { accessItems: { anyone: 'Кто угодно с ссылкой', specific: 'Конкретные группы или члены', organization: 'Только члены внутри предприятия', + external: 'Аутентифицированные внешние пользователи', }, operateGroupAndMember: { searchPlaceholder: 'Искать группы и участников', diff --git a/web/i18n/ru-RU/share-app.ts b/web/i18n/ru-RU/share-app.ts index b2850fa276..dafbe9d6b1 100644 --- a/web/i18n/ru-RU/share-app.ts +++ b/web/i18n/ru-RU/share-app.ts @@ -77,6 +77,9 @@ const translation = { execution: 'ИСПОЛНЕНИЕ', executions: '{{num}} ВЫПОЛНЕНИЯ', }, + login: { + backToHome: 'Назад на главную', + }, } export default translation diff --git a/web/i18n/sl-SI/app-debug.ts b/web/i18n/sl-SI/app-debug.ts index 8672fa5c58..7e5c7dd6b0 100644 --- a/web/i18n/sl-SI/app-debug.ts +++ b/web/i18n/sl-SI/app-debug.ts @@ -239,4 +239,4 @@ const translation = { }, } -module.exports = translation +export default translation diff --git a/web/i18n/sl-SI/app.ts b/web/i18n/sl-SI/app.ts index 6241d40f30..4ac445872d 100644 --- a/web/i18n/sl-SI/app.ts +++ b/web/i18n/sl-SI/app.ts @@ -213,12 +213,14 @@ const translation = { anyone: 'Vsakdo lahko dostopa do spletne aplikacije', specific: 'Samo določenim skupinam ali članom je omogočen dostop do spletne aplikacije', organization: 'Vsakdo v organizaciji lahko dostopa do spletne aplikacije', + external: 'Samo avtentificirani zunanji uporabniki lahko dostopajo do spletne aplikacije.', }, accessControlDialog: { accessItems: { anyone: 'Kdorkoli s povezavo', specific: 'Specifične skupine ali člani', organization: 'Samo člani znotraj podjetja', + external: 'Avtorizirani zunanji uporabniki', }, operateGroupAndMember: { searchPlaceholder: 'Išči skupine in člane', diff --git a/web/i18n/sl-SI/share-app.ts b/web/i18n/sl-SI/share-app.ts index 28d62b2336..8b7fe87cbd 100644 --- a/web/i18n/sl-SI/share-app.ts +++ b/web/i18n/sl-SI/share-app.ts @@ -74,6 +74,9 @@ const translation = { execution: 'IZVEDBA', executions: '{{num}} IZVRŠITEV', }, + login: { + backToHome: 'Nazaj na začetno stran', + }, } export default translation diff --git a/web/i18n/th-TH/app.ts b/web/i18n/th-TH/app.ts index 9204c71d32..0979d07f51 100644 --- a/web/i18n/th-TH/app.ts +++ b/web/i18n/th-TH/app.ts @@ -209,12 +209,14 @@ const translation = { anyone: 'ใครก็สามารถเข้าถึงเว็บแอปได้', specific: 'สมาชิกหรือกลุ่มเฉพาะเท่านั้นที่สามารถเข้าถึงแอปเว็บได้', organization: 'ใครก็ได้ในองค์กรสามารถเข้าถึงแอปเว็บได้', + external: 'ผู้ใช้งานภายนอกที่ได้รับการยืนยันตัวตนเท่านั้นที่สามารถเข้าถึงแอปพลิเคชันเว็บได้', }, accessControlDialog: { accessItems: { specific: 'กลุ่มหรือสมาชิกเฉพาะ', organization: 'เฉพาะสมาชิกภายในองค์กร', anyone: 'ใครก็ตามที่มีลิงก์', + external: 'ผู้ใช้ภายนอกที่ได้รับการตรวจสอบแล้ว', }, operateGroupAndMember: { searchPlaceholder: 'ค้นหากลุ่มและสมาชิก', diff --git a/web/i18n/th-TH/share-app.ts b/web/i18n/th-TH/share-app.ts index fd4a8f386c..eca049b9a2 100644 --- a/web/i18n/th-TH/share-app.ts +++ b/web/i18n/th-TH/share-app.ts @@ -73,6 +73,9 @@ const translation = { execution: 'การดำเนินการ', executions: '{{num}} การประหารชีวิต', }, + login: { + backToHome: 'กลับไปที่หน้าแรก', + }, } export default translation diff --git a/web/i18n/tr-TR/app.ts b/web/i18n/tr-TR/app.ts index 995cc9c795..5e55ffa349 100644 --- a/web/i18n/tr-TR/app.ts +++ b/web/i18n/tr-TR/app.ts @@ -209,12 +209,14 @@ const translation = { anyone: 'Herkes web uygulamasına erişebilir', organization: 'Kuruluşta herkes web uygulamasına erişebilir.', specific: 'Sadece belirli gruplar veya üyeler web uygulamasına erişebilir.', + external: 'Sadece kimliği doğrulanmış dış kullanıcılar Web uygulamasına erişebilir', }, accessControlDialog: { accessItems: { anyone: 'Bağlantıya sahip olan herkes', organization: 'Sadece işletme içindeki üyeler', specific: 'Belirli gruplar veya üyeler', + external: 'Kimliği onaylanmış harici kullanıcılar', }, operateGroupAndMember: { searchPlaceholder: 'Grupları ve üyeleri ara', diff --git a/web/i18n/tr-TR/share-app.ts b/web/i18n/tr-TR/share-app.ts index 184f44e147..e7ad4fcd68 100644 --- a/web/i18n/tr-TR/share-app.ts +++ b/web/i18n/tr-TR/share-app.ts @@ -73,6 +73,9 @@ const translation = { execution: 'İFRAZAT', executions: '{{num}} İDAM', }, + login: { + backToHome: 'Ana Sayfaya Dön', + }, } export default translation diff --git a/web/i18n/uk-UA/app-debug.ts b/web/i18n/uk-UA/app-debug.ts index 1fc6981122..7e410ffef9 100644 --- a/web/i18n/uk-UA/app-debug.ts +++ b/web/i18n/uk-UA/app-debug.ts @@ -279,6 +279,7 @@ const translation = { 'labelName': 'Назва мітки', 'inputPlaceholder': 'Будь ласка, введіть', 'required': 'Обов\'язково', + 'hide': 'Приховати', 'errorMsg': { varNameRequired: 'Потрібно вказати назву змінної', labelNameRequired: 'Потрібно вказати назву мітки', diff --git a/web/i18n/uk-UA/app.ts b/web/i18n/uk-UA/app.ts index 4bbb0dcbf1..6e5ff5dc74 100644 --- a/web/i18n/uk-UA/app.ts +++ b/web/i18n/uk-UA/app.ts @@ -213,12 +213,14 @@ const translation = { anyone: 'Будь-хто може отримати доступ до веб-додатку', specific: 'Тільки окремі групи або члени можуть отримати доступ до веб-додатку.', organization: 'Будь-хто в організації може отримати доступ до веб-додатку.', + external: 'Тільки перевірені зовнішні користувачі можуть отримати доступ до веб-застосунку.', }, accessControlDialog: { accessItems: { anyone: 'Кожен, у кого є посилання', specific: 'Конкретні групи або члени', organization: 'Тільки члени підприємства', + external: 'Аутентифіковані зовнішні користувачі', }, operateGroupAndMember: { searchPlaceholder: 'Шукати групи та учасників', diff --git a/web/i18n/uk-UA/share-app.ts b/web/i18n/uk-UA/share-app.ts index 058925ff15..92f25545d9 100644 --- a/web/i18n/uk-UA/share-app.ts +++ b/web/i18n/uk-UA/share-app.ts @@ -73,6 +73,9 @@ const translation = { execution: 'ВИКОНАННЯ', executions: '{{num}} ВИКОНАНЬ', }, + login: { + backToHome: 'Повернутися на головну', + }, } export default translation diff --git a/web/i18n/vi-VN/app-debug.ts b/web/i18n/vi-VN/app-debug.ts index 4e8a1962fe..c091cb5abb 100644 --- a/web/i18n/vi-VN/app-debug.ts +++ b/web/i18n/vi-VN/app-debug.ts @@ -279,6 +279,7 @@ const translation = { 'labelName': 'Tên nhãn', 'inputPlaceholder': 'Vui lòng nhập', 'required': 'Bắt buộc', + 'hide': 'Ẩn', 'errorMsg': { varNameRequired: 'Tên biến là bắt buộc', labelNameRequired: 'Tên nhãn là bắt buộc', diff --git a/web/i18n/vi-VN/app.ts b/web/i18n/vi-VN/app.ts index 243454d011..c5f1a7496d 100644 --- a/web/i18n/vi-VN/app.ts +++ b/web/i18n/vi-VN/app.ts @@ -213,12 +213,14 @@ const translation = { anyone: 'Mọi người đều có thể truy cập ứng dụng web.', specific: 'Chỉ những nhóm hoặc thành viên cụ thể mới có thể truy cập ứng dụng web.', organization: 'Bất kỳ ai trong tổ chức đều có thể truy cập ứng dụng web.', + external: 'Chỉ những người dùng bên ngoài đã xác thực mới có thể truy cập vào ứng dụng Web.', }, accessControlDialog: { accessItems: { anyone: 'Ai có liên kết', specific: 'Các nhóm hoặc thành viên cụ thể', organization: 'Chỉ các thành viên trong doanh nghiệp', + external: 'Người dùng bên ngoài được xác thực', }, operateGroupAndMember: { searchPlaceholder: 'Tìm kiếm nhóm và thành viên', diff --git a/web/i18n/vi-VN/share-app.ts b/web/i18n/vi-VN/share-app.ts index a55f9b8476..12a31bd40b 100644 --- a/web/i18n/vi-VN/share-app.ts +++ b/web/i18n/vi-VN/share-app.ts @@ -73,6 +73,9 @@ const translation = { executions: '{{num}} ÁN TỬ HÌNH', execution: 'THI HÀNH', }, + login: { + backToHome: 'Trở về Trang Chủ', + }, } export default translation diff --git a/web/i18n/vi-VN/workflow.ts b/web/i18n/vi-VN/workflow.ts index 2e0acf0c4d..448a4657a4 100644 --- a/web/i18n/vi-VN/workflow.ts +++ b/web/i18n/vi-VN/workflow.ts @@ -228,7 +228,7 @@ const translation = { 'logic': 'Logic', 'transform': 'Chuyển đổi', 'utilities': 'Tiện ích', - 'noResult': 'Không tìm thấy kế;t quả phù hợp', + 'noResult': 'Không tìm thấy kế. t quả phù hợp', 'searchTool': 'Công cụ tìm kiếm', 'agent': 'Chiến lược đại lý', 'plugin': 'Plugin', diff --git a/web/i18n/zh-Hans/app-api.ts b/web/i18n/zh-Hans/app-api.ts index f59d9065a6..70b8413244 100644 --- a/web/i18n/zh-Hans/app-api.ts +++ b/web/i18n/zh-Hans/app-api.ts @@ -41,7 +41,7 @@ const translation = { messageFeedbackApi: '消息反馈(点赞)', messageFeedbackApiTip: '代表最终用户对返回消息进行评价,可以点赞与点踩,该数据将在“日志与标注”页中可见,并用于后续的模型微调。', messageIDTip: '消息 ID', - ratingTip: 'like 或 dislike, 空值为撤销', + ratingTip: 'like 或 dislike,空值为撤销', parametersApi: '获取应用配置信息', parametersApiTip: '获取已配置的 Input 参数,包括变量名、字段名称、类型与默认值。通常用于客户端加载后显示这些字段的表单或填入默认值。', }, @@ -58,7 +58,7 @@ const translation = { messageFeedbackApi: '消息反馈(点赞)', messageFeedbackApiTip: '代表最终用户对返回消息进行评价,可以点赞与点踩,该数据将在“日志与标注”页中可见,并用于后续的模型微调。', messageIDTip: '消息 ID', - ratingTip: 'like 或 dislike, 空值为撤销', + ratingTip: 'like 或 dislike,空值为撤销', chatMsgHistoryApi: '获取会话历史消息', chatMsgHistoryApiTip: '滚动加载形式返回历史聊天记录,第一页返回最新 `limit` 条,即:倒序返回。', chatMsgHistoryConversationIdTip: '会话 ID', diff --git a/web/i18n/zh-Hans/app-debug.ts b/web/i18n/zh-Hans/app-debug.ts index af221a926c..4f84b396d0 100644 --- a/web/i18n/zh-Hans/app-debug.ts +++ b/web/i18n/zh-Hans/app-debug.ts @@ -123,7 +123,7 @@ const translation = { ok: '好的', contextVarNotEmpty: '上下文查询变量不能为空', deleteContextVarTitle: '删除变量“{{varName}}”?', - deleteContextVarTip: '该变量已被设置为上下文查询变量,删除该变量将影响知识库的正常使用。 如果您仍需要删除它,请在上下文部分中重新选择它。', + deleteContextVarTip: '该变量已被设置为上下文查询变量,删除该变量将影响知识库的正常使用。如果您仍需要删除它,请在上下文部分中重新选择它。', }, }, tools: { @@ -361,6 +361,7 @@ const translation = { 'inputPlaceholder': '请输入', 'labelName': '显示名称', 'required': '必填', + 'hide': '隐藏', 'file': { supportFileTypes: '支持的文件类型', image: { @@ -402,9 +403,9 @@ const translation = { visionSettings: { title: '视觉设置', resolution: '分辨率', - resolutionTooltip: `低分辨率模式将使模型接收图像的低分辨率版本,尺寸为512 x 512,并使用65 Tokens 来表示图像。这样可以使API更快地返回响应,并在不需要高细节的用例中消耗更少的输入。 + resolutionTooltip: `低分辨率模式将使模型接收图像的低分辨率版本,尺寸为 512 x 512,并使用 65 Tokens 来表示图像。这样可以使 API 更快地返回响应,并在不需要高细节的用例中消耗更少的输入。 \n - 高分辨率模式将首先允许模型查看低分辨率图像,然后根据输入图像的大小创建512像素的详细裁剪图像。每个详细裁剪图像使用两倍的预算总共为129 Tokens。`, + 高分辨率模式将首先允许模型查看低分辨率图像,然后根据输入图像的大小创建 512 像素的详细裁剪图像。每个详细裁剪图像使用两倍的预算总共为 129 Tokens。`, high: '高', low: '低', uploadMethod: '上传方式', @@ -437,7 +438,7 @@ const translation = { openingQuestion: '开场问题', noDataPlaceHolder: '在对话型应用中,让 AI 主动说第一段话可以拉近与用户间的距离。', - varTip: '你可以使用变量, 试试输入 {{variable}}', + varTip: '你可以使用变量,试试输入 {{variable}}', tooShort: '对话前提示词至少 20 字才能生成开场白', notIncludeKey: '前缀提示词中不包含变量 {{key}}。请在前缀提示词中添加该变量', }, @@ -466,9 +467,9 @@ const translation = { noResult: '输出结果展示在这', datasetConfig: { settingTitle: '召回设置', - knowledgeTip: '点击 “+” 按钮添加知识库', + knowledgeTip: '点击“+”按钮添加知识库', retrieveOneWay: { - title: 'N选1召回', + title: 'N 选 1 召回', description: '根据用户意图和知识库描述,由 Agent 自主判断选择最匹配的单个知识库来查询相关文本,适合知识库区分度大且知识库数量偏少的应用。', }, retrieveMultiWay: { diff --git a/web/i18n/zh-Hans/app-overview.ts b/web/i18n/zh-Hans/app-overview.ts index 1486f9b4c4..a41a86975a 100644 --- a/web/i18n/zh-Hans/app-overview.ts +++ b/web/i18n/zh-Hans/app-overview.ts @@ -1,6 +1,6 @@ const translation = { welcome: { - firstStepTip: '开始之前,', + firstStepTip: '开始之前,', enterKeyTip: '请先在下方输入你的 OpenAI API Key', getKeyTip: '从 OpenAI 获取你的 API Key', placeholder: '你的 OpenAI API Key(例如 sk-xxxx)', @@ -9,11 +9,11 @@ const translation = { cloud: { trial: { title: '您正在使用 {{providerName}} 的试用配额。', - description: '试用配额仅供您测试使用。 在试用配额用完之前,请自行设置模型提供商或购买额外配额。', + description: '试用配额仅供您测试使用。在试用配额用完之前,请自行设置模型提供商或购买额外配额。', }, exhausted: { - title: '您的试用额度已用完,请设置您的APIKey。', - description: '您的试用配额已用完。 请设置您自己的模型提供商或购买额外配额。', + title: '您的试用额度已用完,请设置您的 APIKey。', + description: '您的试用配额已用完。请设置您自己的模型提供商或购买额外配额。', }, }, selfHost: { diff --git a/web/i18n/zh-Hans/app.ts b/web/i18n/zh-Hans/app.ts index a634394cfb..4ec1e65059 100644 --- a/web/i18n/zh-Hans/app.ts +++ b/web/i18n/zh-Hans/app.ts @@ -23,7 +23,7 @@ const translation = { importFromDSLFile: '文件', importFromDSLUrl: 'URL', importFromDSLUrlPlaceholder: '输入 DSL 文件的 URL', - deleteAppConfirmTitle: '确认删除应用?', + deleteAppConfirmTitle: '确认删除应用?', deleteAppConfirmContent: '删除应用将无法撤销。用户将不能访问你的应用,所有 Prompt 编排配置和日志均将一并被删除。', appDeleted: '应用已删除', @@ -169,12 +169,12 @@ const translation = { publicKey: '公钥', secretKey: '密钥', viewDocsLink: '查看 {{key}} 的文档', - removeConfirmTitle: '删除 {{key}} 配置?', + removeConfirmTitle: '删除 {{key}} 配置?', removeConfirmContent: '当前配置正在使用中,删除它将关闭追踪功能。', }, weave: { title: '编织', - description: 'Weave是一个开源平台,用于评估、测试和监控大型语言模型应用程序。', + description: 'Weave 是一个开源平台,用于评估、测试和监控大型语言模型应用程序。', }, }, appSelector: { @@ -198,30 +198,27 @@ const translation = { }, accessControl: 'Web 应用访问控制', accessItemsDescription: { - anyone: '任何人可以访问 web 应用', - specific: '特定组或成员可以访问 web 应用', - organization: '组织内任何人可以访问 web 应用', + anyone: '任何人都可以访问该 web 应用(无需登录)', + specific: '仅指定的平台内成员可访问该 Web 应用', + organization: '平台内所有成员均可访问该 Web 应用', + external: '仅经认证的外部用户可访问该 Web 应用', }, accessControlDialog: { title: 'Web 应用访问权限', description: '设置 web 应用访问权限。', accessLabel: '谁可以访问', - accessItemsDescription: { - anyone: '任何人可以访问 web 应用', - specific: '特定组或成员可以访问 web 应用', - organization: '组织内任何人可以访问 web 应用', - }, accessItems: { anyone: '任何人', - specific: '特定组或成员', - organization: '组织内任何人', + specific: '平台内指定成员', + organization: '平台内所有成员', + external: '经认证的外部用户', }, groups_one: '{{count}} 个组', groups_other: '{{count}} 个组', members_one: '{{count}} 个成员', members_other: '{{count}} 个成员', noGroupsOrMembers: '未选择分组或成员', - webAppSSONotEnabledTip: '请联系企业管理员配置 web 应用的身份认证方式。', + webAppSSONotEnabledTip: '请联系企业管理员配置 Web 应用外部认证方式。', operateGroupAndMember: { searchPlaceholder: '搜索组或成员', allMembers: '所有成员', diff --git a/web/i18n/zh-Hans/common.ts b/web/i18n/zh-Hans/common.ts index b8e1342fc4..00c8a33837 100644 --- a/web/i18n/zh-Hans/common.ts +++ b/web/i18n/zh-Hans/common.ts @@ -126,7 +126,7 @@ const translation = { '影响常见与罕见词汇使用。\n值较大时,倾向于生成不常见的词汇和表达方式。\n值越小,更倾向于使用常见和普遍接受的词汇或短语。', max_tokens: '单次回复限制 max_tokens', max_tokensTip: - '用于限制回复的最大长度,以 token 为单位。\n较大的值可能会限制给提示词、聊天记录和知识库留出的空间。\n建议将其设置在三分之二以下。\ngpt-4-1106-preview、gpt-4-vision-preview 最大长度 (输入128k,输出4k)', + '用于限制回复的最大长度,以 token 为单位。\n较大的值可能会限制给提示词、聊天记录和知识库留出的空间。\n建议将其设置在三分之二以下。\ngpt-4-1106-preview、gpt-4-vision-preview 最大长度 (输入 128k,输出 4k)', maxTokenSettingTip: '您设置的最大 tokens 数较大,可能会导致 prompt、用户问题、知识库内容没有 token 空间进行处理,建议设置到 2/3 以下。', setToCurrentModelMaxTokenTip: '最大令牌数更新为当前模型最大的令牌数 {{maxToken}} 的 80%。', stop_sequences: '停止序列 stop_sequences', @@ -153,7 +153,7 @@ const translation = { exploreMarketplace: '探索 Marketplace', pluginsTips: '集成第三方插件或创建与 ChatGPT 兼容的 AI 插件。', datasets: '知识库', - datasetsTips: '即将到来: 上传自己的长文本数据,或通过 Webhook 集成自己的数据源', + datasetsTips: '即将到来:上传自己的长文本数据,或通过 Webhook 集成自己的数据源', newApp: '创建应用', newDataset: '创建知识库', tools: '工具', @@ -270,7 +270,7 @@ const translation = { deleteMember: '删除成员', you: '(你)', builderTip: '可以构建和编辑自己的应用程序', - setBuilder: 'Set as builder (设置为构建器)', + setBuilder: 'Set as builder(设置为构建器)', builder: '构建器', }, integrations: { @@ -348,7 +348,7 @@ const translation = { }, embeddingModel: { key: 'Embedding 模型', - tip: '设置知识库文档嵌入处理的默认模型,检索和导入知识库均使用该Embedding模型进行向量化处理,切换后将导致已导入的知识库与问题之间的向量维度不一致,从而导致检索失败。为避免检索失败,请勿随意切换该模型。', + tip: '设置知识库文档嵌入处理的默认模型,检索和导入知识库均使用该 Embedding 模型进行向量化处理,切换后将导致已导入的知识库与问题之间的向量维度不一致,从而导致检索失败。为避免检索失败,请勿随意切换该模型。', required: '请选择 Embedding 模型', }, speechToTextModel: { @@ -384,7 +384,7 @@ const translation = { buyQuota: '购买额度', priorityUse: '优先使用', removeKey: '删除 API 密钥', - tip: '已付费额度将优先考虑。 试用额度将在付费额度用完后使用。', + tip: '已付费额度将优先考虑。试用额度将在付费额度用完后使用。', }, item: { deleteDesc: '{{modelName}} 被用作系统推理模型。删除后部分功能将无法使用。请确认。', @@ -415,7 +415,7 @@ const translation = { getFreeTokens: '获得免费 Tokens', priorityUsing: '优先使用', deprecated: '已弃用', - confirmDelete: '确认删除?', + confirmDelete: '确认删除?', quotaTip: '剩余免费额度', loadPresets: '加载预设', parameters: '参数', @@ -667,7 +667,7 @@ const translation = { imageInput: { dropImageHere: '将图片拖放到此处,或', browse: '浏览', - supportedFormats: '支持PNG、JPG、JPEG、WEBP和GIF格式', + supportedFormats: '支持 PNG、JPG、JPEG、WEBP 和 GIF 格式', }, you: '你', } diff --git a/web/i18n/zh-Hans/dataset-creation.ts b/web/i18n/zh-Hans/dataset-creation.ts index 72e2511bd4..6f4c5af7eb 100644 --- a/web/i18n/zh-Hans/dataset-creation.ts +++ b/web/i18n/zh-Hans/dataset-creation.ts @@ -92,18 +92,18 @@ const translation = { excludePaths: '排除路径', includeOnlyPaths: '仅包含路径', extractOnlyMainContent: '仅提取主要内容(无标题、导航、页脚等)', - exceptionErrorTitle: '运行时发生异常:', + exceptionErrorTitle: '运行时发生异常:', unknownError: '未知错误', - totalPageScraped: '抓取页面总数:', + totalPageScraped: '抓取页面总数:', selectAll: '全选', resetAll: '重置全部', scrapTimeInfo: '总共在 {{time}}秒 内抓取了 {{total}} 个页面', preview: '预览', - maxDepthTooltip: '相对于输入 URL 的最大抓取深度。深度0仅抓取输入 URL 本身的页面,深度1抓取输入 URL 及其后的一层目录(一个 /),依此类推。', + maxDepthTooltip: '相对于输入 URL 的最大抓取深度。深度 0 仅抓取输入 URL 本身的页面,深度 1 抓取输入 URL 及其后的一层目录(一个 /),依此类推。', watercrawlDocLink: '从网站同步', watercrawlDoc: 'Watercrawl 文档', configureWatercrawl: '配置水爬行', - watercrawlTitle: '使用Watercrawl提取网页内容', + watercrawlTitle: '使用 Watercrawl 提取网页内容', }, }, stepTwo: { @@ -121,19 +121,19 @@ const translation = { paragraph: '段落', paragraphTip: '此模式根据分隔符和最大块长度将文本拆分为段落,使用拆分文本作为检索的父块', fullDoc: '全文', - fullDocTip: '整个文档用作父块并直接检索。请注意,出于性能原因,超过10000个标记的文本将被自动截断。', + fullDocTip: '整个文档用作父块并直接检索。请注意,出于性能原因,超过 10000 个标记的文本将被自动截断。', separator: '分段标识符', separatorTip: '分隔符是用于分隔文本的字符。\\n\\n 和 \\n 是常用于分隔段落和行的分隔符。用逗号连接分隔符(\\n\\n,\\n),当段落超过最大块长度时,会按行进行分割。你也可以使用自定义的特殊分隔符(例如 ***)。', separatorPlaceholder: '\\n\\n 用于分段;\\n 用于分行', maxLength: '分段最大长度', maxLengthCheck: '分段最大长度不能大于 {{limit}}', overlap: '分段重叠长度', - overlapTip: '设置分段之间的重叠长度可以保留分段之间的语义关系,提升召回效果。建议设置为最大分段长度的10%-25%', + overlapTip: '设置分段之间的重叠长度可以保留分段之间的语义关系,提升召回效果。建议设置为最大分段长度的 10%-25%', overlapCheck: '分段重叠长度不能大于分段最大长度', rules: '文本预处理规则', removeExtraSpaces: '替换掉连续的空格、换行符和制表符', removeUrlEmails: '删除所有 URL 和电子邮件地址', - removeStopwords: '去除停用词,例如 “a”,“an”,“the” 等', + removeStopwords: '去除停用词,例如“a”,“an”,“the”等', preview: '预览', previewChunk: '预览块', reset: '重置', @@ -141,11 +141,11 @@ const translation = { qualified: '高质量', highQualityTip: '使用高质量模式进行嵌入后,无法切换回经济模式。', recommend: '推荐', - qualifiedTip: '调用嵌入模型处理文档以实现更精确的检索,可以帮助LLM生成高质量的答案。', + qualifiedTip: '调用嵌入模型处理文档以实现更精确的检索,可以帮助 LLM 生成高质量的答案。', warning: '请先完成模型供应商的 API KEY 设置。.', click: '前往设置', economical: '经济', - economicalTip: '每个数据块使用10个关键词进行检索,不会消耗任何tokens,但会以降低检索准确性为代价。', + economicalTip: '每个数据块使用 10 个关键词进行检索,不会消耗任何 tokens,但会以降低检索准确性为代价。', QATitle: '采用 Q&A 分段模式', QATip: '开启后将会消耗额外的 token', QALanguage: '分段使用', diff --git a/web/i18n/zh-Hans/dataset.ts b/web/i18n/zh-Hans/dataset.ts index 064ceb3c03..9b5691ae24 100644 --- a/web/i18n/zh-Hans/dataset.ts +++ b/web/i18n/zh-Hans/dataset.ts @@ -27,7 +27,7 @@ const translation = { createDataset: '创建知识库', noExternalKnowledge: '还没有外部知识库 API,点击此处创建', createExternalAPI: '添加外部知识库 API', - createNewExternalAPI: '创建新的外部知识库API', + createNewExternalAPI: '创建新的外部知识库 API', editExternalAPIFormTitle: '编辑外部知识库 API', editExternalAPITooltipTitle: '个关联知识库', editExternalAPIConfirmWarningContent: { @@ -69,8 +69,8 @@ const translation = { createDatasetIntro: '导入您自己的文本数据或通过 Webhook 实时写入数据以增强 LLM 的上下文。', deleteDatasetConfirmTitle: '要删除知识库吗?', deleteDatasetConfirmContent: - '删除知识库是不可逆的。用户将无法再访问您的知识库,所有的提示配置和日志将被永久删除。', - datasetUsedByApp: '某些应用正在使用该知识库。应用将无法再使用该知识库,所有的提示配置和日志将被永久删除。', + '删除知识库是不可逆的。用户将无法再访问您的知识库,所有的提示配置和日志将被永久删除。', + datasetUsedByApp: '某些应用正在使用该知识库。应用将无法再使用该知识库,所有的提示配置和日志将被永久删除。', datasetDeleted: '知识库已删除', datasetDeleteFailed: '删除知识库失败', selectExternalKnowledgeAPI: { @@ -207,7 +207,7 @@ const translation = { builtIn: '内置', builtInDescription: '内置元数据是系统预定义的元数据,您可以在此处查看和管理内置元数据。', deleteTitle: '确定删除', - deleteContent: '你确定要删除元数据 "{{name}}" 吗?', + deleteContent: '你确定要删除元数据 "{{name}}" 吗?', }, documentMetadata: { metadataToolTip: '元数据是关于文档的数据,用于描述文档的属性。元数据可以帮助您更好地组织和管理文档。', diff --git a/web/i18n/zh-Hans/login.ts b/web/i18n/zh-Hans/login.ts index e2a958ea05..a37fc104eb 100644 --- a/web/i18n/zh-Hans/login.ts +++ b/web/i18n/zh-Hans/login.ts @@ -59,12 +59,12 @@ const translation = { emailInValid: '请输入有效的邮箱地址', nameEmpty: '用户名不能为空', passwordEmpty: '密码不能为空', - passwordInvalid: '密码必须包含字母和数字,且长度不小于8位', + passwordInvalid: '密码必须包含字母和数字,且长度不小于 8 位', passwordLengthInValid: '密码必须至少为 8 个字符', registrationNotAllowed: '账户不存在,请联系系统管理员注册账户', }, license: { - tip: '启动 Dify 社区版之前, 请阅读 GitHub 上的', + tip: '启动 Dify 社区版之前,请阅读 GitHub 上的', link: '开源协议', }, join: '加入 ', diff --git a/web/i18n/zh-Hans/plugin.ts b/web/i18n/zh-Hans/plugin.ts index 6648e3b65a..db653913a2 100644 --- a/web/i18n/zh-Hans/plugin.ts +++ b/web/i18n/zh-Hans/plugin.ts @@ -67,7 +67,7 @@ const translation = { endpointsDocLink: '查看文档', endpointsEmpty: '点击 \'+\' 按钮添加 API 端点', endpointDisableTip: '停用 API 端点', - endpointDisableContent: '是否要停用 {{name}} 的 API 端点 ?', + endpointDisableContent: '是否要停用 {{name}} 的 API 端点?', endpointDeleteTip: '移除 API 端点', endpointDeleteContent: '是否要移除 {{name}} ?', endpointModalTitle: '设置 API 端点', @@ -124,7 +124,7 @@ const translation = { pluginInfo: '插件信息', delete: '移除插件', deleteContentLeft: '是否要移除 ', - deleteContentRight: ' 插件?', + deleteContentRight: ' 插件?', usedInApps: '此插件正在 {{num}} 个应用中使用。', }, installModal: { diff --git a/web/i18n/zh-Hans/share-app.ts b/web/i18n/zh-Hans/share-app.ts index 3e89aec041..ce1270dae8 100644 --- a/web/i18n/zh-Hans/share-app.ts +++ b/web/i18n/zh-Hans/share-app.ts @@ -68,11 +68,14 @@ const translation = { empty: '上传文件的内容不能为空', fileStructNotMatch: '上传文件的内容与结构不匹配', emptyLine: '第 {{rowIndex}} 行的内容为空', - invalidLine: '第 {{rowIndex}} 行: {{varName}}值必填', - moreThanMaxLengthLine: '第 {{rowIndex}} 行: {{varName}}值超过最大长度 {{maxLength}}', + invalidLine: '第 {{rowIndex}} 行:{{varName}}值必填', + moreThanMaxLengthLine: '第 {{rowIndex}} 行:{{varName}}值超过最大长度 {{maxLength}}', atLeastOne: '上传文件的内容不能少于一条', }, }, + login: { + backToHome: '返回首页', + }, } export default translation diff --git a/web/i18n/zh-Hans/tools.ts b/web/i18n/zh-Hans/tools.ts index 81be870765..9a573ad308 100644 --- a/web/i18n/zh-Hans/tools.ts +++ b/web/i18n/zh-Hans/tools.ts @@ -29,7 +29,7 @@ const translation = { added: '已添加', manageInTools: '去工具列表管理', emptyTitle: '没有可用的工作流工具', - emptyTip: '去 “工作流 -> 发布为工具” 添加', + emptyTip: '去“工作流 -> 发布为工具”添加', emptyTitleCustom: '没有可用的自定义工具', emptyTipCustom: '创建自定义工具', }, @@ -52,8 +52,8 @@ const translation = { urlError: '请输入有效的 URL', examples: '例子', exampleOptions: { - json: '天气(JSON)', - yaml: '宠物商店(YAML)', + json: '天气 (JSON)', + yaml: '宠物商店 (YAML)', blankTemplate: '空白模版', }, availableTools: { @@ -98,7 +98,7 @@ const translation = { methodParameter: 'LLM 填入', methodParameterTip: 'LLM 在推理过程中填写', label: '标签', - labelPlaceholder: '选择标签(可选)', + labelPlaceholder: '选择标签 (可选)', description: '描述', descriptionPlaceholder: '参数意义的描述', }, @@ -135,7 +135,7 @@ const translation = { infoAndSetting: '信息和设置', }, noCustomTool: { - title: '没有自定义工具!', + title: '没有自定义工具!', content: '在此统一添加和管理你的自定义工具,方便构建应用时使用。', createTool: '创建工具', }, diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index b8257d8229..79b9e674fd 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -58,7 +58,7 @@ const translation = { processData: '数据处理', input: '输入', output: '输出', - jinjaEditorPlaceholder: '输入 “/” 或 “{” 插入变量', + jinjaEditorPlaceholder: '输入“/”或“{”插入变量', viewOnly: '只读', showRunHistory: '显示运行历史', enableJinja: '开启支持 Jinja 模板', @@ -271,7 +271,7 @@ const translation = { 'variable-aggregator': '将多路分支的变量聚合为一个变量,以实现下游节点统一配置。', 'iteration': '对列表对象执行多次步骤直至输出所有结果。', 'loop': '循环执行一段逻辑直到满足结束条件或者到达循环次数上限。', - 'loop-end': '相当于“break” 此节点没有配置项,当循环体内运行到此节点后循环终止。', + 'loop-end': '相当于“break”此节点没有配置项,当循环体内运行到此节点后循环终止。', 'parameter-extractor': '利用 LLM 从自然语言内推理提取出结构化参数,用于后置的工具调用或 HTTP 请求。', 'document-extractor': '用于将用户上传的文档解析为 LLM 便于理解的文本内容。', 'list-operator': '用于过滤或排序数组内容。', @@ -538,7 +538,7 @@ const translation = { writePlaceholder: '输入写入超时(以秒为单位)', }, curl: { - title: '导入cURL', + title: '导入 cURL', placeholder: '粘贴 cURL 字符串', }, }, @@ -661,9 +661,9 @@ const translation = { type: '支持类型。现在只支持图片', transfer_method: '传输方式。值为 remote_url 或 local_file', url: '图片链接', - upload_file_id: '上传文件ID', + upload_file_id: '上传文件 ID', }, - json: '工具生成的json', + json: '工具生成的 json', }, }, questionClassifiers: { @@ -788,7 +788,7 @@ const translation = { outputVars: { text: '提取的文本', }, - supportFileTypes: '支持的文件类型: {{types}}。', + supportFileTypes: '支持的文件类型:{{types}}。', learnMore: '了解更多', }, listFilter: { @@ -863,9 +863,9 @@ const translation = { type: '支持类型。现在只支持图片', transfer_method: '传输方式。值为 remote_url 或 local_file', url: '图片链接', - upload_file_id: '上传文件ID', + upload_file_id: '上传文件 ID', }, - json: 'agent 生成的json', + json: 'agent 生成的 json', }, checkList: { strategyNotSelected: '未选择策略', diff --git a/web/i18n/zh-Hant/app-api.ts b/web/i18n/zh-Hant/app-api.ts index 1a2cff7202..db43cd8b77 100644 --- a/web/i18n/zh-Hant/app-api.ts +++ b/web/i18n/zh-Hant/app-api.ts @@ -40,7 +40,7 @@ const translation = { messageFeedbackApi: '訊息反饋(點贊)', messageFeedbackApiTip: '代表終端使用者對返回訊息進行評價,可以點贊與點踩,該資料將在“日誌與標註”頁中可見,並用於後續的模型微調。', messageIDTip: '訊息 ID', - ratingTip: 'like 或 dislike, 空值為撤銷', + ratingTip: 'like 或 dislike,空值為撤銷', parametersApi: '獲取應用配置資訊', parametersApiTip: '獲取已配置的 Input 引數,包括變數名、欄位名稱、型別與預設值。通常用於客戶端載入後顯示這些欄位的表單或填入預設值。', }, @@ -57,7 +57,7 @@ const translation = { messageFeedbackApi: '訊息反饋(點贊)', messageFeedbackApiTip: '代表終端使用者對返回訊息進行評價,可以點贊與點踩,該資料將在“日誌與標註”頁中可見,並用於後續的模型微調。', messageIDTip: '訊息 ID', - ratingTip: 'like 或 dislike, 空值為撤銷', + ratingTip: 'like 或 dislike,空值為撤銷', chatMsgHistoryApi: '獲取會話歷史訊息', chatMsgHistoryApiTip: '滾動載入形式返回歷史聊天記錄,第一頁返回最新 `limit` 條,即:倒序返回。', chatMsgHistoryConversationIdTip: '會話 ID', diff --git a/web/i18n/zh-Hant/app-debug.ts b/web/i18n/zh-Hant/app-debug.ts index ad29195cb5..16374992b5 100644 --- a/web/i18n/zh-Hant/app-debug.ts +++ b/web/i18n/zh-Hant/app-debug.ts @@ -123,7 +123,7 @@ const translation = { ok: '好的', contextVarNotEmpty: '上下文查詢變數不能為空', deleteContextVarTitle: '刪除變數“{{varName}}”?', - deleteContextVarTip: '該變數已被設定為上下文查詢變數,刪除該變數將影響知識庫的正常使用。 如果您仍需要刪除它,請在上下文部分中重新選擇它。', + deleteContextVarTip: '該變數已被設定為上下文查詢變數,刪除該變數將影響知識庫的正常使用。如果您仍需要刪除它,請在上下文部分中重新選擇它。', }, }, tools: { @@ -264,6 +264,7 @@ const translation = { 'inputPlaceholder': '請輸入', 'labelName': '顯示名稱', 'required': '必填', + 'hide': '隱藏', 'errorMsg': { varNameRequired: '變數名稱必填', labelNameRequired: '顯示名稱必填', @@ -279,9 +280,9 @@ const translation = { visionSettings: { title: '視覺設定', resolution: '解析度', - resolutionTooltip: `低解析度模式將使模型接收影象的低解析度版本,尺寸為512 x 512,並使用65 Tokens 來表示影象。這樣可以使API更快地返回響應,並在不需要高細節的用例中消耗更少的輸入。 + resolutionTooltip: `低解析度模式將使模型接收影象的低解析度版本,尺寸為 512 x 512,並使用 65 Tokens 來表示影象。這樣可以使 API 更快地返回響應,並在不需要高細節的用例中消耗更少的輸入。 \n - 高解析度模式將首先允許模型檢視低解析度影象,然後根據輸入影象的大小建立512畫素的詳細裁剪影象。每個詳細裁剪影象使用兩倍的預算總共為129 Tokens。`, + 高解析度模式將首先允許模型檢視低解析度影象,然後根據輸入影象的大小建立 512 畫素的詳細裁剪影象。每個詳細裁剪影象使用兩倍的預算總共為 129 Tokens。`, high: '高', low: '低', uploadMethod: '上傳方式', @@ -314,7 +315,7 @@ const translation = { openingQuestion: '開場問題', noDataPlaceHolder: '在對話型應用中,讓 AI 主動說第一段話可以拉近與使用者間的距離。', - varTip: '你可以使用變數, 試試輸入 {{variable}}', + varTip: '你可以使用變數,試試輸入 {{variable}}', tooShort: '對話前提示詞至少 20 字才能生成開場白', notIncludeKey: '字首提示詞中不包含變數 {{key}}。請在字首提示詞中新增該變數', }, @@ -342,9 +343,9 @@ const translation = { result: '結果', datasetConfig: { settingTitle: '召回設定', - knowledgeTip: '點選 “+” 按鈕新增知識庫', + knowledgeTip: '點選“+”按鈕新增知識庫', retrieveOneWay: { - title: 'N選1召回', + title: 'N 選 1 召回', description: '根據使用者意圖和知識庫描述,由 Agent 自主判斷選擇最匹配的單個知識庫來查詢相關文字,適合知識庫區分度大且知識庫數量偏少的應用。', }, retrieveMultiWay: { diff --git a/web/i18n/zh-Hant/app-overview.ts b/web/i18n/zh-Hant/app-overview.ts index 2537e0d4c7..21d9247361 100644 --- a/web/i18n/zh-Hant/app-overview.ts +++ b/web/i18n/zh-Hant/app-overview.ts @@ -1,6 +1,6 @@ const translation = { welcome: { - firstStepTip: '開始之前,', + firstStepTip: '開始之前,', enterKeyTip: '請先在下方輸入你的 OpenAI API Key', getKeyTip: '從 OpenAI 獲取你的 API Key', placeholder: '你的 OpenAI API Key(例如 sk-xxxx)', @@ -9,11 +9,11 @@ const translation = { cloud: { trial: { title: '您正在使用 {{providerName}} 的試用配額。', - description: '試用配額僅供您測試使用。 在試用配額用完之前,請自行設定模型提供商或購買額外配額。', + description: '試用配額僅供您測試使用。在試用配額用完之前,請自行設定模型提供商或購買額外配額。', }, exhausted: { - title: '您的試用額度已用完,請設定您的APIKey。', - description: '您的試用配額已用完。 請設定您自己的模型提供商或購買額外配額。', + title: '您的試用額度已用完,請設定您的 APIKey。', + description: '您的試用配額已用完。請設定您自己的模型提供商或購買額外配額。', }, }, selfHost: { @@ -136,11 +136,11 @@ const translation = { }, activeUsers: { title: '活躍使用者數', - explanation: '每日AI互動次數。', + explanation: '每日 AI 互動次數。', }, totalConversations: { title: '總對話數', - explanation: '每日AI對話次數;不包括提示工程/調試。', + explanation: '每日 AI 對話次數;不包括提示工程/調試。', }, tokenUsage: { title: '費用消耗', diff --git a/web/i18n/zh-Hant/app.ts b/web/i18n/zh-Hant/app.ts index f4135d3e73..c43b2ee308 100644 --- a/web/i18n/zh-Hant/app.ts +++ b/web/i18n/zh-Hant/app.ts @@ -15,7 +15,7 @@ const translation = { exportFailed: '匯出 DSL 失敗', importDSL: '匯入 DSL 檔案', createFromConfigFile: '透過 DSL 檔案建立', - deleteAppConfirmTitle: '確認刪除應用?', + deleteAppConfirmTitle: '確認刪除應用?', deleteAppConfirmContent: '刪除應用將無法復原。使用者將無法存取你的應用,所有 Prompt 設定和日誌都將一併被刪除。', appDeleted: '應用已刪除', @@ -30,17 +30,17 @@ const translation = { chatbotDescription: '使用大型語言模型構建聊天助手', completionDescription: '構建一個根據提示生成高品質文字的應用程式,例如生成文章、摘要、翻譯等。', completionWarning: '該類型不久後將不再支援建立', - agentDescription: '構建一個智慧Agent,可以自主選擇工具來完成任務', - workflowDescription: '以工作流的形式編排生成型應用,提供更多的自訂設定。 它適合有經驗的使用者。', + agentDescription: '構建一個智慧 Agent,可以自主選擇工具來完成任務', + workflowDescription: '以工作流的形式編排生成型應用,提供更多的自訂設定。它適合有經驗的使用者。', workflowWarning: '正在進行 Beta 測試', chatbotType: '聊天助手編排方法', basic: '基礎編排', basicTip: '新手適用,可以切換成工作流編排', basicFor: '新手適用', - basicDescription: '基本編排允許使用簡單的設定編排聊天機器人應用程式,而無需修改內建提示。 它適合初學者。', + basicDescription: '基本編排允許使用簡單的設定編排聊天機器人應用程式,而無需修改內建提示。它適合初學者。', advanced: '工作流編排', advancedFor: '進階使用者適用', - advancedDescription: '工作流編排以工作流的形式編排聊天機器人,提供自訂設定,包括編輯內建提示的能力。 它適合有經驗的使用者。', + advancedDescription: '工作流編排以工作流的形式編排聊天機器人,提供自訂設定,包括編輯內建提示的能力。它適合有經驗的使用者。', captionName: '應用名稱 & 圖示', appNamePlaceholder: '給你的應用起個名字', captionDescription: '描述', @@ -120,7 +120,7 @@ const translation = { }, tracing: { title: '追蹤應用程式效能', - description: '配置第三方LLMOps提供商並追蹤應用程式效能。', + description: '配置第三方 LLMOps 提供商並追蹤應用程式效能。', config: '配置', view: '查看', collapse: '收起', @@ -129,7 +129,7 @@ const translation = { disabled: '已禁用', disabledTip: '請先配置提供商', enabled: '服務中', - tracingDescription: '捕獲應用程式執行的完整上下文,包括LLM調用、上下文、提示、HTTP請求等,到第三方追蹤平台。', + tracingDescription: '捕獲應用程式執行的完整上下文,包括 LLM 調用、上下文、提示、HTTP 請求等,到第三方追蹤平台。', configProviderTitle: { configured: '已配置', notConfigured: '配置提供商以啟用追蹤', @@ -137,11 +137,11 @@ const translation = { }, langsmith: { title: 'LangSmith', - description: '一個全方位的開發者平台,用於LLM驅動的應用程式生命週期的每個步驟。', + description: '一個全方位的開發者平台,用於 LLM 驅動的應用程式生命週期的每個步驟。', }, langfuse: { title: 'Langfuse', - description: '追蹤、評估、提示管理和指標,用於調試和改進您的LLM應用程式。', + description: '追蹤、評估、提示管理和指標,用於調試和改進您的 LLM 應用程式。', }, inUse: '使用中', configProvider: { @@ -198,7 +198,7 @@ const translation = { label: '應用程式', }, structOutput: { - moreFillTip: '顯示最多10層的嵌套', + moreFillTip: '顯示最多 10 層的嵌套', required: '必需的', LLMResponse: 'LLM 回應', structured: '結構化的', @@ -212,12 +212,14 @@ const translation = { anyone: '任何人都可以訪問這個網絡應用程式', specific: '只有特定的群體或成員可以訪問這個網絡應用程序', organization: '組織中的任何人都可以訪問該網絡應用程序', + external: '只有經過身份驗證的外部用戶才能訪問該網絡應用程序', }, accessControlDialog: { accessItems: { anyone: '擁有鏈接的人', specific: '特定群體或成員', organization: '只有企業內部成員', + external: '經過驗證的外部用戶', }, operateGroupAndMember: { searchPlaceholder: '搜尋群組和成員', diff --git a/web/i18n/zh-Hant/billing.ts b/web/i18n/zh-Hant/billing.ts index f3ce5ec79c..6ede2c6213 100644 --- a/web/i18n/zh-Hant/billing.ts +++ b/web/i18n/zh-Hant/billing.ts @@ -9,7 +9,7 @@ const translation = { buyPermissionDeniedTip: '請聯絡企業管理員訂閱', plansCommon: { title: '選擇適合您的套餐', - yearlyTip: '訂閱年度計劃可免費獲得 2個月!', + yearlyTip: '訂閱年度計劃可免費獲得 2 個月!', mostPopular: '最受歡迎', planRange: { monthly: '按月', @@ -31,7 +31,7 @@ const translation = { buildApps: '構建應用程式數', vectorSpace: '向量空間', vectorSpaceTooltip: '向量空間是 LLMs 理解您的資料所需的長期記憶系統。', - vectorSpaceBillingTooltip: '向量儲存是將知識庫向量化處理後為讓 LLMs 理解資料而使用的長期記憶儲存,1MB 大約能滿足1.2 million character 的向量化後資料儲存(以 OpenAI Embedding 模型估算,不同模型計算方式有差異)。在向量化過程中,實際的壓縮或尺寸減小取決於內容的複雜性和冗餘性。', + vectorSpaceBillingTooltip: '向量儲存是將知識庫向量化處理後為讓 LLMs 理解資料而使用的長期記憶儲存,1MB 大約能滿足 1.2 million character 的向量化後資料儲存(以 OpenAI Embedding 模型估算,不同模型計算方式有差異)。在向量化過程中,實際的壓縮或尺寸減小取決於內容的複雜性和冗餘性。', documentsUploadQuota: '文件上傳配額', documentProcessingPriority: '文件處理優先順序', documentProcessingPriorityTip: '如需更高的文件處理優先順序,請升級您的套餐', @@ -88,7 +88,7 @@ const translation = { priceTip: '每個工作區/', cloud: '雲服務', documentsRequestQuota: '{{count,number}}/分鐘 知識請求速率限制', - unlimitedApiRate: '沒有API速率限制', + unlimitedApiRate: '沒有 API 速率限制', apiRateLimitTooltip: 'API 使用次數限制適用於通過 Dify API 所做的所有請求,包括文本生成、聊天對話、工作流執行和文檔處理。', getStarted: '開始使用', freeTrialTip: '200 次 OpenAI 通話的免費試用。', @@ -97,12 +97,12 @@ const translation = { apiRateLimit: 'API 限速', teamMember_other: '{{count,number}} 團隊成員', documentsTooltip: '從知識數據來源導入的文件數量配額。', - documentsRequestQuotaTooltip: '指定工作區在知識基礎中每分鐘可以執行的總操作次數,包括數據集的創建、刪除、更新、文檔上傳、修改、歸檔和知識基礎查詢。這個指標用於評估知識基礎請求的性能。例如,如果一個沙箱用戶在一分鐘內連續執行10次命中測試,他們的工作區將在接下來的一分鐘內暫時禁止執行以下操作:數據集的創建、刪除、更新以及文檔上傳或修改。', + documentsRequestQuotaTooltip: '指定工作區在知識基礎中每分鐘可以執行的總操作次數,包括數據集的創建、刪除、更新、文檔上傳、修改、歸檔和知識基礎查詢。這個指標用於評估知識基礎請求的性能。例如,如果一個沙箱用戶在一分鐘內連續執行 10 次命中測試,他們的工作區將在接下來的一分鐘內暫時禁止執行以下操作:數據集的創建、刪除、更新以及文檔上傳或修改。', }, plans: { sandbox: { name: 'Sandbox', - description: '200次 GPT 免費試用', + description: '200 次 GPT 免費試用', includesTitle: '包括:', for: '核心功能免費試用', }, @@ -159,7 +159,7 @@ const translation = { 3: '優先電子郵件及聊天支持', }, for: '適用於中型組織和團隊', - comingSoon: '微軟Azure與Google Cloud支持即將推出', + comingSoon: '微軟 Azure 與 Google Cloud 支持即將推出', priceTip: '根據雲端市場', btnText: '獲取高級版在', name: '高級', diff --git a/web/i18n/zh-Hant/common.ts b/web/i18n/zh-Hant/common.ts index 08510c286b..296fae9e7e 100644 --- a/web/i18n/zh-Hant/common.ts +++ b/web/i18n/zh-Hant/common.ts @@ -116,7 +116,7 @@ const translation = { '影響常見與罕見詞彙使用。\n值較大時,傾向於生成不常見的詞彙和表達方式。\n值越小,更傾向於使用常見和普遍接受的詞彙或短語。', max_tokens: '單次回覆限制 max_tokens', max_tokensTip: - '用於限制回覆的最大長度,以 token 為單位。\n較大的值可能會限制給提示詞、聊天記錄和知識庫留出的空間。\n建議將其設定在三分之二以下。\ngpt-4-1106-preview、gpt-4-vision-preview 最大長度 (輸入128k,輸出4k)', + '用於限制回覆的最大長度,以 token 為單位。\n較大的值可能會限制給提示詞、聊天記錄和知識庫留出的空間。\n建議將其設定在三分之二以下。\ngpt-4-1106-preview、gpt-4-vision-preview 最大長度 (輸入 128k,輸出 4k)', maxTokenSettingTip: '您設定的最大 tokens 數較大,可能會導致 prompt、使用者問題、知識庫內容沒有 token 空間進行處理,建議設定到 2/3 以下。', setToCurrentModelMaxTokenTip: '最大令牌數更新為當前模型最大的令牌數 {{maxToken}} 的 80%。', stop_sequences: '停止序列 stop_sequences', @@ -142,7 +142,7 @@ const translation = { plugins: '外掛', pluginsTips: '整合第三方外掛或建立與 ChatGPT 相容的 AI 外掛。', datasets: '知識庫', - datasetsTips: '即將到來: 上傳自己的長文字資料,或透過 Webhook 整合自己的資料來源', + datasetsTips: '即將到來:上傳自己的長文字資料,或透過 Webhook 整合自己的資料來源', newApp: '建立應用', newDataset: '建立知識庫', tools: '工具', @@ -205,7 +205,7 @@ const translation = { deleteLabel: '要確認,請在下方輸入您的電子郵件', deletePlaceholder: '請輸入您的電子郵件', verificationLabel: '驗證碼', - verificationPlaceholder: '粘貼6位代碼', + verificationPlaceholder: '粘貼 6 位代碼', permanentlyDeleteButton: '永久刪除帳戶', feedbackTitle: '反饋', feedbackLabel: '告訴我們您刪除帳戶的原因?', @@ -250,7 +250,7 @@ const translation = { disInvite: '取消邀請', deleteMember: '刪除成員', you: '(你)', - setBuilder: 'Set as builder (設置為建構器)', + setBuilder: 'Set as builder(設置為建構器)', datasetOperator: '知識管理員', builder: '建築工人', builderTip: '可以構建和編輯自己的應用程式', @@ -331,7 +331,7 @@ const translation = { }, embeddingModel: { key: 'Embedding 模型', - tip: '設定知識庫文件嵌入處理的預設模型,檢索和匯入知識庫均使用該Embedding模型進行向量化處理,切換後將導致已匯入的知識庫與問題之間的向量維度不一致,從而導致檢索失敗。為避免檢索失敗,請勿隨意切換該模型。', + tip: '設定知識庫文件嵌入處理的預設模型,檢索和匯入知識庫均使用該 Embedding 模型進行向量化處理,切換後將導致已匯入的知識庫與問題之間的向量維度不一致,從而導致檢索失敗。為避免檢索失敗,請勿隨意切換該模型。', required: '請選擇 Embedding 模型', }, speechToTextModel: { @@ -367,7 +367,7 @@ const translation = { buyQuota: '購買額度', priorityUse: '優先使用', removeKey: '刪除 API 金鑰', - tip: '已付費額度將優先考慮。 試用額度將在付費額度用完後使用。', + tip: '已付費額度將優先考慮。試用額度將在付費額度用完後使用。', }, item: { deleteDesc: '{{modelName}} 被用作系統推理模型。刪除後部分功能將無法使用。請確認。', @@ -398,7 +398,7 @@ const translation = { getFreeTokens: '獲得免費 Tokens', priorityUsing: '優先使用', deprecated: '已棄用', - confirmDelete: '確認刪除?', + confirmDelete: '確認刪除?', quotaTip: '剩餘免費額度', loadPresets: '載入預設', parameters: '引數', @@ -408,7 +408,7 @@ const translation = { configLoadBalancing: '配置負載均衡', loadBalancingDescription: '使用多組憑證減輕壓力。', addConfig: '添加配置', - upgradeForLoadBalancing: '升級您的計劃以啟用Load Balancing。', + upgradeForLoadBalancing: '升級您的計劃以啟用 Load Balancing。', apiKey: 'API 金鑰', loadBalancing: '負載均衡', providerManagedDescription: '使用模型提供程式提供的單組憑證。', @@ -631,7 +631,7 @@ const translation = { }, errorMsg: { fieldRequired: '{{field}} 為必填項', - urlError: 'URL應以 http:// 或 https:// 開頭', + urlError: 'URL 應以 http:// 或 https:// 開頭', }, fileUploader: { pasteFileLink: '粘貼文件連結', @@ -644,7 +644,7 @@ const translation = { uploadFromComputerLimit: '上傳文件不能超過 {{size}}', }, license: { - expiring: '將在1天內過期', + expiring: '將在 1 天內過期', expiring_plural: '將在 {{count}} 天后過期', unlimited: '無限制', }, @@ -666,7 +666,7 @@ const translation = { iso27001: 'ISO 27001:2022 認證', }, imageInput: { - supportedFormats: '支援PNG、JPG、JPEG、WEBP和GIF', + supportedFormats: '支援 PNG、JPG、JPEG、WEBP 和 GIF', browse: '瀏覽', dropImageHere: '將您的圖片放在這裡,或', }, diff --git a/web/i18n/zh-Hant/dataset-creation.ts b/web/i18n/zh-Hant/dataset-creation.ts index ca2c410ac6..8955deadc8 100644 --- a/web/i18n/zh-Hant/dataset-creation.ts +++ b/web/i18n/zh-Hant/dataset-creation.ts @@ -103,12 +103,12 @@ const translation = { separatorPlaceholder: '例如換行符(\n)或特定的分隔符(如 "***")', maxLength: '分段最大長度', overlap: '分段重疊長度', - overlapTip: '設定分段之間的重疊長度可以保留分段之間的語義關係,提升召回效果。建議設定為最大分段長度的10%-25%', + overlapTip: '設定分段之間的重疊長度可以保留分段之間的語義關係,提升召回效果。建議設定為最大分段長度的 10%-25%', overlapCheck: '分段重疊長度不能大於分段最大長度', rules: '文字預處理規則', removeExtraSpaces: '替換掉連續的空格、換行符和製表符', removeUrlEmails: '刪除所有 URL 和電子郵件地址', - removeStopwords: '去除停用詞,例如 “a”,“an”,“the” 等', + removeStopwords: '去除停用詞,例如“a”,“an”,“the”等', preview: '預覽', reset: '重置', indexMode: '索引方式', @@ -151,14 +151,14 @@ const translation = { datasetSettingLink: '知識庫設定。', websiteSource: '預處理網站', webpageUnit: '頁面', - separatorTip: '分隔符是用於分隔文字的字元。\\n\\n 和 \\n 是分隔段落和行的常用分隔符。與逗號 (\\n\\n,\\n) 組合使用時,當超過最大區塊長度時,段落將按行分段。您也可以使用自定義的特殊分隔符(例如 ***)。', + separatorTip: '分隔符是用於分隔文字的字元。\\n\\n 和 \\n 是分隔段落和行的常用分隔符。與逗號(\\n\\n,\\n)組合使用時,當超過最大區塊長度時,段落將按行分段。您也可以使用自定義的特殊分隔符(例如 ***)。', maxLengthCheck: '塊最大長度應小於 {{limit}}', general: '常規', previewChunkCount: '{{count}}估計塊數', - useQALanguage: '使用Q&A格式的塊', - qaSwitchHighQualityTipContent: '目前,只有高品質索引方法支援Q&A格式分塊。是否要切換到高品質模式?', + useQALanguage: '使用 Q&A 格式的塊', + qaSwitchHighQualityTipContent: '目前,只有高品質索引方法支援 Q&A 格式分塊。是否要切換到高品質模式?', previewChunk: '預覽資料塊(Preview Chunk)', - fullDocTip: '整個文件用作父塊並直接檢索。請注意,出於性能原因,超過10000個令牌的文本將被自動截斷。', + fullDocTip: '整個文件用作父塊並直接檢索。請注意,出於性能原因,超過 10000 個令牌的文本將被自動截斷。', parentChunkForContext: '父母的背景', previewChunkTip: '點擊左側的 『Preview Chunk』 按鈕載入預覽', parentChild: '父子', @@ -166,7 +166,7 @@ const translation = { parentChildChunkDelimiterTip: '分隔符是用於分隔文字的字元。建議使用 \\n 將父塊拆分為小的子塊。您還可以使用自己定義的特殊分隔符。', parentChildDelimiterTip: '分隔符是用於分隔文字的字元。建議將原始文檔拆分為多個大型父塊。您還可以使用自己定義的特殊分隔符。', generalTip: '常規文本分塊模式,檢索和調用的塊是相同的。', - highQualityTip: '在 High Quality 模式下完成嵌入後,將無法恢復到 Economical (經濟) 模式。', + highQualityTip: '在 High Quality 模式下完成嵌入後,將無法恢復到 Economical(經濟)模式。', childChunkForRetrieval: '用於檢索的 Child-chunk', paragraphTip: '此模式根據分隔符和最大區塊長度將文本拆分為段落,使用拆分文本作為父區塊進行檢索。', paragraph: '段', diff --git a/web/i18n/zh-Hant/dataset-documents.ts b/web/i18n/zh-Hant/dataset-documents.ts index 5ad2c8f61f..60b1df80f3 100644 --- a/web/i18n/zh-Hant/dataset-documents.ts +++ b/web/i18n/zh-Hant/dataset-documents.ts @@ -355,7 +355,7 @@ const translation = { addChunk: '添加數據塊', addChildChunk: '添加子塊', addAnother: '添加另一個', - childChunkAdded: '添加了1個子塊', + childChunkAdded: '添加了 1 個子塊', editParentChunk: '編輯父塊(Edit Parent Chunk)', editChildChunk: '編輯子塊', chunkDetail: '數據塊詳細資訊', diff --git a/web/i18n/zh-Hant/dataset-settings.ts b/web/i18n/zh-Hant/dataset-settings.ts index 3dce646e7e..9068706762 100644 --- a/web/i18n/zh-Hant/dataset-settings.ts +++ b/web/i18n/zh-Hant/dataset-settings.ts @@ -30,7 +30,7 @@ const translation = { save: '儲存', permissionsInvitedMembers: '部分團隊成員', me: '(您)', - externalKnowledgeID: '外部知識ID', + externalKnowledgeID: '外部知識 ID', externalKnowledgeAPI: '外部知識 API', retrievalSettings: '檢索設置', indexMethodChangeToEconomyDisabledTip: '不適用於從 HQ 降級到 ECO', diff --git a/web/i18n/zh-Hant/dataset.ts b/web/i18n/zh-Hant/dataset.ts index ce4d63a54b..0a6b015155 100644 --- a/web/i18n/zh-Hant/dataset.ts +++ b/web/i18n/zh-Hant/dataset.ts @@ -7,7 +7,7 @@ const translation = { createDatasetIntro: '匯入您自己的文字資料或透過 Webhook 實時寫入資料以增強 LLM 的上下文。', deleteDatasetConfirmTitle: '要刪除知識庫嗎?', deleteDatasetConfirmContent: - '刪除知識庫是不可逆的。使用者將無法再訪問您的知識庫,所有的提示配置和日誌將被永久刪除。', + '刪除知識庫是不可逆的。使用者將無法再訪問您的知識庫,所有的提示配置和日誌將被永久刪除。', datasetUsedByApp: '這些知識正被一些應用程序使用。應用程序將無法再使用這些知識,所有提示配置和日誌將被永久刪除。', datasetDeleted: '知識庫已刪除', datasetDeleteFailed: '刪除知識庫失敗', @@ -68,9 +68,9 @@ const translation = { semantic: '語義', keyword: '關鍵詞', }, - nTo1RetrievalLegacy: 'N對1檢索將從9月起正式棄用。建議使用最新的多路徑檢索以獲得更好的結果。', + nTo1RetrievalLegacy: 'N 對 1 檢索將從 9 月起正式棄用。建議使用最新的多路徑檢索以獲得更好的結果。', nTo1RetrievalLegacyLink: '了解更多', - nTo1RetrievalLegacyLinkText: 'N對1檢索將於9月正式棄用。', + nTo1RetrievalLegacyLinkText: 'N 對 1 檢索將於 9 月正式棄用。', defaultRetrievalTip: '默認情況下,使用多路徑檢索。從多個知識庫中檢索知識,然後重新排名。', editExternalAPIConfirmWarningContent: { end: 'external knowledge,並且此修改將應用於所有這些 Knowledge。是否確實要保存此更改?', @@ -98,7 +98,7 @@ const translation = { content: { link: '瞭解如何創建外部 API', front: '要連接到外部知識庫,您需要先創建外部 API。請仔細閱讀並參考', - end: '.然後找到對應的知識ID並在左側的表單中填寫。如果資訊全部正確,點擊連接按鈕后,會自動跳轉到知識庫中的檢索測試。', + end: '.然後找到對應的知識 ID 並在左側的表單中填寫。如果資訊全部正確,點擊連接按鈕后,會自動跳轉到知識庫中的檢索測試。', }, title: '如何連接到外部知識庫', learnMore: '瞭解更多資訊', @@ -138,7 +138,7 @@ const translation = { allExternalTip: '僅使用外部知識時,用戶可以選擇是否啟用 Rerank 模型。如果未啟用,則檢索到的數據塊將根據分數進行排序。當不同知識庫的檢索策略不一致時,就會不準確。', externalKnowledgeIdPlaceholder: '請輸入 Knowledge ID', editExternalAPIFormTitle: '編輯外部知識 API', - externalKnowledgeId: '外部知識ID', + externalKnowledgeId: '外部知識 ID', externalAPIPanelDescription: '外部知識 API 用於連接到 Dify 外部的知識庫,並從該知識庫中檢索知識。', externalAPI: '外部 API', editExternalAPITooltipTitle: '關聯知識', diff --git a/web/i18n/zh-Hant/education.ts b/web/i18n/zh-Hant/education.ts index 9637324b85..8efc70f4d6 100644 --- a/web/i18n/zh-Hant/education.ts +++ b/web/i18n/zh-Hant/education.ts @@ -26,14 +26,14 @@ const translation = { end: '透過提交:', }, option: { - age: '我確認我至少18歲', + age: '我確認我至少 18 歲', inSchool: '我確認我已在所提供的機構註冊或受僱。Dify 可能會要求提供註冊/就業的證明。如果我錯誤表述我的資格,我同意支付根據我的教育狀況最初免除的任何費用。', }, title: '條款與協議', }, }, - rejectContent: '不幸的是,您不符合教育驗證狀態,因此如果您使用此電子郵件地址,將無法獲得Dify專業計劃的100%獨家優惠券。', - successContent: '我們已經向您的帳戶發放了Dify專業計劃的100%折扣優惠券。該優惠券有效期為一年,請在有效期內使用它。', + rejectContent: '不幸的是,您不符合教育驗證狀態,因此如果您使用此電子郵件地址,將無法獲得 Dify 專業計劃的 100% 獨家優惠券。', + successContent: '我們已經向您的帳戶發放了 Dify 專業計劃的 100% 折扣優惠券。該優惠券有效期為一年,請在有效期內使用它。', learn: '了解如何進行教育驗證', rejectTitle: '您的 Dify 教育驗證已被拒絕', submitError: '表單提交失敗。請稍後再試。', diff --git a/web/i18n/zh-Hant/login.ts b/web/i18n/zh-Hant/login.ts index ada0e1bf89..a928b3b800 100644 --- a/web/i18n/zh-Hant/login.ts +++ b/web/i18n/zh-Hant/login.ts @@ -52,12 +52,12 @@ const translation = { emailInValid: '請輸入有效的郵箱地址', nameEmpty: '使用者名稱不能為空', passwordEmpty: '密碼不能為空', - passwordInvalid: '密碼必須包含字母和數字,且長度不小於8位', - passwordLengthInValid: '密碼必須至少為8個字元', + passwordInvalid: '密碼必須包含字母和數字,且長度不小於 8 位', + passwordLengthInValid: '密碼必須至少為 8 個字元', registrationNotAllowed: '找不到帳戶。請聯繫系統管理員進行註冊。', }, license: { - tip: '啟動 Dify 社群版之前, 請閱讀 GitHub 上的', + tip: '啟動 Dify 社群版之前,請閱讀 GitHub 上的', link: '開源協議', }, join: '加入', @@ -78,7 +78,7 @@ const translation = { emptyCode: '驗證碼是必需的', checkYourEmail: '檢查您的電子郵件', tips: '我們將驗證碼發送到 <strong>{{email}}</strong>', - verificationCodePlaceholder: '輸入6位代碼', + verificationCodePlaceholder: '輸入 6 位代碼', useAnotherMethod: '使用其他方法', validTime: '請記住,該代碼的有效期為 5 分鐘', verificationCode: '驗證碼', diff --git a/web/i18n/zh-Hant/plugin.ts b/web/i18n/zh-Hant/plugin.ts index 3b9040dc91..4d4e6acf2e 100644 --- a/web/i18n/zh-Hant/plugin.ts +++ b/web/i18n/zh-Hant/plugin.ts @@ -26,7 +26,7 @@ const translation = { source: { marketplace: '市場', local: '本地包檔', - github: 'GitHub的', + github: 'GitHub 的', }, detailPanel: { categoryTip: { @@ -45,7 +45,7 @@ const translation = { checkUpdate: '檢查更新', }, toolSelector: { - uninstalledContent: '此外掛程式是從local/GitHub儲存庫安裝的。請在安裝後使用。', + uninstalledContent: '此外掛程式是從 local/GitHub 儲存庫安裝的。請在安裝後使用。', descriptionLabel: '工具描述', params: '推理配置', paramsTip2: '當 \'Automatic\' 關閉時,使用預設值。', @@ -181,7 +181,7 @@ const translation = { viewMore: '查看更多', difyMarketplace: 'Dify 市場', pluginsResult: '{{num}} 個結果', - verifiedTip: '由Dify驗證', + verifiedTip: '由 Dify 驗證', partnerTip: '由 Dify 合作夥伴驗證', }, task: { diff --git a/web/i18n/zh-Hant/share-app.ts b/web/i18n/zh-Hant/share-app.ts index bf81582d58..e25aa0c0de 100644 --- a/web/i18n/zh-Hant/share-app.ts +++ b/web/i18n/zh-Hant/share-app.ts @@ -66,13 +66,16 @@ const translation = { empty: '上傳檔案的內容不能為空', fileStructNotMatch: '上傳檔案的內容與結構不匹配', emptyLine: '第 {{rowIndex}} 行的內容為空', - invalidLine: '第 {{rowIndex}} 行: {{varName}}值必填', - moreThanMaxLengthLine: '第 {{rowIndex}} 行: {{varName}}值超過最大長度 {{maxLength}}', + invalidLine: '第 {{rowIndex}} 行:{{varName}}值必填', + moreThanMaxLengthLine: '第 {{rowIndex}} 行:{{varName}}值超過最大長度 {{maxLength}}', atLeastOne: '上傳檔案的內容不能少於一條', }, execution: '執行', executions: '{{num}} 執行', }, + login: { + backToHome: '返回首頁', + }, } export default translation diff --git a/web/i18n/zh-Hant/tools.ts b/web/i18n/zh-Hant/tools.ts index 5669e87cb3..6e5a95f2a5 100644 --- a/web/i18n/zh-Hant/tools.ts +++ b/web/i18n/zh-Hant/tools.ts @@ -35,8 +35,8 @@ const translation = { urlError: '請輸入有效的 URL', examples: '例子', exampleOptions: { - json: '天氣(JSON)', - yaml: '寵物商店(YAML)', + json: '天氣 (JSON)', + yaml: '寵物商店 (YAML)', blankTemplate: '空白模版', }, availableTools: { @@ -91,7 +91,7 @@ const translation = { }, description: '描述', nameForToolCall: '工具調用名稱', - confirmTitle: '確認儲存 ?', + confirmTitle: '確認儲存?', descriptionPlaceholder: '工具用途的簡要描述,例如,獲取特定位置的溫度。', nameForToolCallTip: '僅支援數位、字母和下劃線。', confirmTip: '使用此工具的應用程式將受到影響', @@ -123,7 +123,7 @@ const translation = { file: '檔', }, noCustomTool: { - title: '沒有自定義工具!', + title: '沒有自定義工具!', content: '在此統一新增和管理你的自定義工具,方便構建應用時使用。', createTool: '建立工具', }, diff --git a/web/i18n/zh-Hant/workflow.ts b/web/i18n/zh-Hant/workflow.ts index e3d4d8360d..79f164f0f0 100644 --- a/web/i18n/zh-Hant/workflow.ts +++ b/web/i18n/zh-Hant/workflow.ts @@ -51,7 +51,7 @@ const translation = { processData: '數據處理', input: '輸入', output: '輸出', - jinjaEditorPlaceholder: '輸入 “/” 或 “{” 插入變量', + jinjaEditorPlaceholder: '輸入“/”或“{”插入變量', viewOnly: '只讀', showRunHistory: '顯示運行歷史', enableJinja: '開啟支持 Jinja 模板', @@ -73,7 +73,7 @@ const translation = { backupCurrentDraft: 'Backup Current Draft', overwriteAndImport: '覆蓋和導入', importSuccess: '導入成功', - chooseDSL: '選擇 DSL(yml) 檔', + chooseDSL: '選擇 DSL(yml)檔', syncingData: '同步數據,只需幾秒鐘。', importDSLTip: '當前草稿將被覆蓋。在導入之前將工作流匯出為備份。', importFailure: '匯入失敗', @@ -108,17 +108,17 @@ const translation = { noHistory: '無歷史記錄', publishUpdate: '發布更新', referenceVar: '參考變量', - exportSVG: '匯出為SVG', + exportSVG: '匯出為 SVG', exportPNG: '匯出為 PNG', noExist: '沒有這個變數', versionHistory: '版本歷史', exitVersions: '退出版本', exportImage: '匯出圖像', - exportJPEG: '匯出為JPEG', + exportJPEG: '匯出為 JPEG', }, env: { envPanelTitle: '環境變數', - envDescription: '環境變數可用於存儲私人信息和憑證。它們是唯讀的,並且可以在導出時與DSL文件分開。', + envDescription: '環境變數可用於存儲私人信息和憑證。它們是唯讀的,並且可以在導出時與 DSL 文件分開。', envPanelButton: '添加變數', modal: { title: '添加環境變數', @@ -128,13 +128,13 @@ const translation = { namePlaceholder: '環境名稱', value: '值', valuePlaceholder: '環境值', - secretTip: '用於定義敏感信息或數據,DSL設置配置為防止洩露。', + secretTip: '用於定義敏感信息或數據,DSL 設置配置為防止洩露。', }, export: { title: '導出機密環境變數?', checkbox: '導出機密值', - ignore: '導出DSL', - export: '導出帶有機密值的DSL', + ignore: '導出 DSL', + export: '導出帶有機密值的 DSL', }, }, chatVariable: { @@ -204,7 +204,7 @@ const translation = { }, invalidVariable: '無效的變量', rerankModelRequired: '在開啟 Rerank 模型之前,請在設置中確認模型配置成功。', - toolParameterRequired: '{{field}}: 参數 [{{param}}] 為必填項', + toolParameterRequired: '{{field}}:参數 [{{param}}] 為必填項', noValidTool: '{{field}} 未選擇有效工具', }, singleRun: { @@ -336,9 +336,9 @@ const translation = { failBranch: { title: '失敗分支', desc: '當發生錯誤時,它會執行 exception 分支', - customize: '轉到畫布以自定義fail分支邏輯。', - inLog: 'Node 異常,將自動執行fail分支。節點輸出將返回錯誤類型和錯誤消息,並將其傳遞給下游。', - customizeTip: '啟動fail分支後,節點引發的異常不會終止進程。相反,它將自動執行預定義的fail分支,允許您靈活地提供錯誤消息、報告、修復或跳過操作。', + customize: '轉到畫布以自定義 fail 分支邏輯。', + inLog: 'Node 異常,將自動執行 fail 分支。節點輸出將返回錯誤類型和錯誤消息,並將其傳遞給下游。', + customizeTip: '啟動 fail 分支後,節點引發的異常不會終止進程。相反,它將自動執行預定義的 fail 分支,允許您靈活地提供錯誤消息、報告、修復或跳過操作。', }, partialSucceeded: { tip: '進程中有 {{num}} 個節點運行異常,請前往 tracing 查看日誌。', @@ -436,7 +436,7 @@ const translation = { import: '從 JSON 匯入', generatedResult: '生成的結果', generateJsonSchema: '生成 JSON 架構', - promptTooltip: '將文本描述轉換成標準化的 JSON Schema結構。', + promptTooltip: '將文本描述轉換成標準化的 JSON Schema 結構。', doc: '了解更多有關結構化輸出的資訊', addChildField: '新增子欄位', title: '結構化輸出模式', @@ -643,7 +643,7 @@ const translation = { 'variables': '變數', 'selectAssignedVariable': '選擇配置的變數...', 'setParameter': '設定參數...', - 'noVarTip': '點擊 「+」 按鈕添加變數', + 'noVarTip': '點擊「+」按鈕添加變數', 'assignedVarsDescription': '分配的變數必須是可寫變數,例如對話變數。', 'varNotSet': '未設置變數', }, @@ -657,9 +657,9 @@ const translation = { type: '支持類型。現在只支持圖片', transfer_method: '傳輸方式。值為 remote_url 或 local_file', url: '圖片鏈接', - upload_file_id: '上傳文件ID', + upload_file_id: '上傳文件 ID', }, - json: '工具生成的JSON', + json: '工具生成的 JSON', }, }, questionClassifiers: { @@ -762,13 +762,13 @@ const translation = { result: '篩選結果', }, desc: '描述', - asc: 'ASC的', + asc: 'ASC 的', orderBy: '排序依據', inputVar: '輸入變數', filterConditionComparisonValue: 'Filter Condition 值', filterCondition: '篩選條件', limit: '前 N 名', - selectVariableKeyPlaceholder: 'Select sub variable key (選擇子變數鍵)', + selectVariableKeyPlaceholder: 'Select sub variable key(選擇子變數鍵)', filterConditionComparisonOperator: 'Filter Condition Comparison 運算符', filterConditionKey: '篩選條件鍵', extractsCondition: '提取第 N 項', @@ -806,7 +806,7 @@ const translation = { transfer_method: '轉移方法。值為 remote_url 或 local_file', title: '代理生成的檔', url: '圖片網址', - upload_file_id: '上傳檔ID', + upload_file_id: '上傳檔 ID', }, text: '代理生成的內容', json: '代理生成的 JSON', diff --git a/web/models/access-control.ts b/web/models/access-control.ts index 8ad9cc6491..911662b5c4 100644 --- a/web/models/access-control.ts +++ b/web/models/access-control.ts @@ -7,6 +7,7 @@ export enum AccessMode { PUBLIC = 'public', SPECIFIC_GROUPS_MEMBERS = 'private', ORGANIZATION = 'private_all', + EXTERNAL_MEMBERS = 'sso_verified', } export type AccessControlGroup = { diff --git a/web/package.json b/web/package.json index affbef9382..ff4214f966 100644 --- a/web/package.json +++ b/web/package.json @@ -5,6 +5,18 @@ "engines": { "node": ">=v22.11.0" }, + "browserslist": [ + "last 1 Chrome version", + "last 1 Firefox version", + "last 1 Edge version", + "last 1 Safari version", + "iOS >=15", + "Android >= 10", + "and_chr >= 126", + "and_ff >= 137", + "and_uc >= 15.5", + "and_qq >= 14.9" + ], "scripts": { "dev": "cross-env NODE_OPTIONS='--inspect' next dev", "build": "next build", diff --git a/web/service/base.ts b/web/service/base.ts index 4b08736288..ba398c07a6 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -108,11 +108,13 @@ function unicodeToChar(text: string) { }) } -function requiredWebSSOLogin(message?: string) { +function requiredWebSSOLogin(message?: string, code?: number) { const params = new URLSearchParams() params.append('redirect_url', globalThis.location.pathname) if (message) params.append('message', message) + if (code) + params.append('code', String(code)) globalThis.location.href = `/webapp-signin?${params.toString()}` } @@ -402,10 +404,12 @@ export const ssePost = async ( res.json().then((data: any) => { if (isPublicAPI) { if (data.code === 'web_app_access_denied') - requiredWebSSOLogin(data.message) + requiredWebSSOLogin(data.message, 403) - if (data.code === 'web_sso_auth_required') + if (data.code === 'web_sso_auth_required') { + removeAccessToken() requiredWebSSOLogin() + } if (data.code === 'unauthorized') { removeAccessToken() @@ -483,10 +487,11 @@ export const request = async<T>(url: string, options = {}, otherOptions?: IOther const { code, message } = errRespData // webapp sso if (code === 'web_app_access_denied') { - requiredWebSSOLogin(message) + requiredWebSSOLogin(message, 403) return Promise.reject(err) } if (code === 'web_sso_auth_required') { + removeAccessToken() requiredWebSSOLogin() return Promise.reject(err) } diff --git a/web/service/common.ts b/web/service/common.ts index e76cfb4196..700cd4bf51 100644 --- a/web/service/common.ts +++ b/web/service/common.ts @@ -52,6 +52,9 @@ type LoginResponse = LoginSuccess | LoginFail export const login: Fetcher<LoginResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => { return post(url, { body }) as Promise<LoginResponse> } +export const webAppLogin: Fetcher<LoginResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => { + return post(url, { body }, { isPublicAPI: true }) as Promise<LoginResponse> +} export const fetchNewToken: Fetcher<CommonResponse & { data: { access_token: string; refresh_token: string } }, { body: Record<string, any> }> = ({ body }) => { return post('/refresh-token', { body }) as Promise<CommonResponse & { data: { access_token: string; refresh_token: string } }> @@ -324,6 +327,16 @@ export const verifyForgotPasswordToken: Fetcher<CommonResponse & { is_valid: boo export const changePasswordWithToken: Fetcher<CommonResponse, { url: string; body: { token: string; new_password: string; password_confirm: string } }> = ({ url, body }) => post<CommonResponse>(url, { body }) +export const sendWebAppForgotPasswordEmail: Fetcher<CommonResponse & { data: string }, { url: string; body: { email: string } }> = ({ url, body }) => + post<CommonResponse & { data: string }>(url, { body }, { isPublicAPI: true }) + +export const verifyWebAppForgotPasswordToken: Fetcher<CommonResponse & { is_valid: boolean; email: string }, { url: string; body: { token: string } }> = ({ url, body }) => { + return post(url, { body }, { isPublicAPI: true }) as Promise<CommonResponse & { is_valid: boolean; email: string }> +} + +export const changeWebAppPasswordWithToken: Fetcher<CommonResponse, { url: string; body: { token: string; new_password: string; password_confirm: string } }> = ({ url, body }) => + post<CommonResponse>(url, { body }, { isPublicAPI: true }) + export const uploadRemoteFileInfo = (url: string, isPublic?: boolean) => { return post<{ id: string; name: string; size: number; mime_type: string; url: string }>('/remote-files/upload', { body: { url } }, { isPublicAPI: isPublic }) } @@ -340,6 +353,18 @@ export const sendResetPasswordCode = (email: string, language = 'en-US') => export const verifyResetPasswordCode = (body: { email: string; code: string; token: string }) => post<CommonResponse & { is_valid: boolean; token: string }>('/forgot-password/validity', { body }) +export const sendWebAppEMailLoginCode = (email: string, language = 'en-US') => + post<CommonResponse & { data: string }>('/email-code-login', { body: { email, language } }, { isPublicAPI: true }) + +export const webAppEmailLoginWithCode = (data: { email: string; code: string; token: string }) => + post<LoginResponse>('/email-code-login/validity', { body: data }, { isPublicAPI: true }) + +export const sendWebAppResetPasswordCode = (email: string, language = 'en-US') => + post<CommonResponse & { data: string; message?: string; code?: string }>('/forgot-password', { body: { email, language } }, { isPublicAPI: true }) + +export const verifyWebAppResetPasswordCode = (body: { email: string; code: string; token: string }) => + post<CommonResponse & { is_valid: boolean; token: string }>('/forgot-password/validity', { body }, { isPublicAPI: true }) + export const sendDeleteAccountCode = () => get<CommonResponse & { data: string }>('/account/delete/verify') diff --git a/web/service/share.ts b/web/service/share.ts index 7fb1562185..6a2a7e5b16 100644 --- a/web/service/share.ts +++ b/web/service/share.ts @@ -214,6 +214,34 @@ export const fetchWebOAuth2SSOUrl = async (appCode: string, redirectUrl: string) }) as Promise<{ url: string }> } +export const fetchMembersSAMLSSOUrl = async (appCode: string, redirectUrl: string) => { + return (getAction('get', false))(getUrl('/enterprise/sso/members/saml/login', false, ''), { + params: { + app_code: appCode, + redirect_url: redirectUrl, + }, + }) as Promise<{ url: string }> +} + +export const fetchMembersOIDCSSOUrl = async (appCode: string, redirectUrl: string) => { + return (getAction('get', false))(getUrl('/enterprise/sso/members/oidc/login', false, ''), { + params: { + app_code: appCode, + redirect_url: redirectUrl, + }, + + }) as Promise<{ url: string }> +} + +export const fetchMembersOAuth2SSOUrl = async (appCode: string, redirectUrl: string) => { + return (getAction('get', false))(getUrl('/enterprise/sso/members/oauth2/login', false, ''), { + params: { + app_code: appCode, + redirect_url: redirectUrl, + }, + }) as Promise<{ url: string }> +} + export const fetchAppMeta = async (isInstalledApp: boolean, installedAppId = '') => { return (getAction('get', isInstalledApp))(getUrl('meta', isInstalledApp, installedAppId)) as Promise<AppMeta> } @@ -258,10 +286,13 @@ export const textToAudioStream = (url: string, isPublicAPI: boolean, header: { c return (getAction('post', !isPublicAPI))(url, { body, header }, { needAllResponseContent: true }) } -export const fetchAccessToken = async (appCode: string, userId?: string) => { +export const fetchAccessToken = async ({ appCode, userId, webAppAccessToken }: { appCode: string, userId?: string, webAppAccessToken?: string | null }) => { const headers = new Headers() headers.append('X-App-Code', appCode) - const url = userId ? `/passport?user_id=${encodeURIComponent(userId)}` : '/passport' + const params = new URLSearchParams() + webAppAccessToken && params.append('web_app_access_token', webAppAccessToken) + userId && params.append('user_id', userId) + const url = `/passport?${params.toString()}` return get(url, { headers }) as Promise<{ access_token: string }> } @@ -278,3 +309,7 @@ export const getUserCanAccess = (appId: string, isInstalledApp: boolean) => { return get<{ result: boolean }>(`/webapp/permission?appId=${appId}`) } + +export const getAppAccessModeByAppCode = (appCode: string) => { + return get<{ accessMode: AccessMode }>(`/webapp/access-mode?appCode=${appCode}`) +} diff --git a/web/service/use-share.ts b/web/service/use-share.ts new file mode 100644 index 0000000000..b8f96f6cc5 --- /dev/null +++ b/web/service/use-share.ts @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query' +import { getAppAccessModeByAppCode } from './share' + +const NAME_SPACE = 'webapp' + +export const useAppAccessModeByCode = (code: string | null) => { + return useQuery({ + queryKey: [NAME_SPACE, 'appAccessMode', code], + queryFn: () => { + if (!code) + return null + + return getAppAccessModeByAppCode(code) + }, + enabled: !!code, + }) +}