Merge remote-tracking branch 'origin/main' into feat/queue-based-graph-engine

This commit is contained in:
-LAN- 2025-09-03 01:33:17 +08:00
commit 77a9a73d0d
No known key found for this signature in database
GPG Key ID: 6BA0D108DED011FF
115 changed files with 5875 additions and 268 deletions

View File

@ -42,11 +42,7 @@ jobs:
- name: Run Unit tests
run: |
uv run --project api bash dev/pytest/pytest_unit_tests.sh
- name: Run ty check
run: |
cd api
uv add --dev ty
uv run ty check || true
- name: Run pyrefly check
run: |
cd api

View File

@ -44,6 +44,10 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true'
run: uv sync --project api --dev
- name: Run ty check
if: steps.changed-files.outputs.any_changed == 'true'
run: dev/ty-check
- name: Dotenv check
if: steps.changed-files.outputs.any_changed == 'true'
run: uv run --project api dotenv-linter ./api/.env.example ./web/.env.example

View File

@ -4,6 +4,48 @@ WEB_IMAGE=$(DOCKER_REGISTRY)/dify-web
API_IMAGE=$(DOCKER_REGISTRY)/dify-api
VERSION=latest
# Backend Development Environment Setup
.PHONY: dev-setup prepare-docker prepare-web prepare-api
# Default dev setup target
dev-setup: prepare-docker prepare-web prepare-api
@echo "✅ Backend development environment setup complete!"
# Step 1: Prepare Docker middleware
prepare-docker:
@echo "🐳 Setting up Docker middleware..."
@cp -n docker/middleware.env.example docker/middleware.env 2>/dev/null || echo "Docker middleware.env already exists"
@cd docker && docker compose -f docker-compose.middleware.yaml --env-file middleware.env -p dify-middlewares-dev up -d
@echo "✅ Docker middleware started"
# Step 2: Prepare web environment
prepare-web:
@echo "🌐 Setting up web environment..."
@cp -n web/.env.example web/.env 2>/dev/null || echo "Web .env already exists"
@cd web && pnpm install
@cd web && pnpm build
@echo "✅ Web environment prepared (not started)"
# Step 3: Prepare API environment
prepare-api:
@echo "🔧 Setting up API environment..."
@cp -n api/.env.example api/.env 2>/dev/null || echo "API .env already exists"
@cd api && uv sync --dev --extra all
@cd api && uv run flask db upgrade
@echo "✅ API environment prepared (not started)"
# Clean dev environment
dev-clean:
@echo "⚠️ Stopping Docker containers..."
@cd docker && docker compose -f docker-compose.middleware.yaml --env-file middleware.env -p dify-middlewares-dev down
@echo "🗑️ Removing volumes..."
@rm -rf docker/volumes/db
@rm -rf docker/volumes/redis
@rm -rf docker/volumes/plugin_daemon
@rm -rf docker/volumes/weaviate
@rm -rf api/storage
@echo "✅ Cleanup complete"
# Build Docker images
build-web:
@echo "Building web Docker image: $(WEB_IMAGE):$(VERSION)..."
@ -39,5 +81,21 @@ build-push-web: build-web push-web
build-push-all: build-all push-all
@echo "All Docker images have been built and pushed."
# Help target
help:
@echo "Development Setup Targets:"
@echo " make dev-setup - Run all setup steps for backend dev environment"
@echo " make prepare-docker - Set up Docker middleware"
@echo " make prepare-web - Set up web environment"
@echo " make prepare-api - Set up API environment"
@echo " make dev-clean - Stop Docker middleware containers"
@echo ""
@echo "Docker Build Targets:"
@echo " make build-web - Build web Docker image"
@echo " make build-api - Build API Docker image"
@echo " make build-all - Build all Docker images"
@echo " make push-all - Push all Docker images"
@echo " make build-push-all - Build and push all Docker images"
# Phony targets
.PHONY: build-web build-api push-web push-api build-all push-all build-push-all
.PHONY: build-web build-api push-web push-api build-all push-all build-push-all dev-setup prepare-docker prepare-web prepare-api dev-clean help

View File

@ -27,7 +27,7 @@ class NacosHttpClient:
response = requests.request(method, url="http://" + self.server + url, headers=headers, params=params)
response.raise_for_status()
return response.text
except requests.exceptions.RequestException as e:
except requests.RequestException as e:
return f"Request to Nacos failed: {e}"
def _inject_auth_info(self, headers, params, module="config"):

View File

@ -19,6 +19,7 @@ language_timezone_mapping = {
"fa-IR": "Asia/Tehran",
"sl-SI": "Europe/Ljubljana",
"th-TH": "Asia/Bangkok",
"id-ID": "Asia/Jakarta",
}
languages = list(language_timezone_mapping.keys())

View File

@ -130,15 +130,19 @@ class InsertExploreAppApi(Resource):
app.is_public = False
with Session(db.engine) as session:
installed_apps = session.execute(
select(InstalledApp).where(
InstalledApp.app_id == recommended_app.app_id,
InstalledApp.tenant_id != InstalledApp.app_owner_tenant_id,
installed_apps = (
session.execute(
select(InstalledApp).where(
InstalledApp.app_id == recommended_app.app_id,
InstalledApp.tenant_id != InstalledApp.app_owner_tenant_id,
)
)
).all()
.scalars()
.all()
)
for installed_app in installed_apps:
db.session.delete(installed_app)
for installed_app in installed_apps:
session.delete(installed_app)
db.session.delete(recommended_app)
db.session.commit()

View File

@ -84,7 +84,7 @@ class BaseApiKeyListResource(Resource):
flask_restx.abort(
400,
message=f"Cannot create more than {self.max_keys} API keys for this resource type.",
code="max_keys_exceeded",
custom="max_keys_exceeded",
)
key = ApiToken.generate_api_key(self.token_prefix, 24)

View File

@ -237,9 +237,14 @@ class AppExportApi(Resource):
# Add include_secret params
parser = reqparse.RequestParser()
parser.add_argument("include_secret", type=inputs.boolean, default=False, location="args")
parser.add_argument("workflow_id", type=str, location="args")
args = parser.parse_args()
return {"data": AppDslService.export_dsl(app_model=app_model, include_secret=args["include_secret"])}
return {
"data": AppDslService.export_dsl(
app_model=app_model, include_secret=args["include_secret"], workflow_id=args.get("workflow_id")
)
}
class AppNameApi(Resource):

View File

@ -27,7 +27,9 @@ class WorkflowAppLogApi(Resource):
"""
parser = reqparse.RequestParser()
parser.add_argument("keyword", type=str, location="args")
parser.add_argument("status", type=str, choices=["succeeded", "failed", "stopped"], location="args")
parser.add_argument(
"status", type=str, choices=["succeeded", "failed", "stopped", "partial-succeeded"], location="args"
)
parser.add_argument(
"created_at__before", type=str, location="args", help="Filter logs created before this timestamp"
)

View File

@ -81,7 +81,7 @@ class OAuthDataSourceBinding(Resource):
return {"error": "Invalid code"}, 400
try:
oauth_provider.get_access_token(code)
except requests.exceptions.HTTPError as e:
except requests.HTTPError as e:
logger.exception(
"An error occurred during the OAuthCallback process with %s: %s", provider, e.response.text
)
@ -104,7 +104,7 @@ class OAuthDataSourceSync(Resource):
return {"error": "Invalid provider"}, 400
try:
oauth_provider.sync_data_source(binding_id)
except requests.exceptions.HTTPError as e:
except requests.HTTPError as e:
logger.exception(
"An error occurred during the OAuthCallback process with %s: %s", provider, e.response.text
)

View File

@ -80,7 +80,7 @@ class OAuthCallback(Resource):
try:
token = oauth_provider.get_access_token(code)
user_info = oauth_provider.get_user_info(token)
except requests.exceptions.RequestException as e:
except requests.RequestException as e:
error_text = e.response.text if e.response else str(e)
logger.exception("An error occurred during the OAuth process with %s: %s", provider, error_text)
return {"error": "OAuth process failed"}, 400

View File

@ -55,7 +55,7 @@ class AudioApi(Resource):
file = request.files["file"]
try:
response = AudioService.transcript_asr(app_model=app_model, file=file, end_user=end_user)
response = AudioService.transcript_asr(app_model=app_model, file=file, end_user=end_user.id)
return response
except services.errors.app_model_config.AppModelConfigBrokenError:

View File

@ -291,27 +291,28 @@ def create_or_update_end_user_for_user_id(app_model: App, user_id: Optional[str]
if not user_id:
user_id = "DEFAULT-USER"
end_user = (
db.session.query(EndUser)
.where(
EndUser.tenant_id == app_model.tenant_id,
EndUser.app_id == app_model.id,
EndUser.session_id == user_id,
EndUser.type == "service_api",
with Session(db.engine, expire_on_commit=False) as session:
end_user = (
session.query(EndUser)
.where(
EndUser.tenant_id == app_model.tenant_id,
EndUser.app_id == app_model.id,
EndUser.session_id == user_id,
EndUser.type == "service_api",
)
.first()
)
.first()
)
if end_user is None:
end_user = EndUser(
tenant_id=app_model.tenant_id,
app_id=app_model.id,
type="service_api",
is_anonymous=user_id == "DEFAULT-USER",
session_id=user_id,
)
db.session.add(end_user)
db.session.commit()
if end_user is None:
end_user = EndUser(
tenant_id=app_model.tenant_id,
app_id=app_model.id,
type="service_api",
is_anonymous=user_id == "DEFAULT-USER",
session_id=user_id,
)
session.add(end_user)
session.commit()
return end_user

View File

@ -4,6 +4,7 @@ from functools import wraps
from flask import request
from flask_restx import Resource
from sqlalchemy import select
from sqlalchemy.orm import Session
from werkzeug.exceptions import BadRequest, NotFound, Unauthorized
from controllers.web.error import WebAppAuthAccessDeniedError, WebAppAuthRequiredError
@ -49,18 +50,19 @@ def decode_jwt_token():
decoded = PassportService().verify(tk)
app_code = decoded.get("app_code")
app_id = decoded.get("app_id")
app_model = db.session.scalar(select(App).where(App.id == app_id))
site = db.session.scalar(select(Site).where(Site.code == app_code))
if not app_model:
raise NotFound()
if not app_code or not site:
raise BadRequest("Site URL is no longer valid.")
if app_model.enable_site is False:
raise BadRequest("Site is disabled.")
end_user_id = decoded.get("end_user_id")
end_user = db.session.scalar(select(EndUser).where(EndUser.id == end_user_id))
if not end_user:
raise NotFound()
with Session(db.engine, expire_on_commit=False) as session:
app_model = session.scalar(select(App).where(App.id == app_id))
site = session.scalar(select(Site).where(Site.code == app_code))
if not app_model:
raise NotFound()
if not app_code or not site:
raise BadRequest("Site URL is no longer valid.")
if app_model.enable_site is False:
raise BadRequest("Site is disabled.")
end_user_id = decoded.get("end_user_id")
end_user = session.scalar(select(EndUser).where(EndUser.id == end_user_id))
if not end_user:
raise NotFound()
# for enterprise webapp auth
app_web_auth_enabled = False

View File

@ -453,7 +453,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
# release database connection, because the following new thread operations may take a long time
db.session.refresh(workflow)
db.session.refresh(message)
db.session.refresh(user)
# db.session.refresh(user)
db.session.close()
# return response or stream generator

View File

@ -118,7 +118,7 @@ class AdvancedChatAppGenerateResponseConverter(AppGenerateResponseConverter):
data = cls._error_to_stream_response(sub_stream_response.err)
response_chunk.update(data)
elif isinstance(sub_stream_response, NodeStartStreamResponse | NodeFinishStreamResponse):
response_chunk.update(sub_stream_response.to_ignore_detail_dict())
response_chunk.update(sub_stream_response.to_ignore_detail_dict()) # ty: ignore [unresolved-attribute]
else:
response_chunk.update(sub_stream_response.to_dict())

View File

@ -89,7 +89,7 @@ class WorkflowAppGenerateResponseConverter(AppGenerateResponseConverter):
data = cls._error_to_stream_response(sub_stream_response.err)
response_chunk.update(data)
elif isinstance(sub_stream_response, NodeStartStreamResponse | NodeFinishStreamResponse):
response_chunk.update(sub_stream_response.to_ignore_detail_dict())
response_chunk.update(sub_stream_response.to_ignore_detail_dict()) # ty: ignore [unresolved-attribute]
else:
response_chunk.update(sub_stream_response.to_dict())
yield response_chunk

View File

@ -96,7 +96,11 @@ class RateLimit:
if isinstance(generator, Mapping):
return generator
else:
return RateLimitGenerator(rate_limit=self, generator=generator, request_id=request_id)
return RateLimitGenerator(
rate_limit=self,
generator=generator, # ty: ignore [invalid-argument-type]
request_id=request_id,
)
class RateLimitGenerator:

View File

@ -50,7 +50,7 @@ class BasedGenerateTaskPipeline:
if isinstance(e, InvokeAuthorizationError):
err = InvokeAuthorizationError("Incorrect API key provided")
elif isinstance(e, InvokeError | ValueError):
err = e
err = e # ty: ignore [invalid-assignment]
else:
description = getattr(e, "description", None)
err = Exception(description if description is not None else str(e))

View File

@ -43,9 +43,9 @@ class APIBasedExtensionRequestor:
timeout=self.timeout,
proxies=proxies,
)
except requests.exceptions.Timeout:
except requests.Timeout:
raise ValueError("request timeout")
except requests.exceptions.ConnectionError:
except requests.ConnectionError:
raise ValueError("request connection error")
if response.status_code != 200:

View File

@ -47,7 +47,7 @@ def get_subclasses_from_module(mod: ModuleType, parent_type: type) -> list[type]
def load_single_subclass_from_source(
*, module_name: str, script_path: AnyStr, parent_type: type, use_lazy_loader: bool = False
*, module_name: str, script_path: str, parent_type: type, use_lazy_loader: bool = False
) -> type:
"""
Load a single subclass from the source

View File

@ -57,11 +57,8 @@ class LLMGenerator:
prompts = [UserPromptMessage(content=prompt)]
with measure_time() as timer:
response = cast(
LLMResult,
model_instance.invoke_llm(
prompt_messages=list(prompts), model_parameters={"max_tokens": 500, "temperature": 1}, stream=False
),
response: LLMResult = model_instance.invoke_llm(
prompt_messages=list(prompts), model_parameters={"max_tokens": 500, "temperature": 1}, stream=False
)
answer = cast(str, response.message.content)
cleaned_answer = re.sub(r"^.*(\{.*\}).*$", r"\1", answer, flags=re.DOTALL)
@ -114,13 +111,10 @@ class LLMGenerator:
prompt_messages = [UserPromptMessage(content=prompt)]
try:
response = cast(
LLMResult,
model_instance.invoke_llm(
prompt_messages=list(prompt_messages),
model_parameters={"max_tokens": 256, "temperature": 0},
stream=False,
),
response: LLMResult = model_instance.invoke_llm(
prompt_messages=list(prompt_messages),
model_parameters={"max_tokens": 256, "temperature": 0},
stream=False,
)
text_content = response.message.get_text_content()
@ -163,11 +157,8 @@ class LLMGenerator:
)
try:
response = cast(
LLMResult,
model_instance.invoke_llm(
prompt_messages=list(prompt_messages), model_parameters=model_parameters, stream=False
),
response: LLMResult = model_instance.invoke_llm(
prompt_messages=list(prompt_messages), model_parameters=model_parameters, stream=False
)
rule_config["prompt"] = cast(str, response.message.content)
@ -213,11 +204,8 @@ class LLMGenerator:
try:
try:
# the first step to generate the task prompt
prompt_content = cast(
LLMResult,
model_instance.invoke_llm(
prompt_messages=list(prompt_messages), model_parameters=model_parameters, stream=False
),
prompt_content: LLMResult = model_instance.invoke_llm(
prompt_messages=list(prompt_messages), model_parameters=model_parameters, stream=False
)
except InvokeError as e:
error = str(e)
@ -249,11 +237,8 @@ class LLMGenerator:
statement_messages = [UserPromptMessage(content=statement_generate_prompt)]
try:
parameter_content = cast(
LLMResult,
model_instance.invoke_llm(
prompt_messages=list(parameter_messages), model_parameters=model_parameters, stream=False
),
parameter_content: LLMResult = model_instance.invoke_llm(
prompt_messages=list(parameter_messages), model_parameters=model_parameters, stream=False
)
rule_config["variables"] = re.findall(r'"\s*([^"]+)\s*"', cast(str, parameter_content.message.content))
except InvokeError as e:
@ -261,11 +246,8 @@ class LLMGenerator:
error_step = "generate variables"
try:
statement_content = cast(
LLMResult,
model_instance.invoke_llm(
prompt_messages=list(statement_messages), model_parameters=model_parameters, stream=False
),
statement_content: LLMResult = model_instance.invoke_llm(
prompt_messages=list(statement_messages), model_parameters=model_parameters, stream=False
)
rule_config["opening_statement"] = cast(str, statement_content.message.content)
except InvokeError as e:
@ -308,11 +290,8 @@ class LLMGenerator:
prompt_messages = [UserPromptMessage(content=prompt)]
model_parameters = model_config.get("completion_params", {})
try:
response = cast(
LLMResult,
model_instance.invoke_llm(
prompt_messages=list(prompt_messages), model_parameters=model_parameters, stream=False
),
response: LLMResult = model_instance.invoke_llm(
prompt_messages=list(prompt_messages), model_parameters=model_parameters, stream=False
)
generated_code = cast(str, response.message.content)
@ -339,13 +318,10 @@ class LLMGenerator:
prompt_messages = [SystemPromptMessage(content=prompt), UserPromptMessage(content=query)]
response = cast(
LLMResult,
model_instance.invoke_llm(
prompt_messages=prompt_messages,
model_parameters={"temperature": 0.01, "max_tokens": 2000},
stream=False,
),
response: LLMResult = model_instance.invoke_llm(
prompt_messages=prompt_messages,
model_parameters={"temperature": 0.01, "max_tokens": 2000},
stream=False,
)
answer = cast(str, response.message.content)
@ -368,11 +344,8 @@ class LLMGenerator:
model_parameters = model_config.get("model_parameters", {})
try:
response = cast(
LLMResult,
model_instance.invoke_llm(
prompt_messages=list(prompt_messages), model_parameters=model_parameters, stream=False
),
response: LLMResult = model_instance.invoke_llm(
prompt_messages=list(prompt_messages), model_parameters=model_parameters, stream=False
)
raw_content = response.message.content
@ -556,11 +529,8 @@ class LLMGenerator:
model_parameters = {"temperature": 0.4}
try:
response = cast(
LLMResult,
model_instance.invoke_llm(
prompt_messages=list(prompt_messages), model_parameters=model_parameters, stream=False
),
response: LLMResult = model_instance.invoke_llm(
prompt_messages=list(prompt_messages), model_parameters=model_parameters, stream=False
)
generated_raw = cast(str, response.message.content)

View File

@ -72,7 +72,7 @@ class TraceClient:
else:
logger.debug("AliyunTrace API check failed: Unexpected status code: %s", response.status_code)
return False
except requests.exceptions.RequestException as e:
except requests.RequestException as e:
logger.debug("AliyunTrace API check failed: %s", str(e))
raise ValueError(f"AliyunTrace API check failed: {str(e)}")

View File

@ -64,7 +64,7 @@ class BasePluginClient:
response = requests.request(
method=method, url=str(url), headers=headers, data=data, params=params, stream=stream, files=files
)
except requests.exceptions.ConnectionError:
except requests.ConnectionError:
logger.exception("Request to Plugin Daemon Service failed")
raise PluginDaemonInnerError(code=-500, message="Request to Plugin Daemon Service failed")

View File

@ -192,8 +192,8 @@ class AnalyticdbVectorOpenAPI:
collection=self._collection_name,
metrics=self.config.metrics,
include_values=True,
vector=None,
content=None,
vector=None, # ty: ignore [invalid-argument-type]
content=None, # ty: ignore [invalid-argument-type]
top_k=1,
filter=f"ref_doc_id='{id}'",
)
@ -211,7 +211,7 @@ class AnalyticdbVectorOpenAPI:
namespace=self.config.namespace,
namespace_password=self.config.namespace_password,
collection=self._collection_name,
collection_data=None,
collection_data=None, # ty: ignore [invalid-argument-type]
collection_data_filter=f"ref_doc_id IN {ids_str}",
)
self._client.delete_collection_data(request)
@ -225,7 +225,7 @@ class AnalyticdbVectorOpenAPI:
namespace=self.config.namespace,
namespace_password=self.config.namespace_password,
collection=self._collection_name,
collection_data=None,
collection_data=None, # ty: ignore [invalid-argument-type]
collection_data_filter=f"metadata_ ->> '{key}' = '{value}'",
)
self._client.delete_collection_data(request)
@ -249,7 +249,7 @@ class AnalyticdbVectorOpenAPI:
include_values=kwargs.pop("include_values", True),
metrics=self.config.metrics,
vector=query_vector,
content=None,
content=None, # ty: ignore [invalid-argument-type]
top_k=kwargs.get("top_k", 4),
filter=where_clause,
)
@ -285,7 +285,7 @@ class AnalyticdbVectorOpenAPI:
collection=self._collection_name,
include_values=kwargs.pop("include_values", True),
metrics=self.config.metrics,
vector=None,
vector=None, # ty: ignore [invalid-argument-type]
content=query,
top_k=kwargs.get("top_k", 4),
filter=where_clause,

View File

@ -12,7 +12,7 @@ import clickzetta # type: ignore
from pydantic import BaseModel, model_validator
if TYPE_CHECKING:
from clickzetta import Connection
from clickzetta.connector.v0.connection import Connection # type: ignore
from configs import dify_config
from core.rag.datasource.vdb.field import Field

View File

@ -306,7 +306,7 @@ class CouchbaseVector(BaseVector):
def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]:
top_k = kwargs.get("top_k", 4)
try:
CBrequest = search.SearchRequest.create(search.QueryStringQuery("text:" + query))
CBrequest = search.SearchRequest.create(search.QueryStringQuery("text:" + query)) # ty: ignore [too-many-positional-arguments]
search_iter = self._scope.search(
self._collection_name + "_search", CBrequest, SearchOptions(limit=top_k, fields=["*"])
)

View File

@ -138,7 +138,7 @@ class ElasticSearchVector(BaseVector):
if not client.ping():
raise ConnectionError("Failed to connect to Elasticsearch")
except requests.exceptions.ConnectionError as e:
except requests.ConnectionError as e:
raise ConnectionError(f"Vector database connection error: {str(e)}")
except Exception as e:
raise ConnectionError(f"Elasticsearch client initialization failed: {str(e)}")

View File

@ -376,7 +376,12 @@ class MilvusVector(BaseVector):
if config.token:
client = MilvusClient(uri=config.uri, token=config.token, db_name=config.database)
else:
client = MilvusClient(uri=config.uri, user=config.user, password=config.password, db_name=config.database)
client = MilvusClient(
uri=config.uri,
user=config.user or "",
password=config.password or "",
db_name=config.database,
)
return client

View File

@ -32,9 +32,9 @@ class VikingDBConfig(BaseModel):
scheme: str
connection_timeout: int
socket_timeout: int
index_type: str = IndexType.HNSW
distance: str = DistanceType.L2
quant: str = QuantType.Float
index_type: str = str(IndexType.HNSW)
distance: str = str(DistanceType.L2)
quant: str = str(QuantType.Float)
class VikingDBVector(BaseVector):

View File

@ -37,22 +37,22 @@ class WeaviateVector(BaseVector):
self._attributes = attributes
def _init_client(self, config: WeaviateConfig) -> weaviate.Client:
auth_config = weaviate.auth.AuthApiKey(api_key=config.api_key)
auth_config = weaviate.AuthApiKey(api_key=config.api_key or "")
weaviate.connect.connection.has_grpc = False
weaviate.connect.connection.has_grpc = False # ty: ignore [unresolved-attribute]
# Fix to minimize the performance impact of the deprecation check in weaviate-client 3.24.0,
# by changing the connection timeout to pypi.org from 1 second to 0.001 seconds.
# TODO: This can be removed once weaviate-client is updated to 3.26.7 or higher,
# which does not contain the deprecation check.
if hasattr(weaviate.connect.connection, "PYPI_TIMEOUT"):
weaviate.connect.connection.PYPI_TIMEOUT = 0.001
if hasattr(weaviate.connect.connection, "PYPI_TIMEOUT"): # ty: ignore [unresolved-attribute]
weaviate.connect.connection.PYPI_TIMEOUT = 0.001 # ty: ignore [unresolved-attribute]
try:
client = weaviate.Client(
url=config.endpoint, auth_client_secret=auth_config, timeout_config=(5, 60), startup_period=None
)
except requests.exceptions.ConnectionError:
except requests.ConnectionError:
raise ConnectionError("Vector database connection error")
client.batch.configure(

View File

@ -32,6 +32,7 @@ class BaseIndexProcessor(ABC):
def load(self, dataset: Dataset, documents: list[Document], with_keywords: bool = True, **kwargs):
raise NotImplementedError
@abstractmethod
def clean(self, dataset: Dataset, node_ids: Optional[list[str]], with_keywords: bool = True, **kwargs):
raise NotImplementedError

View File

@ -534,7 +534,6 @@ class DatasetRetrieval:
synchronize_session=False,
)
)
db.session.commit()
else:
query = db.session.query(DocumentSegment).where(
DocumentSegment.index_node_id == document.metadata["doc_id"]
@ -596,7 +595,7 @@ class DatasetRetrieval:
with flask_app.app_context():
with Session(db.engine) as session:
dataset_stmt = select(Dataset).where(Dataset.id == dataset_id)
dataset = db.session.scalar(dataset_stmt)
dataset = session.scalar(dataset_stmt)
if not dataset:
return []

View File

@ -1,4 +1,4 @@
from typing import Union, cast
from typing import Union
from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity
from core.model_manager import ModelInstance
@ -28,14 +28,11 @@ class FunctionCallMultiDatasetRouter:
SystemPromptMessage(content="You are a helpful AI assistant."),
UserPromptMessage(content=query),
]
result = cast(
LLMResult,
model_instance.invoke_llm(
prompt_messages=prompt_messages,
tools=dataset_tools,
stream=False,
model_parameters={"temperature": 0.2, "top_p": 0.3, "max_tokens": 1500},
),
result: LLMResult = model_instance.invoke_llm(
prompt_messages=prompt_messages,
tools=dataset_tools,
stream=False,
model_parameters={"temperature": 0.2, "top_p": 0.3, "max_tokens": 1500},
)
if result.message.tool_calls:
# get retrieval model config

View File

@ -1,5 +1,5 @@
from collections.abc import Generator, Sequence
from typing import Union, cast
from typing import Union
from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity
from core.model_manager import ModelInstance
@ -150,15 +150,12 @@ class ReactMultiDatasetRouter:
:param stop: stop
:return:
"""
invoke_result = cast(
Generator[LLMResult, None, None],
model_instance.invoke_llm(
prompt_messages=prompt_messages,
model_parameters=completion_param,
stop=stop,
stream=True,
user=user_id,
),
invoke_result: Generator[LLMResult, None, None] = model_instance.invoke_llm(
prompt_messages=prompt_messages,
model_parameters=completion_param,
stop=stop,
stream=True,
user=user_id,
)
# handle invoke result

View File

@ -7,9 +7,12 @@ import logging
from collections.abc import Sequence
from typing import Optional, Union
import psycopg2.errors
from sqlalchemy import UnaryExpression, asc, desc, select
from sqlalchemy.engine import Engine
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import sessionmaker
from tenacity import before_sleep_log, retry, retry_if_exception, stop_after_attempt
from core.model_runtime.utils.encoders import jsonable_encoder
from core.workflow.entities import WorkflowNodeExecution
@ -17,6 +20,7 @@ from core.workflow.enums import NodeType, WorkflowNodeExecutionMetadataKey, Work
from core.workflow.repositories.workflow_node_execution_repository import OrderConfig, WorkflowNodeExecutionRepository
from core.workflow.workflow_type_encoder import WorkflowRuntimeTypeConverter
from libs.helper import extract_tenant_id
from libs.uuid_utils import uuidv7
from models import (
Account,
CreatorUserRole,
@ -182,18 +186,31 @@ class SQLAlchemyWorkflowNodeExecutionRepository(WorkflowNodeExecutionRepository)
db_model.finished_at = domain_model.finished_at
return db_model
def _is_duplicate_key_error(self, exception: BaseException) -> bool:
"""Check if the exception is a duplicate key constraint violation."""
return isinstance(exception, IntegrityError) and isinstance(exception.orig, psycopg2.errors.UniqueViolation)
def _regenerate_id_on_duplicate(
self, execution: WorkflowNodeExecution, db_model: WorkflowNodeExecutionModel
) -> None:
"""Regenerate UUID v7 for both domain and database models when duplicate key detected."""
new_id = str(uuidv7())
logger.warning(
"Duplicate key conflict for workflow node execution ID %s, generating new UUID v7: %s", db_model.id, new_id
)
db_model.id = new_id
execution.id = new_id
def save(self, execution: WorkflowNodeExecution) -> None:
"""
Save or update a NodeExecution domain entity to the database.
This method serves as a domain-to-database adapter that:
1. Converts the domain entity to its database representation
2. Persists the database model using SQLAlchemy's merge operation
2. Checks for existing records and updates or inserts accordingly
3. Maintains proper multi-tenancy by including tenant context during conversion
4. Updates the in-memory cache for faster subsequent lookups
The method handles both creating new records and updating existing ones through
SQLAlchemy's merge operation.
5. Handles duplicate key conflicts by retrying with a new UUID v7
Args:
execution: The NodeExecution domain entity to persist
@ -201,18 +218,61 @@ class SQLAlchemyWorkflowNodeExecutionRepository(WorkflowNodeExecutionRepository)
# Convert domain model to database model using tenant context and other attributes
db_model = self.to_db_model(execution)
# Create a new database session
with self._session_factory() as session:
# SQLAlchemy merge intelligently handles both insert and update operations
# based on the presence of the primary key
session.merge(db_model)
session.commit()
# Use tenacity for retry logic with duplicate key handling
@retry(
stop=stop_after_attempt(3),
retry=retry_if_exception(self._is_duplicate_key_error),
before_sleep=before_sleep_log(logger, logging.WARNING),
reraise=True,
)
def _save_with_retry():
try:
self._persist_to_database(db_model)
except IntegrityError as e:
if self._is_duplicate_key_error(e):
# Generate new UUID and retry
self._regenerate_id_on_duplicate(execution, db_model)
raise # Let tenacity handle the retry
else:
# Different integrity error, don't retry
logger.exception("Non-duplicate key integrity error while saving workflow node execution")
raise
# Update the in-memory cache for faster subsequent lookups
# Only cache if we have a node_execution_id to use as the cache key
try:
_save_with_retry()
# Update the in-memory cache after successful save
if db_model.node_execution_id:
self._node_execution_cache[db_model.node_execution_id] = db_model
except Exception as e:
logger.exception("Failed to save workflow node execution after all retries")
raise
def _persist_to_database(self, db_model: WorkflowNodeExecutionModel) -> None:
"""
Persist the database model to the database.
Checks if a record with the same ID exists and either updates it or creates a new one.
Args:
db_model: The database model to persist
"""
with self._session_factory() as session:
# Check if record already exists
existing = session.get(WorkflowNodeExecutionModel, db_model.id)
if existing:
# Update existing record by copying all non-private attributes
for key, value in db_model.__dict__.items():
if not key.startswith("_"):
setattr(existing, key, value)
else:
# Add new record
session.add(db_model)
session.commit()
def get_db_models_by_workflow_run(
self,
workflow_run_id: str,

View File

@ -74,7 +74,7 @@ class BuiltinToolProviderController(ToolProviderController):
tool = load_yaml_file(path.join(tool_path, tool_file), ignore_error=False)
# get tool class, import the module
assistant_tool_class: type[BuiltinTool] = load_single_subclass_from_source(
assistant_tool_class: type = load_single_subclass_from_source(
module_name=f"core.tools.builtin_tool.providers.{provider}.tools.{tool_name}",
script_path=path.join(
path.dirname(path.realpath(__file__)),

View File

@ -26,7 +26,7 @@ class ToolLabelManager:
labels = cls.filter_tool_labels(labels)
if isinstance(controller, ApiToolProviderController | WorkflowToolProviderController):
provider_id = controller.provider_id
provider_id = controller.provider_id # ty: ignore [unresolved-attribute]
else:
raise ValueError("Unsupported tool type")
@ -51,7 +51,7 @@ class ToolLabelManager:
Get tool labels
"""
if isinstance(controller, ApiToolProviderController | WorkflowToolProviderController):
provider_id = controller.provider_id
provider_id = controller.provider_id # ty: ignore [unresolved-attribute]
elif isinstance(controller, BuiltinToolProviderController):
return controller.tool_labels
else:
@ -85,7 +85,7 @@ class ToolLabelManager:
provider_ids = []
for controller in tool_providers:
assert isinstance(controller, ApiToolProviderController | WorkflowToolProviderController)
provider_ids.append(controller.provider_id)
provider_ids.append(controller.provider_id) # ty: ignore [unresolved-attribute]
labels: list[ToolLabelBinding] = (
db.session.query(ToolLabelBinding).where(ToolLabelBinding.tool_id.in_(provider_ids)).all()

View File

@ -1,7 +1,6 @@
from abc import abstractmethod
from abc import ABC, abstractmethod
from typing import Optional
from msal_extensions.persistence import ABC # type: ignore
from pydantic import BaseModel, ConfigDict
from core.callback_handler.index_tool_callback_handler import DatasetIndexToolCallbackHandler

View File

@ -83,7 +83,7 @@ class IfElseNode(Node):
else:
# TODO: Update database then remove this
# Fallback to old structure if cases are not defined
input_conditions, group_result, final_result = _should_not_use_old_function(
input_conditions, group_result, final_result = _should_not_use_old_function( # ty: ignore [deprecated]
condition_processor=condition_processor,
variable_pool=self.graph_runtime_state.variable_pool,
conditions=self._node_data.conditions or [],

View File

@ -2,7 +2,6 @@ from collections.abc import Mapping
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Optional, Union
from uuid import uuid4
from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, WorkflowAppGenerateEntity
from core.app.entities.queue_entities import (
@ -31,6 +30,7 @@ from core.workflow.repositories.workflow_node_execution_repository import Workfl
from core.workflow.system_variable import SystemVariable
from core.workflow.workflow_entry import WorkflowEntry
from libs.datetime_utils import naive_utc_now
from libs.uuid_utils import uuidv7
@dataclass
@ -265,7 +265,7 @@ class WorkflowCycleManager:
"""Get execution ID from system variables or generate a new one."""
if self._workflow_system_variables and self._workflow_system_variables.workflow_execution_id:
return str(self._workflow_system_variables.workflow_execution_id)
return str(uuid4())
return str(uuidv7())
def _save_and_cache_workflow_execution(self, execution: WorkflowExecution) -> WorkflowExecution:
"""Save workflow execution to repository and cache it."""

View File

@ -103,7 +103,7 @@ def init_app(app: DifyApp):
def shutdown_tracer():
provider = trace.get_tracer_provider()
if hasattr(provider, "force_flush"):
provider.force_flush()
provider.force_flush() # ty: ignore [call-non-callable]
class ExceptionLoggingHandler(logging.Handler):
"""Custom logging handler that creates spans for logging.exception() calls"""

View File

@ -260,7 +260,8 @@ def redis_fallback(default_return: Optional[Any] = None):
try:
return func(*args, **kwargs)
except RedisError as e:
logger.warning("Redis operation failed in %s: %s", func.__name__, str(e), exc_info=True)
func_name = getattr(func, "__name__", "Unknown")
logger.warning("Redis operation failed in %s: %s", func_name, str(e), exc_info=True)
return default_return
return wrapper

View File

@ -101,7 +101,7 @@ def register_external_error_handlers(api: Api) -> None:
exc_info: Any = sys.exc_info()
if exc_info[1] is None:
exc_info = None
current_app.log_exception(exc_info)
current_app.log_exception(exc_info) # ty: ignore [invalid-argument-type]
return data, status_code

View File

@ -136,7 +136,7 @@ class PKCS1OAepCipher:
# Step 3a (OS2IP)
em_int = bytes_to_long(em)
# Step 3b (RSAEP)
m_int = gmpy2.powmod(em_int, self._key.e, self._key.n)
m_int = gmpy2.powmod(em_int, self._key.e, self._key.n) # ty: ignore [unresolved-attribute]
# Step 3c (I2OSP)
c = long_to_bytes(m_int, k)
return c
@ -169,7 +169,7 @@ class PKCS1OAepCipher:
ct_int = bytes_to_long(ciphertext)
# Step 2b (RSADP)
# m_int = self._key._decrypt(ct_int)
m_int = gmpy2.powmod(ct_int, self._key.d, self._key.n)
m_int = gmpy2.powmod(ct_int, self._key.d, self._key.n) # ty: ignore [unresolved-attribute]
# Complete step 2c (I2OSP)
em = long_to_bytes(m_int, k)
# Step 3a

View File

@ -14,11 +14,11 @@ class PassportService:
def verify(self, token):
try:
return jwt.decode(token, self.sk, algorithms=["HS256"])
except jwt.exceptions.ExpiredSignatureError:
except jwt.ExpiredSignatureError:
raise Unauthorized("Token has expired.")
except jwt.exceptions.InvalidSignatureError:
except jwt.InvalidSignatureError:
raise Unauthorized("Invalid token signature.")
except jwt.exceptions.DecodeError:
except jwt.DecodeError:
raise Unauthorized("Invalid token.")
except jwt.exceptions.PyJWTError: # Catch-all for other JWT errors
except jwt.PyJWTError: # Catch-all for other JWT errors
raise Unauthorized("Invalid token.")

View File

@ -26,9 +26,9 @@ class SendGridClient:
to_email = To(_to)
subject = mail["subject"]
content = Content("text/html", mail["html"])
mail = Mail(from_email, to_email, subject, content)
mail_json = mail.get() # type: ignore
response = sg.client.mail.send.post(request_body=mail_json)
sg_mail = Mail(from_email, to_email, subject, content)
mail_json = sg_mail.get()
response = sg.client.mail.send.post(request_body=mail_json) # ty: ignore [call-non-callable]
logger.debug(response.status_code)
logger.debug(response.body)
logger.debug(response.headers)

View File

@ -1,7 +1,15 @@
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass
from models.engine import metadata
class Base(DeclarativeBase):
metadata = metadata
class TypeBase(MappedAsDataclass, DeclarativeBase):
"""
This is for adding type, after all finished, rename to Base.
"""
metadata = metadata

View File

@ -9,7 +9,11 @@ from sqlalchemy import ForeignKey, String, func
from sqlalchemy.orm import Mapped, mapped_column
from core.helper import encrypter
from models.base import Base
from core.mcp.types import Tool
from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_bundle import ApiToolBundle
from core.tools.entities.tool_entities import ApiProviderSchemaType, WorkflowToolParameterConfiguration
from models.base import Base, TypeBase
from .engine import db
from .model import Account, App, Tenant
@ -165,7 +169,7 @@ class ApiToolProvider(Base):
return db.session.query(Tenant).where(Tenant.id == self.tenant_id).first()
class ToolLabelBinding(Base):
class ToolLabelBinding(TypeBase):
"""
The table stores the labels for tools.
"""
@ -176,7 +180,7 @@ class ToolLabelBinding(Base):
sa.UniqueConstraint("tool_id", "label_name", name="unique_tool_label_bind"),
)
id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"), init=False)
# tool id
tool_id: Mapped[str] = mapped_column(String(64), nullable=False)
# tool type

View File

@ -110,6 +110,7 @@ dev = [
"dotenv-linter~=0.5.0",
"faker~=32.1.0",
"lxml-stubs~=0.5.1",
"ty~=0.0.1a19",
"mypy~=1.17.1",
"ruff~=0.12.3",
"pytest~=8.3.2",

View File

@ -145,7 +145,10 @@ class AccountService:
if naive_utc_now() - account.last_active_at > timedelta(minutes=10):
account.last_active_at = naive_utc_now()
db.session.commit()
# NOTE: make sure account is accessible outside of a db session
# This ensures that it will work correctly after upgrading to Flask version 3.1.2
db.session.refresh(account)
db.session.close()
return account
@staticmethod

View File

@ -532,7 +532,7 @@ class AppDslService:
return app
@classmethod
def export_dsl(cls, app_model: App, include_secret: bool = False) -> str:
def export_dsl(cls, app_model: App, include_secret: bool = False, workflow_id: Optional[str] = None) -> str:
"""
Export app
:param app_model: App instance
@ -556,7 +556,7 @@ class AppDslService:
if app_mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
cls._append_workflow_export_data(
export_data=export_data, app_model=app_model, include_secret=include_secret
export_data=export_data, app_model=app_model, include_secret=include_secret, workflow_id=workflow_id
)
else:
cls._append_model_config_export_data(export_data, app_model)
@ -564,14 +564,16 @@ class AppDslService:
return yaml.dump(export_data, allow_unicode=True) # type: ignore
@classmethod
def _append_workflow_export_data(cls, *, export_data: dict, app_model: App, include_secret: bool) -> None:
def _append_workflow_export_data(
cls, *, export_data: dict, app_model: App, include_secret: bool, workflow_id: Optional[str] = None
) -> None:
"""
Append workflow export data
:param export_data: export data
:param app_model: App instance
"""
workflow_service = WorkflowService()
workflow = workflow_service.get_draft_workflow(app_model)
workflow = workflow_service.get_draft_workflow(app_model, workflow_id)
if not workflow:
raise ValueError("Missing draft workflow configuration, please check.")

View File

@ -133,7 +133,11 @@ class DatasetService:
# Check if tag_ids is not empty to avoid WHERE false condition
if tag_ids and len(tag_ids) > 0:
target_ids = TagService.get_target_ids_by_tag_ids("knowledge", tenant_id, tag_ids)
target_ids = TagService.get_target_ids_by_tag_ids(
"knowledge",
tenant_id, # ty: ignore [invalid-argument-type]
tag_ids,
)
if target_ids and len(target_ids) > 0:
query = query.where(Dataset.id.in_(target_ids))
else:
@ -2361,7 +2365,9 @@ class SegmentService:
index_node_ids = [seg.index_node_id for seg in segments]
total_words = sum(seg.word_count for seg in segments)
document.word_count -= total_words
document.word_count = (
document.word_count - total_words if document.word_count and document.word_count > total_words else 0
)
db.session.add(document)
delete_segment_from_index_task.delay(index_node_ids, dataset.id, document.id)

View File

@ -229,7 +229,7 @@ class ExternalDatasetService:
@staticmethod
def get_external_knowledge_api_settings(settings: dict) -> ExternalKnowledgeApiSetting:
return ExternalKnowledgeApiSetting.parse_obj(settings)
return ExternalKnowledgeApiSetting.model_validate(settings)
@staticmethod
def create_external_dataset(tenant_id: str, user_id: str, args: dict) -> Dataset:

View File

@ -170,7 +170,9 @@ class ModelLoadBalancingService:
if variable in credentials:
try:
credentials[variable] = encrypter.decrypt_token_with_decoding(
credentials.get(variable), decoding_rsa_key, decoding_cipher_rsa
credentials.get(variable), # ty: ignore [invalid-argument-type]
decoding_rsa_key,
decoding_cipher_rsa,
)
except ValueError:
pass

View File

@ -229,7 +229,7 @@ class MCPToolManageService:
provider_controller = MCPToolProviderController._from_db(mcp_provider)
tool_configuration = ProviderConfigEncrypter(
tenant_id=mcp_provider.tenant_id,
config=list(provider_controller.get_credentials_schema()),
config=list(provider_controller.get_credentials_schema()), # ty: ignore [invalid-argument-type]
provider_config_cache=NoOpProviderCredentialCache(),
)
credentials = tool_configuration.encrypt(credentials)

View File

@ -92,10 +92,12 @@ class WorkflowService:
)
return db.session.execute(stmt).scalar_one()
def get_draft_workflow(self, app_model: App) -> Optional[Workflow]:
def get_draft_workflow(self, app_model: App, workflow_id: Optional[str] = None) -> Optional[Workflow]:
"""
Get draft workflow
"""
if workflow_id:
return self.get_published_workflow_by_id(app_model, workflow_id)
# fetch draft workflow by app_model
workflow = (
db.session.query(Workflow)
@ -111,7 +113,9 @@ class WorkflowService:
return workflow
def get_published_workflow_by_id(self, app_model: App, workflow_id: str) -> Optional[Workflow]:
# fetch published workflow by workflow_id
"""
fetch published workflow by workflow_id
"""
workflow = (
db.session.query(Workflow)
.where(

View File

@ -45,9 +45,11 @@ def duplicate_document_indexing_task(dataset_id: str, document_ids: list):
batch_upload_limit = int(dify_config.BATCH_UPLOAD_LIMIT)
if count > batch_upload_limit:
raise ValueError(f"You have reached the batch upload limit of {batch_upload_limit}.")
if 0 < vector_space.limit <= vector_space.size:
current = int(getattr(vector_space, "size", 0) or 0)
limit = int(getattr(vector_space, "limit", 0) or 0)
if limit > 0 and (current + count) > limit:
raise ValueError(
"Your total number of documents plus the number of uploads have over the limit of "
"Your total number of documents plus the number of uploads have exceeded the limit of "
"your subscription."
)
except Exception as e:

View File

@ -322,7 +322,87 @@ class TestAppDslService:
# Verify workflow service was called
mock_external_service_dependencies["workflow_service"].return_value.get_draft_workflow.assert_called_once_with(
app
app, None
)
def test_export_dsl_with_workflow_id_success(self, db_session_with_containers, mock_external_service_dependencies):
"""
Test successful DSL export with specific workflow ID.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Update app to workflow mode
app.mode = "workflow"
db_session_with_containers.commit()
# Mock workflow service to return a workflow when specific workflow_id is provided
mock_workflow = MagicMock()
mock_workflow.to_dict.return_value = {
"graph": {"nodes": [{"id": "start", "type": "start", "data": {"type": "start"}}], "edges": []},
"features": {},
"environment_variables": [],
"conversation_variables": [],
}
# Mock the get_draft_workflow method to return different workflows based on workflow_id
def mock_get_draft_workflow(app_model, workflow_id=None):
if workflow_id == "specific-workflow-id":
return mock_workflow
return None
mock_external_service_dependencies[
"workflow_service"
].return_value.get_draft_workflow.side_effect = mock_get_draft_workflow
# Export DSL with specific workflow ID
exported_dsl = AppDslService.export_dsl(app, include_secret=False, workflow_id="specific-workflow-id")
# Parse exported YAML
exported_data = yaml.safe_load(exported_dsl)
# Verify exported data structure
assert exported_data["kind"] == "app"
assert exported_data["app"]["name"] == app.name
assert exported_data["app"]["mode"] == "workflow"
# Verify workflow was exported
assert "workflow" in exported_data
assert "graph" in exported_data["workflow"]
assert "nodes" in exported_data["workflow"]["graph"]
# Verify dependencies were exported
assert "dependencies" in exported_data
assert isinstance(exported_data["dependencies"], list)
# Verify workflow service was called with specific workflow ID
mock_external_service_dependencies["workflow_service"].return_value.get_draft_workflow.assert_called_once_with(
app, "specific-workflow-id"
)
def test_export_dsl_with_invalid_workflow_id_raises_error(
self, db_session_with_containers, mock_external_service_dependencies
):
"""
Test that export_dsl raises error when invalid workflow ID is provided.
"""
fake = Faker()
app, account = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
# Update app to workflow mode
app.mode = "workflow"
db_session_with_containers.commit()
# Mock workflow service to return None when invalid workflow ID is provided
mock_external_service_dependencies["workflow_service"].return_value.get_draft_workflow.return_value = None
# Export DSL with invalid workflow ID should raise ValueError
with pytest.raises(ValueError, match="Missing draft workflow configuration, please check."):
AppDslService.export_dsl(app, include_secret=False, workflow_id="invalid-workflow-id")
# Verify workflow service was called with the invalid workflow ID
mock_external_service_dependencies["workflow_service"].return_value.get_draft_workflow.assert_called_once_with(
app, "invalid-workflow-id"
)
def test_check_dependencies_success(self, db_session_with_containers, mock_external_service_dependencies):

View File

@ -0,0 +1,210 @@
"""Unit tests for workflow node execution conflict handling."""
from datetime import datetime
from unittest.mock import MagicMock, Mock
import psycopg2.errors
import pytest
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import sessionmaker
from core.repositories.sqlalchemy_workflow_node_execution_repository import (
SQLAlchemyWorkflowNodeExecutionRepository,
)
from core.workflow.entities.workflow_node_execution import (
WorkflowNodeExecution,
WorkflowNodeExecutionStatus,
)
from core.workflow.nodes.enums import NodeType
from models import Account, WorkflowNodeExecutionTriggeredFrom
class TestWorkflowNodeExecutionConflictHandling:
"""Test cases for handling duplicate key conflicts in workflow node execution."""
def setup_method(self):
"""Set up test fixtures."""
# Create a mock user with tenant_id
self.mock_user = Mock(spec=Account)
self.mock_user.id = "test-user-id"
self.mock_user.current_tenant_id = "test-tenant-id"
# Create mock session factory
self.mock_session_factory = Mock(spec=sessionmaker)
# Create repository instance
self.repository = SQLAlchemyWorkflowNodeExecutionRepository(
session_factory=self.mock_session_factory,
user=self.mock_user,
app_id="test-app-id",
triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN,
)
def test_save_with_duplicate_key_retries_with_new_uuid(self):
"""Test that save retries with a new UUID v7 when encountering duplicate key error."""
# Create a mock session
mock_session = MagicMock()
mock_session.__enter__ = Mock(return_value=mock_session)
mock_session.__exit__ = Mock(return_value=None)
self.mock_session_factory.return_value = mock_session
# Mock session.get to return None (no existing record)
mock_session.get.return_value = None
# Create IntegrityError for duplicate key with proper psycopg2.errors.UniqueViolation
mock_unique_violation = Mock(spec=psycopg2.errors.UniqueViolation)
duplicate_error = IntegrityError(
"duplicate key value violates unique constraint",
params=None,
orig=mock_unique_violation,
)
# First call to session.add raises IntegrityError, second succeeds
mock_session.add.side_effect = [duplicate_error, None]
mock_session.commit.side_effect = [None, None]
# Create test execution
execution = WorkflowNodeExecution(
id="original-id",
workflow_id="test-workflow-id",
workflow_execution_id="test-workflow-execution-id",
node_execution_id="test-node-execution-id",
node_id="test-node-id",
node_type=NodeType.START,
title="Test Node",
index=1,
status=WorkflowNodeExecutionStatus.RUNNING,
created_at=datetime.utcnow(),
)
original_id = execution.id
# Save should succeed after retry
self.repository.save(execution)
# Verify that session.add was called twice (initial attempt + retry)
assert mock_session.add.call_count == 2
# Verify that the ID was changed (new UUID v7 generated)
assert execution.id != original_id
def test_save_with_existing_record_updates_instead_of_insert(self):
"""Test that save updates existing record instead of inserting duplicate."""
# Create a mock session
mock_session = MagicMock()
mock_session.__enter__ = Mock(return_value=mock_session)
mock_session.__exit__ = Mock(return_value=None)
self.mock_session_factory.return_value = mock_session
# Mock existing record
mock_existing = MagicMock()
mock_session.get.return_value = mock_existing
mock_session.commit.return_value = None
# Create test execution
execution = WorkflowNodeExecution(
id="existing-id",
workflow_id="test-workflow-id",
workflow_execution_id="test-workflow-execution-id",
node_execution_id="test-node-execution-id",
node_id="test-node-id",
node_type=NodeType.START,
title="Test Node",
index=1,
status=WorkflowNodeExecutionStatus.SUCCEEDED,
created_at=datetime.utcnow(),
)
# Save should update existing record
self.repository.save(execution)
# Verify that session.add was not called (update path)
mock_session.add.assert_not_called()
# Verify that session.commit was called
mock_session.commit.assert_called_once()
def test_save_exceeds_max_retries_raises_error(self):
"""Test that save raises error after exceeding max retries."""
# Create a mock session
mock_session = MagicMock()
mock_session.__enter__ = Mock(return_value=mock_session)
mock_session.__exit__ = Mock(return_value=None)
self.mock_session_factory.return_value = mock_session
# Mock session.get to return None (no existing record)
mock_session.get.return_value = None
# Create IntegrityError for duplicate key with proper psycopg2.errors.UniqueViolation
mock_unique_violation = Mock(spec=psycopg2.errors.UniqueViolation)
duplicate_error = IntegrityError(
"duplicate key value violates unique constraint",
params=None,
orig=mock_unique_violation,
)
# All attempts fail with duplicate error
mock_session.add.side_effect = duplicate_error
# Create test execution
execution = WorkflowNodeExecution(
id="test-id",
workflow_id="test-workflow-id",
workflow_execution_id="test-workflow-execution-id",
node_execution_id="test-node-execution-id",
node_id="test-node-id",
node_type=NodeType.START,
title="Test Node",
index=1,
status=WorkflowNodeExecutionStatus.RUNNING,
created_at=datetime.utcnow(),
)
# Save should raise IntegrityError after max retries
with pytest.raises(IntegrityError):
self.repository.save(execution)
# Verify that session.add was called 3 times (max_retries)
assert mock_session.add.call_count == 3
def test_save_non_duplicate_integrity_error_raises_immediately(self):
"""Test that non-duplicate IntegrityErrors are raised immediately without retry."""
# Create a mock session
mock_session = MagicMock()
mock_session.__enter__ = Mock(return_value=mock_session)
mock_session.__exit__ = Mock(return_value=None)
self.mock_session_factory.return_value = mock_session
# Mock session.get to return None (no existing record)
mock_session.get.return_value = None
# Create IntegrityError for non-duplicate constraint
other_error = IntegrityError(
"null value in column violates not-null constraint",
params=None,
orig=None,
)
# First call raises non-duplicate error
mock_session.add.side_effect = other_error
# Create test execution
execution = WorkflowNodeExecution(
id="test-id",
workflow_id="test-workflow-id",
workflow_execution_id="test-workflow-execution-id",
node_execution_id="test-node-execution-id",
node_id="test-node-id",
node_type=NodeType.START,
title="Test Node",
index=1,
status=WorkflowNodeExecutionStatus.RUNNING,
created_at=datetime.utcnow(),
)
# Save should raise error immediately
with pytest.raises(IntegrityError):
self.repository.save(execution)
# Verify that session.add was called only once (no retry)
assert mock_session.add.call_count == 1

View File

@ -88,6 +88,8 @@ def test_save(repository, session):
session_obj, _ = session
# Create a mock execution
execution = MagicMock(spec=WorkflowNodeExecutionModel)
execution.id = "test-id"
execution.node_execution_id = "test-node-execution-id"
execution.tenant_id = None
execution.app_id = None
execution.inputs = None
@ -97,7 +99,13 @@ def test_save(repository, session):
# Mock the to_db_model method to return the execution itself
# This simulates the behavior of setting tenant_id and app_id
repository.to_db_model = MagicMock(return_value=execution)
db_model = MagicMock(spec=WorkflowNodeExecutionModel)
db_model.id = "test-id"
db_model.node_execution_id = "test-node-execution-id"
repository.to_db_model = MagicMock(return_value=db_model)
# Mock session.get to return None (no existing record)
session_obj.get.return_value = None
# Call save method
repository.save(execution)
@ -105,8 +113,14 @@ def test_save(repository, session):
# Assert to_db_model was called with the execution
repository.to_db_model.assert_called_once_with(execution)
# Assert session.merge was called (now using merge for both save and update)
session_obj.merge.assert_called_once_with(execution)
# Assert session.get was called to check for existing record
session_obj.get.assert_called_once_with(WorkflowNodeExecutionModel, db_model.id)
# Assert session.add was called for new record
session_obj.add.assert_called_once_with(db_model)
# Assert session.commit was called
session_obj.commit.assert_called_once()
def test_save_with_existing_tenant_id(repository, session):
@ -114,6 +128,8 @@ def test_save_with_existing_tenant_id(repository, session):
session_obj, _ = session
# Create a mock execution with existing tenant_id
execution = MagicMock(spec=WorkflowNodeExecutionModel)
execution.id = "existing-id"
execution.node_execution_id = "existing-node-execution-id"
execution.tenant_id = "existing-tenant"
execution.app_id = None
execution.inputs = None
@ -123,20 +139,39 @@ def test_save_with_existing_tenant_id(repository, session):
# Create a modified execution that will be returned by _to_db_model
modified_execution = MagicMock(spec=WorkflowNodeExecutionModel)
modified_execution.id = "existing-id"
modified_execution.node_execution_id = "existing-node-execution-id"
modified_execution.tenant_id = "existing-tenant" # Tenant ID should not change
modified_execution.app_id = repository._app_id # App ID should be set
# Create a dictionary to simulate __dict__ for updating attributes
modified_execution.__dict__ = {
"id": "existing-id",
"node_execution_id": "existing-node-execution-id",
"tenant_id": "existing-tenant",
"app_id": repository._app_id,
}
# Mock the to_db_model method to return the modified execution
repository.to_db_model = MagicMock(return_value=modified_execution)
# Mock session.get to return an existing record
existing_model = MagicMock(spec=WorkflowNodeExecutionModel)
session_obj.get.return_value = existing_model
# Call save method
repository.save(execution)
# Assert to_db_model was called with the execution
repository.to_db_model.assert_called_once_with(execution)
# Assert session.merge was called with the modified execution (now using merge for both save and update)
session_obj.merge.assert_called_once_with(modified_execution)
# Assert session.get was called to check for existing record
session_obj.get.assert_called_once_with(WorkflowNodeExecutionModel, modified_execution.id)
# Assert session.add was NOT called since we're updating existing
session_obj.add.assert_not_called()
# Assert session.commit was called
session_obj.commit.assert_called_once()
def test_get_by_workflow_run(repository, session, mocker: MockerFixture):

16
api/ty.toml Normal file
View File

@ -0,0 +1,16 @@
[src]
exclude = [
# TODO: enable when violations fixed
"core/app/apps/workflow_app_runner.py",
"controllers/console/app",
"controllers/console/explore",
"controllers/console/datasets",
"controllers/console/workspace",
# non-producition or generated code
"migrations",
"tests",
]
[rules]
missing-argument = "ignore" # TODO: restore when **args for constructor is supported properly
possibly-unbound-attribute = "ignore"

View File

@ -1354,6 +1354,7 @@ dev = [
{ name = "ruff" },
{ name = "scipy-stubs" },
{ name = "testcontainers" },
{ name = "ty" },
{ name = "types-aiofiles" },
{ name = "types-beautifulsoup4" },
{ name = "types-cachetools" },
@ -1544,6 +1545,7 @@ dev = [
{ name = "ruff", specifier = "~=0.12.3" },
{ name = "scipy-stubs", specifier = ">=1.15.3.0" },
{ name = "testcontainers", specifier = "~=4.10.0" },
{ name = "ty", specifier = "~=0.0.1a19" },
{ name = "types-aiofiles", specifier = "~=24.1.0" },
{ name = "types-beautifulsoup4", specifier = "~=4.12.0" },
{ name = "types-cachetools", specifier = "~=5.5.0" },
@ -5848,6 +5850,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/41/b1/d7520cc5cb69c825599042eb3a7c986fa9baa8a8d2dea9acd78e152c81e2/transformers-4.53.3-py3-none-any.whl", hash = "sha256:5aba81c92095806b6baf12df35d756cf23b66c356975fb2a7fa9e536138d7c75", size = 10826382 },
]
[[package]]
name = "ty"
version = "0.0.1a19"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c0/04/281c1a3c9c53dae5826b9d01a3412de653e3caf1ca50ce1265da66e06d73/ty-0.0.1a19.tar.gz", hash = "sha256:894f6a13a43989c8ef891ae079b3b60a0c0eae00244abbfbbe498a3840a235ac", size = 4098412, upload-time = "2025-08-19T13:29:58.559Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3e/65/a61cfcc7248b0257a3110bf98d3d910a4729c1063abdbfdcd1cad9012323/ty-0.0.1a19-py3-none-linux_armv6l.whl", hash = "sha256:e0e7762f040f4bab1b37c57cb1b43cc3bc5afb703fa5d916dfcafa2ef885190e", size = 8143744, upload-time = "2025-08-19T13:29:13.88Z" },
{ url = "https://files.pythonhosted.org/packages/02/d9/232afef97d9afa2274d23a4c49a3ad690282ca9696e1b6bbb6e4e9a1b072/ty-0.0.1a19-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cd0a67ac875f49f34d9a0b42dcabf4724194558a5dd36867209d5695c67768f7", size = 8305799, upload-time = "2025-08-19T13:29:17.322Z" },
{ url = "https://files.pythonhosted.org/packages/20/14/099d268da7a9cccc6ba38dfc124f6742a1d669bc91f2c61a3465672b4f71/ty-0.0.1a19-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ff8b1c0b85137333c39eccd96c42603af8ba7234d6e2ed0877f66a4a26750dd4", size = 7901431, upload-time = "2025-08-19T13:29:21.635Z" },
{ url = "https://files.pythonhosted.org/packages/c2/cd/3f1ca6e1d7f77cc4d08910a3fc4826313c031c0aae72286ae859e737670c/ty-0.0.1a19-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fef34a29f4b97d78aa30e60adbbb12137cf52b8b2b0f1a408dd0feb0466908a", size = 8051501, upload-time = "2025-08-19T13:29:23.741Z" },
{ url = "https://files.pythonhosted.org/packages/47/72/ddbec39f48ce3f5f6a3fa1f905c8fff2873e59d2030f738814032bd783e3/ty-0.0.1a19-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b0f219cb43c0c50fc1091f8ebd5548d3ef31ee57866517b9521d5174978af9fd", size = 7981234, upload-time = "2025-08-19T13:29:25.839Z" },
{ url = "https://files.pythonhosted.org/packages/f2/0f/58e76b8d4634df066c790d362e8e73b25852279cd6f817f099b42a555a66/ty-0.0.1a19-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22abb6c1f14c65c1a2fafd38e25dd3c87994b3ab88cb0b323235b51dbad082d9", size = 8916394, upload-time = "2025-08-19T13:29:27.932Z" },
{ url = "https://files.pythonhosted.org/packages/70/30/01bfd93ccde11540b503e2539e55f6a1fc6e12433a229191e248946eb753/ty-0.0.1a19-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5b49225c349a3866e38dd297cb023a92d084aec0e895ed30ca124704bff600e6", size = 9412024, upload-time = "2025-08-19T13:29:30.942Z" },
{ url = "https://files.pythonhosted.org/packages/a8/a2/2216d752f5f22c5c0995f9b13f18337301220f2a7d952c972b33e6a63583/ty-0.0.1a19-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:88f41728b3b07402e0861e3c34412ca963268e55f6ab1690208f25d37cb9d63c", size = 9032657, upload-time = "2025-08-19T13:29:33.933Z" },
{ url = "https://files.pythonhosted.org/packages/24/c7/e6650b0569be1b69a03869503d07420c9fb3e90c9109b09726c44366ce63/ty-0.0.1a19-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33814a1197ec3e930fcfba6fb80969fe7353957087b42b88059f27a173f7510b", size = 8812775, upload-time = "2025-08-19T13:29:36.505Z" },
{ url = "https://files.pythonhosted.org/packages/35/c6/b8a20e06b97fe8203059d56d8f91cec4f9633e7ba65f413d80f16aa0be04/ty-0.0.1a19-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d71b7f2b674a287258f628acafeecd87691b169522945ff6192cd8a69af15857", size = 8631417, upload-time = "2025-08-19T13:29:38.837Z" },
{ url = "https://files.pythonhosted.org/packages/be/99/821ca1581dcf3d58ffb7bbe1cde7e1644dbdf53db34603a16a459a0b302c/ty-0.0.1a19-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a7f8ef9ac4c38e8651c18c7380649c5a3fa9adb1a6012c721c11f4bbdc0ce24", size = 7928900, upload-time = "2025-08-19T13:29:41.08Z" },
{ url = "https://files.pythonhosted.org/packages/08/cb/59f74a0522e57565fef99e2287b2bc803ee47ff7dac250af26960636939f/ty-0.0.1a19-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:60f40e72f0fbf4e54aa83d9a6cb1959f551f83de73af96abbb94711c1546bd60", size = 8003310, upload-time = "2025-08-19T13:29:43.165Z" },
{ url = "https://files.pythonhosted.org/packages/4c/b3/1209b9acb5af00a2755114042e48fb0f71decc20d9d77a987bf5b3d1a102/ty-0.0.1a19-py3-none-musllinux_1_2_i686.whl", hash = "sha256:64971e4d3e3f83dc79deb606cc438255146cab1ab74f783f7507f49f9346d89d", size = 8496463, upload-time = "2025-08-19T13:29:46.136Z" },
{ url = "https://files.pythonhosted.org/packages/a2/d6/a4b6ba552d347a08196d83a4d60cb23460404a053dd3596e23a922bce544/ty-0.0.1a19-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9aadbff487e2e1486e83543b4f4c2165557f17432369f419be9ba48dc47625ca", size = 8700633, upload-time = "2025-08-19T13:29:49.351Z" },
{ url = "https://files.pythonhosted.org/packages/96/c5/258f318d68b95685c8d98fb654a38882c9d01ce5d9426bed06124f690f04/ty-0.0.1a19-py3-none-win32.whl", hash = "sha256:00b75b446357ee22bcdeb837cb019dc3bc1dc5e5013ff0f46a22dfe6ce498fe2", size = 7811441, upload-time = "2025-08-19T13:29:52.077Z" },
{ url = "https://files.pythonhosted.org/packages/fb/bb/039227eee3c0c0cddc25f45031eea0f7f10440713f12d333f2f29cf8e934/ty-0.0.1a19-py3-none-win_amd64.whl", hash = "sha256:aaef76b2f44f6379c47adfe58286f0c56041cb2e374fd8462ae8368788634469", size = 8441186, upload-time = "2025-08-19T13:29:54.53Z" },
{ url = "https://files.pythonhosted.org/packages/74/5f/bceb29009670ae6f759340f9cb434121bc5ed84ad0f07bdc6179eaaa3204/ty-0.0.1a19-py3-none-win_arm64.whl", hash = "sha256:893755bb35f30653deb28865707e3b16907375c830546def2741f6ff9a764710", size = 8000810, upload-time = "2025-08-19T13:29:56.796Z" },
]
[[package]]
name = "typer"
version = "0.16.0"

View File

@ -17,5 +17,8 @@ uv run --project api --dev dotenv-linter ./api/.env.example ./web/.env.example
# run import-linter
uv run --directory api --dev lint-imports
# run ty check
dev/ty-check
# run mypy check
dev/mypy-check

10
dev/ty-check Executable file
View File

@ -0,0 +1,10 @@
#!/bin/bash
set -x
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
cd "$SCRIPT_DIR/.."
# run ty checks
uv run --directory api --dev \
ty check

View File

@ -41,6 +41,15 @@ if $api_modified; then
echo "Please run 'dev/reformat' to fix the fixable linting errors."
exit 1
fi
# run ty checks
uv run --directory api --dev ty check || status=$?
status=${status:-0}
if [ $status -ne 0 ]; then
echo "ty type checker on api module error, exit code: $status"
echo "Please run 'dev/ty-check' to check the type errors."
exit 1
fi
fi
if $web_modified; then

View File

@ -69,7 +69,6 @@ export default function AccountPage() {
}
catch (e) {
notify({ type: 'error', message: (e as Error).message })
setEditNameModalVisible(false)
setEditing(false)
}
}

View File

@ -43,6 +43,7 @@ const Filter: FC<IFilterProps> = ({ queryParams, setQueryParams }: IFilterProps)
{ value: 'succeeded', name: 'Success' },
{ value: 'failed', name: 'Fail' },
{ value: 'stopped', name: 'Stop' },
{ value: 'partial-succeeded', name: 'Partial Success' },
]}
/>
<Chip

View File

@ -103,12 +103,6 @@ const BaseField = ({
})
}, [values, show_on])
const booleanRadioValue = useMemo(() => {
if (value === null || value === undefined)
return undefined
return value ? 1 : 0
}, [value])
if (!show)
return null
@ -149,6 +143,7 @@ const BaseField = ({
onBlur={field.handleBlur}
disabled={disabled}
placeholder={memorizedPlaceholder}
autoComplete={'new-password'}
/>
)
}
@ -215,11 +210,11 @@ const BaseField = ({
formSchema.type === FormTypeEnum.boolean && (
<Radio.Group
className='flex w-fit items-center'
value={booleanRadioValue}
onChange={val => field.handleChange(val === 1)}
value={value}
onChange={v => field.handleChange(v)}
>
<Radio value={1} className='!mr-1'>True</Radio>
<Radio value={0}>False</Radio>
<Radio value={true} className='!mr-1'>True</Radio>
<Radio value={false}>False</Radio>
</Radio.Group>
)
}

View File

@ -5,7 +5,7 @@ import cn from '@/utils/classnames'
export type TRadioGroupProps = {
children?: ReactNode | ReactNode[]
value?: string | number
value?: string | number | boolean
className?: string
onChange?: (value: any) => void
}

View File

@ -10,7 +10,7 @@ export type IRadioProps = {
labelClassName?: string
children?: string | ReactNode
checked?: boolean
value?: string | number
value?: string | number | boolean
disabled?: boolean
onChange?: (e?: IRadioProps['value']) => void
}

View File

@ -36,7 +36,7 @@ export default function LocaleSigninSelect({
leaveTo="transform opacity-0 scale-95"
>
<MenuItems className="absolute right-0 z-10 mt-2 w-[200px] origin-top-right divide-y divide-divider-regular rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg focus:outline-none">
<div className="px-1 py-1 ">
<div className="max-h-96 overflow-y-auto px-1 py-1 [mask-image:linear-gradient(to_bottom,transparent_0px,black_8px,black_calc(100%-8px),transparent_100%)]">
{items.map((item) => {
return <MenuItem key={item.value}>
<button

View File

@ -65,7 +65,10 @@ export const useModelFormSchemas = (
}, [formSchemas, t])
const formValues = useMemo(() => {
let result = {}
let result: any = {}
formSchemas.forEach((schema) => {
result[schema.variable] = schema.default
})
if (credential) {
result = { ...result, __authorization_name__: credential?.credential_name }
if (credentials)
@ -74,7 +77,7 @@ export const useModelFormSchemas = (
if (model)
result = { ...result, __model_name: model?.model, __model_type: model?.model_type }
return result
}, [credentials, credential, model])
}, [credentials, credential, model, formSchemas])
return {
formSchemas: formSchemasWithAuthorizationName,

View File

@ -284,11 +284,11 @@ function Form<
</div>
<Radio.Group
className='flex items-center'
value={value[variable] === null ? undefined : (value[variable] ? 1 : 0)}
onChange={val => handleFormChange(variable, val === 1)}
value={value[variable]}
onChange={val => handleFormChange(variable, val)}
>
<Radio value={1} className='!mr-1'>True</Radio>
<Radio value={0}>False</Radio>
<Radio value={true} className='!mr-1'>True</Radio>
<Radio value={false}>False</Radio>
</Radio.Group>
</div>
{fieldMoreInfo?.(formSchema)}

View File

@ -2,6 +2,7 @@ import type { FC } from 'react'
import {
memo,
useCallback,
useEffect,
useMemo,
useRef,
} from 'react'
@ -188,6 +189,20 @@ const ModelModal: FC<ModelModalProps> = ({
return null
}, [model, provider])
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
event.stopPropagation()
onCancel()
}
}
document.addEventListener('keydown', handleKeyDown, true)
return () => {
document.removeEventListener('keydown', handleKeyDown, true)
}
}, [onCancel])
return (
<PortalToFollowElem open>
<PortalToFollowElemContent className='z-[60] h-full w-full'>

View File

@ -91,8 +91,8 @@ const ParameterItem: FC<ParameterItemProps> = ({
numberInputRef.current!.value = `${num}`
}
const handleRadioChange = (v: number) => {
handleInputChange(v === 1)
const handleRadioChange = (v: boolean) => {
handleInputChange(v)
}
const handleStringInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
@ -187,11 +187,11 @@ const ParameterItem: FC<ParameterItemProps> = ({
return (
<Radio.Group
className='flex w-[178px] items-center'
value={renderValue ? 1 : 0}
value={renderValue as boolean}
onChange={handleRadioChange}
>
<Radio value={1} className='w-[83px]'>True</Radio>
<Radio value={0} className='w-[83px]'>False</Radio>
<Radio value={true} className='w-[83px]'>True</Radio>
<Radio value={false} className='w-[83px]'>False</Radio>
</Radio.Group>
)
}

View File

@ -111,7 +111,7 @@ const ConfigCredential: FC<Props> = ({
<Button onClick={onRemove}>{t('common.operation.remove')}</Button>
)
}
< div className='flex space-x-2'>
<div className='flex space-x-2'>
<Button onClick={onCancel}>{t('common.operation.cancel')}</Button>
<Button loading={isLoading || isSaving} disabled={isLoading || isSaving} variant='primary' onClick={handleSave}>{t('common.operation.save')}</Button>
</div>

View File

@ -346,7 +346,7 @@ export const useDSL = () => {
const appDetail = useAppStore(s => s.appDetail)
const handleExportDSL = useCallback(async (include = false) => {
const handleExportDSL = useCallback(async (include = false, workflowId?: string) => {
if (!appDetail)
return
@ -358,6 +358,7 @@ export const useDSL = () => {
await doSyncWorkflowDraft()
const { data } = await exportAppConfig({
appID: appDetail.id,
workflowID: workflowId,
include,
})
const a = document.createElement('a')

View File

@ -140,7 +140,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
<ConfigPrompt
readOnly={readOnly}
nodeId={id}
filterVar={filterInputVar}
filterVar={isShowVars ? filterJinja2InputVar : filterInputVar}
isChatModel={isChatModel}
isChatApp={isChatMode}
isShowContext

View File

@ -308,7 +308,7 @@ const useConfig = (id: string, payload: LLMNodeType) => {
}, [])
const filterJinja2InputVar = useCallback((varPayload: Var) => {
return [VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber].includes(varPayload.type)
return [VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber, VarType.arrayBoolean, VarType.arrayObject, VarType.object, VarType.array, VarType.boolean].includes(varPayload.type)
}, [])
const filterMemoryPromptVar = useCallback((varPayload: Var) => {

View File

@ -29,6 +29,10 @@ const useContextMenu = (props: ContextMenuProps) => {
key: VersionHistoryContextMenuOptions.edit,
name: t('workflow.versionHistory.nameThisVersion'),
},
{
key: VersionHistoryContextMenuOptions.exportDSL,
name: t('app.export'),
},
{
key: VersionHistoryContextMenuOptions.copyId,
name: t('workflow.versionHistory.copyId'),

View File

@ -3,7 +3,7 @@ import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiArrowDownDoubleLine, RiCloseLine, RiLoader2Line } from '@remixicon/react'
import copy from 'copy-to-clipboard'
import { useNodesSyncDraft, useWorkflowRun } from '../../hooks'
import { useDSL, useNodesSyncDraft, useWorkflowRun } from '../../hooks'
import { useStore, useWorkflowStore } from '../../store'
import { VersionHistoryContextMenuOptions, WorkflowVersionFilterOptions } from '../../types'
import VersionHistoryItem from './version-history-item'
@ -33,6 +33,7 @@ const VersionHistoryPanel = () => {
const workflowStore = useWorkflowStore()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const { handleRestoreFromPublishedWorkflow, handleLoadBackupDraft } = useWorkflowRun()
const { handleExportDSL } = useDSL()
const appDetail = useAppStore.getState().appDetail
const setShowWorkflowVersionHistoryPanel = useStore(s => s.setShowWorkflowVersionHistoryPanel)
const currentVersion = useStore(s => s.currentVersion)
@ -107,8 +108,11 @@ const VersionHistoryPanel = () => {
message: t('workflow.versionHistory.action.copyIdSuccess'),
})
break
case VersionHistoryContextMenuOptions.exportDSL:
handleExportDSL(false, item.id)
break
}
}, [t])
}, [t, handleExportDSL])
const handleCancel = useCallback((operation: VersionHistoryContextMenuOptions) => {
switch (operation) {

View File

@ -451,6 +451,7 @@ export enum VersionHistoryContextMenuOptions {
restore = 'restore',
edit = 'edit',
delete = 'delete',
exportDSL = 'exportDSL',
copyId = 'copyId',
}

View File

@ -86,10 +86,9 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
const isCurrentWorkspaceEditor = useMemo(() => ['owner', 'admin', 'editor'].includes(currentWorkspace.role), [currentWorkspace.role])
const isCurrentWorkspaceDatasetOperator = useMemo(() => currentWorkspace.role === 'dataset_operator', [currentWorkspace.role])
const updateUserProfileAndVersion = useCallback(async () => {
if (userProfileResponse) {
if (userProfileResponse && !userProfileResponse.bodyUsed) {
try {
const clonedResponse = (userProfileResponse as Response).clone()
const result = await clonedResponse.json()
const result = await userProfileResponse.json()
setUserProfile(result)
const current_version = userProfileResponse.headers.get('x-version')
const current_env = process.env.NODE_ENV === 'development' ? 'DEVELOPMENT' : userProfileResponse.headers.get('x-env')

View File

@ -48,7 +48,7 @@ By default we will use `LanguagesSupported` to determine which languages are sup
```
cd web/i18n
cp -r en-US fr-FR
cp -r en-US id-ID
```
2. Modify the translation files in the new folder.
@ -68,6 +68,8 @@ export type I18nText = {
'ru-RU': string
'it-IT': string
'uk-UA': string
'id-ID': string
'tr-TR': string
'YOUR_LANGUAGE_CODE': string
}
```
@ -146,8 +148,8 @@ export const languages = [
{
value: 'id-ID',
name: 'Bahasa Indonesia',
example: 'Saluto, Dify!',
supported: false,
example: 'Halo, Dify!',
supported: true,
},
{
value: 'uk-UA',

View File

@ -8,6 +8,7 @@ export type Item = {
export type I18nText = {
'en-US': string
'zh-Hans': string
'zh-Hant': string
'pt-BR': string
'es-ES': string
'fr-FR': string
@ -16,16 +17,16 @@ export type I18nText = {
'ko-KR': string
'ru-RU': string
'it-IT': string
'th-TH': string
'id-ID': string
'uk-UA': string
'vi-VN': string
'de_DE': string
'zh_Hant': string
'ro-RO': string
'pl-PL': string
'hi-IN': string
'tr-TR': string
'fa-IR': string
'sl-SI': string
'th-TH': string
}
export const languages = data.languages
@ -61,20 +62,25 @@ export const NOTICE_I18N = {
title: {
en_US: 'Important Notice',
zh_Hans: '重要公告',
zh_Hant: '重要公告',
pt_BR: 'Aviso Importante',
es_ES: 'Aviso Importante',
fr_FR: 'Avis important',
de_DE: 'Wichtiger Hinweis',
ja_JP: '重要なお知らせ',
ko_KR: '중요 공지',
pl_PL: 'Ważne ogłoszenie',
uk_UA: 'Важливе повідомлення',
ru_RU: 'Важное Уведомление',
vi_VN: 'Thông báo quan trọng',
it_IT: 'Avviso Importante',
th_TH: 'ประกาศสำคัญ',
id_ID: 'Pengumuman Penting',
uk_UA: 'Важливе повідомлення',
vi_VN: 'Thông báo quan trọng',
ro_RO: 'Anunț Important',
pl_PL: 'Ważne ogłoszenie',
hi_IN: 'महत्वपूर्ण सूचना',
tr_TR: 'Önemli Duyuru',
fa_IR: 'هشدار مهم',
sl_SI: 'Pomembno obvestilo',
th_TH: 'ประกาศสำคัญ',
},
desc: {
en_US:
@ -101,6 +107,8 @@ export const NOTICE_I18N = {
'Наша система будет недоступна с 19:00 до 24:00 UTC 28 августа для обновления. По вопросам, пожалуйста, обращайтесь в нашу службу поддержки (support@dify.ai). Спасибо за ваше терпение',
vi_VN:
'Hệ thống của chúng tôi sẽ ngừng hoạt động từ 19:00 đến 24:00 UTC vào ngày 28 tháng 8 để nâng cấp. Nếu có thắc mắc, vui lòng liên hệ với nhóm hỗ trợ của chúng tôi (support@dify.ai). Chúng tôi đánh giá cao sự kiên nhẫn của bạn.',
id_ID:
'Sistem kami tidak akan tersedia dari 19:00 hingga 24:00 UTC pada 28 Agustus untuk pemutakhiran. Untuk pertanyaan, silakan hubungi tim dukungan kami (support@dify.ai). Kami menghargai kesabaran Anda.',
tr_TR:
'Sistemimiz, 28 Ağustos\'ta 19:00 ile 24:00 UTC saatleri arasında güncelleme nedeniyle kullanılamayacaktır. Sorularınız için lütfen destek ekibimizle iletişime geçin (support@dify.ai). Sabrınız için teşekkür ederiz.',
fa_IR:

View File

@ -32,7 +32,7 @@
"value": "es-ES",
"name": "Español (España)",
"prompt_name": "Spanish",
"example": "Saluton, Dify!",
"example": "¡Hola, Dify!",
"supported": true
},
{
@ -84,13 +84,6 @@
"example": "สวัสดี Dify!",
"supported": true
},
{
"value": "id-ID",
"name": "Bahasa Indonesia",
"prompt_name": "Indonesian",
"example": "Saluto, Dify!",
"supported": false
},
{
"value": "uk-UA",
"name": "Українська (Україна)",
@ -124,14 +117,14 @@
"name": "Hindi (India)",
"prompt_name": "Hindi",
"example": "नमस्ते, Dify!",
"supported": "true"
"supported": true
},
{
"value": "tr-TR",
"name": "Türkçe",
"prompt_name": "Türkçe",
"example": "Selam!",
"supported": "true"
"supported": true
},
{
"value": "fa-IR",
@ -146,6 +139,13 @@
"prompt_name": "Slovensko",
"example": "Zdravo, Dify!",
"supported": true
},
{
"value": "id-ID",
"name": "Bahasa Indonesia",
"prompt_name": "Indonesian",
"example": "Halo, Dify!",
"supported": true
}
]
}

View File

@ -0,0 +1,97 @@
const translation = {
noData: {
title: 'Tidak ada anotasi',
description: 'Anda dapat mengedit anotasi selama penelusuran kesalahan aplikasi atau mengimpor anotasi secara massal di sini untuk respons berkualitas tinggi.',
},
table: {
header: {
answer: 'menjawab',
question: 'pertanyaan',
createdAt: 'dibuat di',
hits: 'Hits',
addAnnotation: 'Tambahkan Anotasi',
bulkImport: 'Impor Massal',
clearAllConfirm: 'Menghapus semua anotasi?',
bulkExport: 'Ekspor Massal',
clearAll: 'Hapus Semua',
actions: 'Tindakan',
},
},
editModal: {
answerPlaceholder: 'Ketik jawaban Anda di sini',
removeThisCache: 'Hapus Anotasi ini',
title: 'Edit Balas Anotasi',
yourAnswer: 'Jawaban Anda',
queryName: 'Kueri Pengguna',
queryPlaceholder: 'Ketik kueri Anda di sini',
yourQuery: 'Pertanyaan Anda',
createdAt: 'Dibuat di',
answerName: 'Bot Pendongeng',
},
addModal: {
answerName: 'Menjawab',
title: 'Tambahkan Anotasi Balasan',
queryName: 'Pertanyaan',
createNext: 'Tambahkan respons beranotasi lainnya',
queryPlaceholder: 'Ketik kueri di sini',
answerPlaceholder: 'Ketik jawaban di sini',
},
batchModal: {
ok: 'OKE',
content: 'puas',
error: 'Kesalahan Impor',
runError: 'Menjalankan batch gagal',
run: 'Jalankan Batch',
cancel: 'Membatalkan',
title: 'Impor Massal',
browse: 'ramban',
template: 'Unduh templat di sini',
tip: 'File CSV harus sesuai dengan struktur berikut:',
answer: 'menjawab',
contentTitle: 'konten potongan',
processing: 'Dalam pemrosesan batch',
completed: 'Impor selesai',
csvUploadTitle: 'Seret dan lepas file CSV Anda di sini, atau',
question: 'pertanyaan',
},
list: {
delete: {
title: 'Apakah Anda yakin Hapus?',
},
},
batchAction: {
selected: 'Dipilih',
delete: 'Menghapus',
cancel: 'Membatalkan',
},
errorMessage: {
queryRequired: 'Pertanyaan diperlukan',
answerRequired: 'Jawaban diperlukan',
},
viewModal: {
hit: 'Pukul',
hitHistory: 'Riwayat Hit',
noHitHistory: 'Tidak ada riwayat hit',
annotatedResponse: 'Balas Anotasi',
hits: 'Hits',
},
hitHistoryTable: {
response: 'Jawaban',
match: 'Korek api',
query: 'Kueri',
source: 'Sumber',
time: 'Waktu',
score: 'Skor',
},
initSetup: {
confirmBtn: 'Simpan & Aktifkan',
configTitle: 'Pengaturan Balasan Anotasi',
title: 'Pengaturan Awal Balasan Anotasi',
configConfirmBtn: 'Simpan',
},
title: 'Anotasi',
name: 'Balas Anotasi',
embeddingModelSwitchTip: 'Model vektorisasi teks anotasi, model switching akan disematkan kembali, menghasilkan biaya tambahan.',
}
export default translation

85
web/i18n/id-ID/app-api.ts Normal file
View File

@ -0,0 +1,85 @@
const translation = {
merMaid: {
rerender: 'Ulangi Render Ulang',
},
apiKeyModal: {
lastUsed: 'TERAKHIR DIGUNAKAN',
secretKey: 'Kunci Rahasia',
apiSecretKey: 'Kunci rahasia API',
created: 'DIBUAT',
apiSecretKeyTips: 'Untuk mencegah penyalahgunaan API, lindungi Kunci API Anda. Hindari menggunakannya sebagai teks biasa dalam kode front-end. :)',
generateTips: 'Simpan kunci ini di tempat yang aman dan mudah diakses.',
createNewSecretKey: 'Membuat kunci Rahasia baru',
},
actionMsg: {
deleteConfirmTips: 'Tindakan ini tidak dapat dibatalkan.',
ok: 'OKE',
deleteConfirmTitle: 'Hapus kunci rahasia ini?',
},
completionMode: {
createCompletionApi: 'Membuat Pesan Penyelesaian',
messageIDTip: 'ID Pesan',
messageFeedbackApi: 'Umpan balik pesan (seperti)',
ratingTip: 'suka atau tidak suka, null adalah undo',
parametersApi: 'Dapatkan informasi parameter aplikasi',
parametersApiTip: 'Ambil parameter Input yang dikonfigurasi, termasuk nama variabel, nama bidang, jenis, dan nilai default. Biasanya digunakan untuk menampilkan bidang ini dalam formulir atau mengisi nilai default setelah klien dimuat.',
info: 'Untuk pembuatan teks berkualitas tinggi, seperti artikel, ringkasan, dan terjemahan, gunakan API pesan penyelesaian dengan input pengguna. Pembuatan teks bergantung pada parameter model dan templat prompt yang ditetapkan di Dify Prompt Engineering.',
createCompletionApiTip: 'Buat Pesan Penyelesaian untuk mendukung mode tanya jawab.',
title: 'API Aplikasi Penyelesaian',
blocking: 'Jenis pemblokiran, menunggu eksekusi selesai dan mengembalikan hasil. (Permintaan dapat terganggu jika prosesnya panjang)',
streaming: 'streaming kembali. Implementasi pengembalian streaming berdasarkan SSE (Server-Sent Events).',
inputsTips: '(Opsional) Berikan bidang input pengguna sebagai pasangan kunci-nilai, sesuai dengan variabel di Prompt Eng. Kunci adalah nama variabel, Nilai adalah nilai parameter. Jika jenis bidang adalah Pilih, Nilai yang dikirimkan harus menjadi salah satu pilihan prasetel.',
messageFeedbackApiTip: 'Beri peringkat pesan yang diterima atas nama pengguna akhir yang suka atau tidak suka. Data ini terlihat di halaman Log & Anotasi dan digunakan untuk penyempurnaan model di masa mendatang.',
queryTips: 'Konten teks input pengguna.',
},
chatMode: {
messageFeedbackApiTip: 'Beri peringkat pesan yang diterima atas nama pengguna akhir yang suka atau tidak suka. Data ini terlihat di halaman Log & Anotasi dan digunakan untuk penyempurnaan model di masa mendatang.',
conversationRenamingApiTip: 'Ganti nama percakapan; Nama ditampilkan di antarmuka klien multi-sesi.',
conversationRenamingNameTip: 'Nama baru',
chatMsgHistoryFirstId: 'ID rekaman obrolan pertama di halaman saat ini. Defaultnya tidak ada.',
conversationsListApiTip: 'Mendapatkan daftar sesi pengguna saat ini. Secara default, 20 sesi terakhir ditampilkan.',
title: 'API Aplikasi Obrolan',
chatMsgHistoryApi: 'Mendapatkan pesan riwayat obrolan',
blocking: 'Jenis pemblokiran, menunggu eksekusi selesai dan mengembalikan hasil. (Permintaan dapat terganggu jika prosesnya panjang)',
createChatApi: 'Buat pesan obrolan',
info: 'Untuk aplikasi percakapan serbaguna yang menggunakan format Tanya Jawab, panggil API pesan obrolan untuk memulai dialog. Pertahankan percakapan yang sedang berlangsung dengan meneruskan conversation_id yang dikembalikan. Parameter respons dan templat bergantung pada pengaturan Dify Prompt Eng.',
conversationIdTip: '(Opsional) ID Percakapan: kosongkan untuk percakapan pertama kali; Teruskan conversation_id dari konteks untuk melanjutkan dialog.',
queryTips: 'Konten input/pertanyaan pengguna',
conversationsListLimitTip: 'Berapa banyak obrolan yang dikembalikan dalam satu permintaan',
chatMsgHistoryLimit: 'Berapa banyak obrolan yang dikembalikan dalam satu permintaan',
conversationsListFirstIdTip: 'ID rekaman terakhir di halaman saat ini, default tidak ada.',
messageFeedbackApi: 'Umpan balik pengguna terminal pesan, seperti',
parametersApi: 'Dapatkan informasi parameter aplikasi',
streaming: 'streaming kembali. Implementasi pengembalian streaming berdasarkan SSE (Server-Sent Events).',
inputsTips: '(Opsional) Berikan bidang input pengguna sebagai pasangan kunci-nilai, sesuai dengan variabel di Prompt Eng. Kunci adalah nama variabel, Nilai adalah nilai parameter. Jika jenis bidang adalah Pilih, Nilai yang dikirimkan harus menjadi salah satu pilihan prasetel.',
parametersApiTip: 'Ambil parameter Input yang dikonfigurasi, termasuk nama variabel, nama bidang, jenis, dan nilai default. Biasanya digunakan untuk menampilkan bidang ini dalam formulir atau mengisi nilai default setelah klien dimuat.',
chatMsgHistoryConversationIdTip: 'ID Percakapan',
messageIDTip: 'ID Pesan',
createChatApiTip: 'Buat pesan percakapan baru atau lanjutkan dialog yang ada.',
chatMsgHistoryApiTip: 'Halaman pertama mengembalikan bilah \'batas\' terbaru, yang dalam urutan terbalik.',
conversationsListApi: 'Dapatkan daftar percakapan',
ratingTip: 'suka atau tidak suka, null adalah undo',
conversationRenamingApi: 'Penggantian nama percakapan',
},
develop: {
query: 'Kueri',
toc: 'Isi',
pathParams: 'Parameter Jalur',
requestBody: 'Isi Permintaan',
},
apiServer: 'API Server',
copied: 'Disalin',
copy: 'Menyalin',
ok: 'Dalam Layanan',
regenerate: 'Regenerasi',
status: 'Keadaan',
never: 'Tidak pernah',
playing: 'Bermain',
play: 'Bermain',
disabled: 'Cacat',
apiKey: 'Kunci API',
pause: 'Jeda',
loading: 'Loading',
}
export default translation

533
web/i18n/id-ID/app-debug.ts Normal file
View File

@ -0,0 +1,533 @@
const translation = {
pageTitle: {
line1: 'CEPAT',
line2: 'Teknik',
},
promptMode: {
advancedWarning: {
ok: 'OKE',
description: 'Dalam Mode Pakar, Anda dapat mengedit seluruh PROMPT.',
learnMore: 'Pelajari lebih lanjut',
title: 'Anda telah beralih ke Mode Pakar, dan setelah Anda memodifikasi PROMPT, Anda TIDAK DAPAT kembali ke mode dasar.',
},
operation: {
addMessage: 'Tambahkan Pesan',
},
contextMissing: 'Komponen konteks terlewatkan, efektivitas prompt mungkin tidak baik.',
switchBack: 'Beralih kembali',
simple: 'Beralih ke Mode Pakar untuk mengedit seluruh PROMPT',
advanced: 'Mode Ahli',
},
operation: {
applyConfig: 'Menerbitkan',
automatic: 'Menghasilkan',
addFeature: 'Tambahkan Fitur',
cancelDisagree: 'Batalkan ketidaksukaan',
stopResponding: 'Berhenti merespons',
disagree: 'tidak suka',
debugConfig: 'Awakutu',
agree: 'suka',
userAction: 'Pengguna',
cancelAgree: 'Batalkan seperti',
resetConfig: 'Reset',
},
notSetAPIKey: {
title: 'Kunci penyedia LLM belum diatur',
description: 'Kunci penyedia LLM belum diatur, dan perlu diatur sebelum debugging.',
settingBtn: 'Buka pengaturan',
trailFinished: 'Jejak selesai',
},
trailUseGPT4Info: {
description: 'Gunakan gpt-4, silakan atur Kunci API.',
title: 'Tidak mendukung gpt-4 sekarang',
},
feature: {
groupChat: {
title: 'Peningkatan obrolan',
description: 'Menambahkan pengaturan pra-percakapan untuk aplikasi dapat meningkatkan pengalaman pengguna.',
},
groupExperience: {
title: 'Pengalaman meningkatkan',
},
conversationOpener: {
description: 'Dalam aplikasi obrolan, kalimat pertama yang AI secara aktif berbicara kepada pengguna biasanya digunakan sebagai sambutan.',
title: 'Pembuka Percakapan',
},
suggestedQuestionsAfterAnswer: {
description: 'Menyiapkan saran pertanyaan berikutnya dapat memberi pengguna obrolan yang lebih baik.',
title: 'Tindak lanjut',
tryToAsk: 'Cobalah untuk bertanya',
resDes: '3 saran untuk pertanyaan pengguna berikutnya.',
},
moreLikeThis: {
tip: 'Menggunakan fitur ini akan dikenakan overhead token tambahan',
description: 'Buat beberapa teks sekaligus, lalu edit dan lanjutkan membuat',
title: 'Lebih seperti ini',
generateNumTip: 'Jumlah setiap kali yang dihasilkan',
},
speechToText: {
resDes: 'Input suara diaktifkan',
title: 'Ucapan ke Teks',
description: 'Input suara dapat digunakan dalam obrolan.',
},
textToSpeech: {
resDes: 'Teks ke Audio diaktifkan',
title: 'Teks ke Ucapan',
description: 'Pesan percakapan dapat diubah menjadi ucapan.',
},
citation: {
description: 'Tampilkan dokumen sumber dan bagian atribut dari konten yang dihasilkan.',
title: 'Kutipan dan Atribusi',
resDes: 'Kutipan dan Atribusi diaktifkan',
},
annotation: {
scoreThreshold: {
accurateMatch: 'Kecocokan yang Akurat',
title: 'Ambang Skor',
description: 'Digunakan untuk menetapkan ambang batas kesamaan untuk balasan anotasi.',
easyMatch: 'Pencocokan Mudah',
},
matchVariable: {
choosePlaceholder: 'Pilih variabel pencocokan',
title: 'Cocokkan Variabel',
},
add: 'Menambahkan anotasi',
cached: 'Beranotasi',
description: 'Anda dapat menambahkan respons berkualitas tinggi secara manual ke cache untuk pencocokan prioritas dengan pertanyaan pengguna serupa.',
edit: 'Edit anotasi',
resDes: 'Respons Anotasi diaktifkan',
cacheManagement: 'Anotasi',
remove: 'Buka',
title: 'Balas Anotasi',
removeConfirm: 'Hapus anotasi ini ?',
},
dataSet: {
queryVariable: {
choosePlaceholder: 'Pilih variabel kueri',
tip: 'Variabel ini akan digunakan sebagai input kueri untuk pengambilan konteks, mendapatkan informasi konteks yang terkait dengan input variabel ini.',
unableToQueryDataSet: 'Tidak dapat mengkueri Pengetahuan',
title: 'Variabel kueri',
noVar: 'Non-variabel',
unableToQueryDataSetTip: 'Tidak dapat berhasil mengkueri Pengetahuan, silakan pilih variabel kueri konteks di bagian konteks.',
contextVarNotEmpty: 'Variabel kueri konteks tidak dapat kosong',
deleteContextVarTip: 'Variabel ini telah ditetapkan sebagai variabel kueri konteks, dan menghapusnya akan berdampak pada penggunaan normal Pengetahuan. Jika Anda masih perlu menghapusnya, silakan pilih kembali di bagian konteks.',
ok: 'OKE',
noVarTip: 'silakan buat variabel di bawah bagian Variabel',
},
notSupportSelectMulti: 'Saat ini hanya mendukung satu Pengetahuan',
textBlocks: 'Blok Teks',
selectTitle: 'Pilih referensi Pengetahuan',
words: 'Kata',
toCreate: 'Pergi ke membuat',
noDataSet: 'Tidak ada Pengetahuan yang ditemukan',
noData: 'Anda dapat mengimpor Pengetahuan sebagai konteks',
title: 'Pengetahuan',
selected: 'Pengetahuan yang dipilih',
},
tools: {
modal: {
toolType: {
title: 'Jenis Alat',
placeholder: 'Silakan pilih jenis alat',
},
name: {
title: 'Nama',
placeholder: 'Silakan masukkan nama',
},
variableName: {
placeholder: 'Silakan masukkan nama variabel',
title: 'Nama Variabel',
},
title: 'Alat',
},
tips: 'Alat menyediakan metode panggilan API standar, mengambil input atau variabel pengguna sebagai parameter permintaan untuk mengkueri data eksternal sebagai konteks.',
title: 'Perkakas',
},
conversationHistory: {
editModal: {
assistantPrefix: 'Awalan asisten',
userPrefix: 'Awalan pengguna',
title: 'Mengedit Nama Peran Percakapan',
},
description: 'Menetapkan nama awalan untuk peran percakapan',
title: 'Riwayat Percakapan',
learnMore: 'Pelajari lebih lanjut',
},
toolbox: {
title: 'TOOLBOX',
},
moderation: {
modal: {
provider: {
openaiTip: {
suffix: '.',
prefix: 'OpenAI Moderation memerlukan kunci OpenAI API yang dikonfigurasi di',
},
title: 'Penyedia',
keywords: 'Kata kunci',
openai: 'Moderasi OpenAI',
},
keywords: {
line: 'Garis',
placeholder: 'Satu per baris, dipisahkan oleh jeda baris',
tip: 'Satu per baris, dipisahkan oleh jeda baris. Hingga 100 karakter per baris.',
},
content: {
preset: 'Balasan yang telah ditetapkan sebelumnya',
input: 'Konten INPUT Moderatif',
output: 'Konten OUTPUT Moderatif',
errorMessage: 'Balasan prasetel tidak boleh kosong',
condition: 'Konten INPUT dan OUTPUT moderat mengaktifkan setidaknya satu',
placeholder: 'Konten balasan preset di sini',
supportMarkdown: 'Penurunan harga didukung',
fromApi: 'Balasan prasetel dikembalikan oleh API',
},
openaiNotConfig: {
after: '',
before: 'OpenAI Moderation memerlukan kunci OpenAI API yang dikonfigurasi di',
},
title: 'Setelan moderasi konten',
},
title: 'Moderasi konten',
outputEnabled: 'HASIL',
contentEnableLabel: 'Moderasi konten diaktifkan',
inputEnabled: 'MASUKAN',
allEnabled: 'MASUKAN & KELUARAN',
description: 'Amankan output model dengan menggunakan API moderasi atau mempertahankan daftar kata yang sensitif.',
},
fileUpload: {
title: 'Unggah File',
supportedTypes: 'Jenis File Dukungan',
numberLimit: 'Upload maksimal',
modalTitle: 'Pengaturan Unggahan File',
description: 'Kotak input obrolan memungkinkan pengunggahan gambar, dokumen, dan file lainnya.',
},
imageUpload: {
supportedTypes: 'Jenis File Dukungan',
description: 'Izinkan mengunggah gambar.',
modalTitle: 'Pengaturan Unggahan Gambar',
numberLimit: 'Upload maksimal',
title: 'Unggah Gambar',
},
bar: {
empty: 'Aktifkan fitur untuk meningkatkan pengalaman pengguna aplikasi web',
enableText: 'Fitur Diaktifkan',
manage: 'Urus',
},
documentUpload: {
title: 'Surat',
description: 'Aktifkan Dokumen akan memungkinkan model untuk mengambil dokumen dan menjawab pertanyaan tentangnya.',
},
audioUpload: {
title: 'Audio',
description: 'Aktifkan Audio akan memungkinkan model untuk memproses file audio untuk transkripsi dan analisis.',
},
},
codegen: {
title: 'Pembuat Kode',
noDataLine2: 'Pratinjau kode akan ditampilkan di sini.',
apply: 'Berlaku',
instruction: 'Peraturan',
description: 'Pembuat Kode menggunakan model yang dikonfigurasi untuk menghasilkan kode berkualitas tinggi berdasarkan instruksi Anda. Harap berikan instruksi yang jelas dan terperinci.',
loading: 'Menghasilkan kode...',
overwriteConfirmMessage: 'Tindakan ini akan menimpa kode yang ada. Apakah Anda ingin melanjutkan?',
generate: 'Menghasilkan',
generatedCodeTitle: 'Kode yang Dihasilkan',
overwriteConfirmTitle: 'Menimpa kode yang ada?',
resTitle: 'Kode yang Dihasilkan',
instructionPlaceholder: 'Masukkan deskripsi terperinci tentang kode yang ingin Anda hasilkan.',
applyChanges: 'Terapkan Perubahan',
noDataLine1: 'Jelaskan kasus penggunaan Anda di sebelah kiri,',
},
generate: {
template: {
pythonDebugger: {
name: 'Debugger Python',
instruction: 'Bot yang dapat menghasilkan dan men-debug kode Anda berdasarkan instruksi Anda',
},
translation: {
name: 'Terjemahan',
instruction: 'Penerjemah yang dapat menerjemahkan berbagai bahasa',
},
professionalAnalyst: {
name: 'Analis profesional',
instruction: 'Ekstrak wawasan, identifikasi risiko, dan suling informasi penting dari laporan panjang ke dalam satu memo',
},
excelFormulaExpert: {
name: 'Pakar rumus Excel',
instruction: 'Chatbot yang dapat membantu pengguna pemula memahami, menggunakan, dan membuat rumus Excel berdasarkan instruksi pengguna',
},
travelPlanning: {
name: 'Perencanaan perjalanan',
instruction: 'Asisten Perencanaan Perjalanan adalah alat cerdas yang dirancang untuk membantu pengguna merencanakan perjalanan mereka dengan mudah',
},
SQLSorcerer: {
instruction: 'Mengubah bahasa sehari-hari menjadi kueri SQL',
name: 'Penyihir SQL',
},
GitGud: {
instruction: 'Hasilkan perintah Git yang sesuai berdasarkan tindakan kontrol versi yang dijelaskan pengguna',
name: 'Git gud',
},
meetingTakeaways: {
name: 'Kesimpulan rapat',
instruction: 'Suling rapat menjadi ringkasan ringkas termasuk topik diskusi, poin penting, dan item tindakan',
},
writingsPolisher: {
name: 'Menulis pemoles',
instruction: 'Gunakan teknik copyediting canggih untuk meningkatkan tulisan Anda',
},
},
overwriteMessage: 'Menerapkan prompt ini akan mengganti konfigurasi yang ada.',
title: 'Prompt Generator',
tryIt: 'Cobalah',
idealOutputPlaceholder: 'Jelaskan format respons ideal Anda, panjang, nada, dan persyaratan konten...',
resTitle: 'Prompt yang Dihasilkan',
overwriteTitle: 'Ganti konfigurasi yang ada?',
optional: 'Fakultatif',
instruction: 'Peraturan',
versions: 'Versi',
to: 'ke',
press: 'Peras',
latest: 'Terbaru',
version: 'Versi',
generate: 'Menghasilkan',
loading: 'Mengatur aplikasi untuk Anda...',
optimizePromptTooltip: 'Optimalkan di Prompt Generator',
apply: 'Berlaku',
instructionPlaceHolderLine3: 'Nadanya terlalu keras, tolong buat lebih ramah.',
optimizationNote: 'Catatan Pengoptimalan',
dismiss: 'Mengabaikan',
description: 'Prompt Generator menggunakan model yang dikonfigurasi untuk mengoptimalkan prompt untuk kualitas yang lebih tinggi dan struktur yang lebih baik. Silakan tulis instruksi yang jelas dan terperinci.',
idealOutput: 'Keluaran Ideal',
codeGenInstructionPlaceHolderLine: 'Semakin detail umpan balik, seperti jenis data input dan output serta bagaimana variabel diproses, semakin akurat pembuatan kode.',
newNoDataLine1: 'Tulis instruksi di kolom kiri, dan klik Hasilkan untuk melihat respons.',
instructionPlaceHolderTitle: 'Jelaskan bagaimana Anda ingin meningkatkan Prompt ini. Misalnya:',
instructionPlaceHolderLine1: 'Buat output lebih ringkas, pertahankan poin-poin inti.',
insertContext: 'Sisipkan konteks',
instructionPlaceHolderLine2: 'Format keluarannya salah, harap ikuti format JSON dengan ketat.',
},
resetConfig: {
title: 'Mengonfirmasi reset?',
message: 'Atur ulang membuang perubahan, memulihkan konfigurasi terakhir yang diterbitkan.',
},
errorMessage: {
queryRequired: 'Teks permintaan diperlukan.',
waitForImgUpload: 'Silakan tunggu gambar diunggah',
notSelectModel: 'Silakan pilih model',
waitForResponse: 'Harap tunggu hingga respons terhadap pesan sebelumnya selesai.',
waitForBatchResponse: 'Harap tunggu hingga respons terhadap tugas batch selesai.',
waitForFileUpload: 'Harap tunggu file/file diunggah',
},
warningMessage: {
timeoutExceeded: 'Hasil tidak ditampilkan karena batas waktu. Silakan lihat log untuk mengumpulkan hasil lengkap.',
},
variableTable: {
action: 'Tindakan',
typeString: 'Tali',
optional: 'Fakultatif',
typeSelect: 'Pilih',
type: 'Jenis Masukan',
key: 'Kunci Variabel',
name: 'Nama Bidang Input Pengguna',
},
varKeyError: {},
otherError: {
queryNoBeEmpty: 'Kueri harus diatur dalam prompt',
historyNoBeEmpty: 'Riwayat percakapan harus diatur dalam prompt',
promptNoBeEmpty: 'Prompt tidak bisa kosong',
},
variableConfig: {
'file': {
image: {
name: 'Citra',
},
audio: {
name: 'Audio',
},
document: {
name: 'Surat',
},
video: {
name: 'Video',
},
custom: {
name: 'Jenis file lainnya',
description: 'Tentukan jenis file lainnya.',
createPlaceholder: ' Ekstensi file, misalnya .doc',
},
supportFileTypes: 'Jenis File Dukungan',
},
'errorMsg': {
atLeastOneOption: 'Setidaknya satu opsi diperlukan',
labelNameRequired: 'Nama label diperlukan',
optionRepeat: 'Memiliki opsi pengulangan',
varNameCanBeRepeat: 'Nama variabel tidak dapat diulang',
},
'string': 'Teks Singkat',
'single-file': 'File Tunggal',
'labelName': 'Nama Label',
'number': 'Angka',
'json': 'Kode JSON',
'optional': 'fakultatif',
'addOption': 'Tambahkan opsi',
'checkbox': 'Kotak centang',
'hide': 'Menyembunyikan',
'required': 'Diperlukan',
'maxLength': 'Panjang maks',
'select': 'Pilih',
'paragraph': 'Paragraf',
'options': 'Pilihan',
'apiBasedVar': 'Variabel berbasis API',
'varName': 'Nama Variabel',
'editModalTitle': 'Edit Bidang Input',
'multi-files': 'Daftar File',
'text-input': 'Teks Singkat',
'content': 'Puas',
'inputPlaceholder': 'Silakan masukkan',
'selectDefaultValue': 'Pilih nilai default',
'fieldType': 'Jenis bidang',
'addModalTitle': 'Tambahkan Bidang Input',
'stringTitle': 'Opsi kotak teks formulir',
'jsonSchema': 'Skema JSON',
'noDefaultValue': 'Tidak ada nilai default',
'defaultValue': 'Nilai default',
'localUpload': 'Unggahan Lokal',
'maxNumberOfUploads': 'Jumlah upload maksimal',
'both': 'Keduanya',
'uploadFileTypes': 'Unggah Jenis File',
},
vision: {
visionSettings: {
both: 'Keduanya',
high: 'Tinggi',
uploadMethod: 'Metode Unggah',
title: 'Pengaturan Penglihatan',
localUpload: 'Unggahan Lokal',
low: 'Rendah',
uploadLimit: 'Batas Unggahan',
resolution: 'Resolusi',
url: 'URL',
},
settings: 'Pengaturan',
description: 'Aktifkan Penglihatan akan memungkinkan model untuk mengambil gambar dan menjawab pertanyaan tentangnya.',
onlySupportVisionModelTip: 'Hanya mendukung model penglihatan',
name: 'Penglihatan',
},
voice: {
voiceSettings: {
autoPlayEnabled: 'Di atas',
voice: 'Suara',
language: 'Bahasa',
title: 'Pengaturan Suara',
autoPlay: 'Putar Otomatis',
autoPlayDisabled: 'Off',
resolutionTooltip: 'Bahasa pendukung suara text-to-speech。',
},
settings: 'Pengaturan',
defaultDisplay: 'Suara Default',
description: 'Pengaturan suara teks ke ucapan',
name: 'Suara',
},
openingStatement: {
openingQuestion: 'Pertanyaan Pembuka',
add: 'Tambah',
writeOpener: 'Edit pembuka',
title: 'Pembuka Percakapan',
noDataPlaceHolder: 'Memulai percakapan dengan pengguna dapat membantu AI menjalin hubungan yang lebih dekat dengan mereka dalam aplikasi percakapan.',
tooShort: 'Setidaknya 20 kata prompt awal diperlukan untuk menghasilkan pidato pembuka untuk percakapan.',
},
modelConfig: {
modeType: {
completion: 'Lengkap',
chat: 'Mengobrol',
},
title: 'Model dan Parameter',
model: 'Pola',
setTone: 'Atur nada respons',
},
inputs: {
queryPlaceholder: 'Silakan masukkan teks permintaan.',
run: 'LARI',
completionVarTip: 'Isi nilai variabel, yang akan secara otomatis diganti dengan kata-kata prompt setiap kali pertanyaan diajukan.',
noVar: 'Isi nilai variabel, yang akan secara otomatis diganti dalam kata prompt setiap kali sesi baru dimulai.',
noPrompt: 'Coba tulis beberapa prompt dalam input pra-prompt',
previewTitle: 'Pratinjau prompt',
userInputField: 'Bidang Input Pengguna',
queryTitle: 'Kueri konten',
title: 'Debug & Pratinjau',
chatVarTip: 'Isi nilai variabel, yang akan secara otomatis diganti dalam kata prompt setiap kali sesi baru dimulai',
},
datasetConfig: {
retrieveOneWay: {
title: 'Pengambilan N-ke-1',
description: 'Berdasarkan maksud pengguna dan deskripsi Pengetahuan, Agen secara mandiri memilih Pengetahuan terbaik untuk kueri. Terbaik untuk aplikasi dengan Pengetahuan yang berbeda dan terbatas.',
},
retrieveMultiWay: {
title: 'Pengambilan multi-jalur',
description: 'Berdasarkan maksud pengguna, kueri di semua Pengetahuan, mengambil teks yang relevan dari berbagai sumber, dan memilih hasil terbaik yang cocok dengan kueri pengguna setelah peringkat ulang.',
},
rerankModelRequired: 'Model Rerank yang dikonfigurasi diperlukan',
top_k: 'K Teratas',
score_threshold: 'Ambang Skor',
settingTitle: 'Pengaturan pengambilan',
score_thresholdTip: 'Digunakan untuk mengatur ambang kesamaan untuk pemfilteran potongan.',
retrieveChangeTip: 'Memodifikasi mode indeks dan mode pengambilan dapat memengaruhi aplikasi yang terkait dengan Pengetahuan ini.',
embeddingModelRequired: 'Model Penyematan yang dikonfigurasi diperlukan',
params: 'Parameter',
top_kTip: 'Digunakan untuk memfilter potongan yang paling mirip dengan pertanyaan pengguna. Sistem juga akan secara dinamis menyesuaikan nilai Top K, sesuai dengan max_tokens model yang dipilih.',
knowledgeTip: 'Klik tombol " " untuk menambahkan pengetahuan',
},
assistantType: {
chatAssistant: {
description: 'Membuat asisten berbasis obrolan menggunakan Model Bahasa Besar',
name: 'Asisten Dasar',
},
agentAssistant: {
description: 'Bangun Agen cerdas yang dapat secara mandiri memilih alat untuk menyelesaikan tugas',
name: 'Asisten Agen',
},
name: 'Jenis Asisten',
},
agent: {
agentModeType: {
ReACT: 'Bereaksi',
functionCall: 'Fungsi Panggilan',
},
setting: {
maximumIterations: {
name: 'Iterasi Maksimum',
description: 'Membatasi jumlah iterasi yang dapat dijalankan oleh asisten agen',
},
description: 'Pengaturan Asisten Agen memungkinkan pengaturan mode agen dan fitur lanjutan seperti perintah bawaan, hanya tersedia dalam jenis Agen.',
name: 'Pengaturan Agen',
},
tools: {
enabled: 'Diaktifkan',
name: 'Perkakas',
description: 'Menggunakan alat dapat memperluas kemampuan LLM, seperti mencari di internet atau melakukan perhitungan ilmiah',
},
agentModeDes: 'Mengatur jenis mode inferensi untuk agen',
firstPrompt: 'Prompt Pertama',
buildInPrompt: 'Perintah Bawaan',
agentMode: 'Mode Agen',
nextIteration: 'Iterasi Berikutnya',
promptPlaceholder: 'Tulis prompt Anda di sini',
},
orchestrate: 'Mengatur',
chatSubTitle: 'Peraturan',
variableTitle: 'Variabel',
variableTip: 'Pengguna mengisi variabel dalam formulir, secara otomatis mengganti variabel dalam perintah.',
formattingChangedText: 'Memodifikasi pemformatan akan mengatur ulang area debug, apakah Anda yakin?',
completionSubTitle: 'Prompt Awalan',
debugAsMultipleModel: 'Debug sebagai Beberapa Model',
publishAs: 'Publikasikan sebagai',
autoAddVar: 'Variabel yang tidak ditentukan direferensikan dalam pra-prompt, apakah Anda ingin menambahkannya dalam formulir input pengguna?',
debugAsSingleModel: 'Debug sebagai Model Tunggal',
formattingChangedTitle: 'Pemformatan diubah',
duplicateModel: 'Duplikat',
result: 'Teks Keluaran',
noResult: 'Output akan ditampilkan di sini.',
}
export default translation

96
web/i18n/id-ID/app-log.ts Normal file
View File

@ -0,0 +1,96 @@
const translation = {
table: {
header: {
output: 'Hasil',
version: 'VERSI',
time: 'Waktu yang dibuat',
messageCount: 'Jumlah Pesan',
summary: 'Titel',
adminRate: 'Tingkat Op.',
user: 'Pengguna Akhir atau Akun',
startTime: 'WAKTU MULAI',
updatedTime: 'Waktu yang diperbarui',
tokens: 'TOKEN',
endUser: 'Pengguna Akhir atau Akun',
userRate: 'Tarif Pengguna',
input: 'Masukan',
status: 'KEADAAN',
runtime: 'WAKTU BERJALAN',
},
pagination: {
previous: 'Prev',
next: 'Depan',
},
empty: {
element: {
title: 'Apakah ada yang ada di sana?',
},
noChat: 'Belum ada percakapan',
noOutput: 'Tidak ada keluaran',
},
},
detail: {
timeConsuming: '',
operation: {
dislike: 'tidak suka',
like: 'suka',
addAnnotation: 'Tambahkan Peningkatan',
editAnnotation: 'Edit Peningkatan',
annotationPlaceholder: 'Masukkan jawaban yang diharapkan yang Anda inginkan untuk dibalas AI, yang dapat digunakan untuk penyempurnaan model dan peningkatan berkelanjutan kualitas pembuatan teks di masa mendatang.',
},
time: 'Waktu',
loading: 'Loading',
variables: 'Variabel',
tokenCost: 'Token yang dibelanjakan',
modelParams: 'Parameter model',
conversationId: 'ID Percakapan',
promptTemplate: 'Templat Prompt',
second: 's',
promptTemplateBeforeChat: 'Templat Prompt Sebelum Obrolan · Sebagai Pesan Sistem',
uploadImages: 'Gambar yang Diunggah',
},
filter: {
period: {
today: 'Hari Ini',
last4weeks: '4 minggu terakhir',
quarterToDate: 'Kuartal hingga saat ini',
last7days: '7 Hari Terakhir',
monthToDate: 'Bulan hingga saat ini',
last3months: '3 bulan terakhir',
yearToDate: 'Tahun hingga saat ini',
allTime: 'Sepanjang masa',
last12months: '12 bulan terakhir',
},
annotation: {
all: 'Semua',
not_annotated: 'Tidak Beranotasi',
},
ascending: 'Naik',
descending: 'Turun',
sortBy: 'Kota hitam:',
},
runDetail: {
fileListDetail: 'Detail',
workflowTitle: 'Log Detail',
title: 'Log Percakapan',
fileListLabel: 'Rincian File',
},
agentLogDetail: {
iterations: 'Iterasi',
finalProcessing: 'Pemrosesan Akhir',
iteration: 'Iterasi',
toolUsed: 'Alat yang Digunakan',
agentMode: 'Mode Agen',
},
title: 'Log',
description: 'Log mencatat status berjalan aplikasi, termasuk input pengguna dan balasan AI.',
dateFormat: 'MM / DD / YYYY',
agentLog: 'Log Agen',
viewLog: 'Lihat Log',
dateTimeFormat: 'MM/DD/YYYY hh:mm:ss A',
promptLog: 'Prompt Log',
workflowSubtitle: 'Log mencatat pengoperasian Automate.',
workflowTitle: 'Log Alur Kerja',
}
export default translation

View File

@ -0,0 +1,171 @@
const translation = {
welcome: {
firstStepTip: 'Untuk memulai,',
placeholder: 'Kunci API OpenAI Anda (misalnya.sk-xxxx)',
enterKeyTip: 'masukkan Kunci API OpenAI Anda di bawah ini',
getKeyTip: 'Dapatkan Kunci API Anda dari dasbor OpenAI',
},
apiKeyInfo: {
cloud: {
trial: {
description: 'Kuota uji coba disediakan untuk tujuan pengujian Anda. Sebelum kuota uji coba habis, harap siapkan penyedia model Anda sendiri atau beli kuota tambahan.',
},
exhausted: {
title: 'Kuota uji coba Anda telah habis, silakan atur APIKey Anda.',
description: 'Anda telah menghabiskan kuota percobaan Anda. Silakan siapkan penyedia model Anda sendiri atau beli kuota tambahan.',
},
},
selfHost: {
title: {
row1: 'Untuk memulai,',
row2: 'Siapkan penyedia model Anda terlebih dahulu.',
},
},
usedToken: 'Token bekas',
callTimes: 'Waktu panggilan',
tryCloud: 'Atau coba Dify versi cloud dengan penawaran gratis',
setAPIBtn: 'Buka penyedia model penyiapan',
},
overview: {
appInfo: {
settings: {
workflow: {
hide: 'Menyembunyikan',
subTitle: 'Detail Alur Kerja',
showDesc: 'Menampilkan atau menyembunyikan detail alur kerja di aplikasi web',
title: 'Alur Kerja',
show: 'Memperlihatkan',
},
sso: {
label: 'Penegakan SSO',
tooltip: 'Hubungi administrator untuk mengaktifkan SSO aplikasi web',
title: 'aplikasi web SSO',
description: 'Semua pengguna diharuskan masuk dengan SSO sebelum menggunakan aplikasi web',
},
more: {
customDisclaimerPlaceholder: 'Masukkan teks penafian khusus',
copyrightTooltip: 'Silakan tingkatkan ke paket Profesional atau lebih tinggi',
entry: 'Tampilkan setelan lainnya',
copyRightPlaceholder: 'Masukkan nama penulis atau organisasi',
copyrightTip: 'Menampilkan informasi hak cipta di aplikasi web',
privacyPolicy: 'Kebijakan Privasi',
customDisclaimer: 'Penafian Kustom',
privacyPolicyPlaceholder: 'Masukkan tautan kebijakan privasi',
customDisclaimerTip: 'Teks penafian khusus akan ditampilkan di sisi klien, memberikan informasi tambahan tentang aplikasi',
copyright: 'Hak cipta',
},
chatColorThemeInverted: 'Terbalik',
invalidPrivacyPolicy: 'Tautan kebijakan privasi tidak valid. Silakan gunakan tautan valid yang dimulai dengan http atau https',
language: 'Bahasa',
invalidHexMessage: 'Nilai hex tidak valid',
webName: 'Nama aplikasi web',
webDescPlaceholder: 'Masukkan deskripsi aplikasi web',
chatColorThemeDesc: 'Atur tema warna chatbot',
modalTip: 'Pengaturan aplikasi web sisi klien.',
title: 'Pengaturan Aplikasi Web',
webDescTip: 'Teks ini akan ditampilkan di sisi klien, memberikan panduan dasar tentang cara menggunakan aplikasi',
entry: 'Pengaturan',
chatColorTheme: 'Tema warna obrolan',
webDesc: 'Deskripsi aplikasi web',
},
embedded: {
copied: 'Disalin',
title: 'Sematkan di situs web',
entry: 'Tertanam',
explanation: 'Pilih cara menyematkan aplikasi obrolan ke situs web Anda',
copy: 'Menyalin',
chromePlugin: 'Instal Ekstensi Chrome Dify Chatbot',
iframe: 'Untuk menambahkan aplikasi obrolan di mana saja di situs web Anda, tambahkan iframe ini ke kode html Anda.',
scripts: 'Untuk menambahkan aplikasi obrolan ke kanan bawah situs web Anda, tambahkan kode ini ke html Anda.',
},
qrcode: {
scan: 'Pindai Untuk Berbagi',
download: 'Unduh Kode QR',
title: 'Tautan Kode QR',
},
customize: {
way1: {
step1Operation: 'Dify-WebClient',
step2Operation: 'Impor repositori',
step1: 'Fork kode klien dan modifikasi',
step2Tip: 'Klik di sini untuk mengimpor repositori ke Vercel dan menyebarkan',
step2: 'Terapkan ke Vercel',
name: 'Fork kode klien, modifikasi dan terapkan ke Vercel (disarankan)',
step3: 'Mengonfigurasi variabel lingkungan',
step1Tip: 'Klik di sini untuk melakukan fork kode sumber ke akun GitHub Anda dan memodifikasi kode',
step3Tip: 'Tambahkan variabel lingkungan berikut di Vercel',
},
way2: {
operation: 'Dokumentasi',
name: 'Tulis kode sisi klien untuk memanggil API dan menyebarkannya ke server',
},
way: 'jalan',
entry: 'Menyesuaikan',
title: 'Sesuaikan aplikasi web AI',
explanation: 'Anda dapat menyesuaikan frontend Aplikasi Web agar sesuai dengan skenario dan kebutuhan gaya Anda.',
},
launch: 'Luncur',
regenerate: 'Regenerasi',
preview: 'Pratayang',
accessibleAddress: 'URL publik',
preUseReminder: 'Harap aktifkan aplikasi web sebelum melanjutkan.',
regenerateNotice: 'Apakah Anda ingin membuat ulang URL publik?',
explanation: 'Aplikasi web AI siap pakai',
},
apiInfo: {
accessibleAddress: 'Titik Akhir API Layanan',
title: 'API Layanan Backend',
doc: 'Referensi API',
explanation: 'Mudah diintegrasikan ke dalam aplikasi Anda',
},
status: {
disable: 'Cacat',
running: 'Dalam Layanan',
},
title: 'Ikhtisar',
},
analysis: {
totalMessages: {
explanation: 'Interaksi AI harian diperhitungkan.',
title: 'Total Pesan',
},
totalConversations: {
title: 'Total Percakapan',
explanation: 'Percakapan AI harian diperhitungkan; Pengecualian rekayasa/debugging prompt.',
},
activeUsers: {
explanation: 'Pengguna unik yang terlibat dalam Tanya Jawab dengan AI; Pengecualian rekayasa/debugging prompt.',
title: 'Pengguna Aktif',
},
tokenUsage: {
title: 'Penggunaan Token',
explanation: 'Mencerminkan penggunaan token harian dari model bahasa untuk aplikasi, berguna untuk tujuan pengendalian biaya.',
consumed: 'Dikonsumsi',
},
avgSessionInteractions: {
title: 'Interaksi Sesi Rata-rata',
explanation: 'Jumlah komunikasi pengguna-AI yang berkelanjutan; untuk aplikasi berbasis percakapan.',
},
avgUserInteractions: {
explanation: 'Mencerminkan frekuensi penggunaan harian pengguna. Metrik ini mencerminkan kelengketan pengguna.',
title: 'Rata-rata Interaksi Pengguna',
},
userSatisfactionRate: {
title: 'Tingkat Kepuasan Pengguna',
explanation: 'Jumlah suka per 1.000 pesan. Ini menunjukkan proporsi jawaban yang sangat dipuaskan pengguna.',
},
avgResponseTime: {
explanation: 'Waktu (ms) bagi AI untuk memproses/merespons; untuk aplikasi berbasis teks.',
title: 'Rata-rata Waktu Respons',
},
tps: {
title: 'Kecepatan Keluaran Token',
explanation: 'Mengukur kinerja LLM. Hitung kecepatan keluaran Token LLM dari awal permintaan hingga penyelesaian output.',
},
tokenPS: 'Token',
title: 'Analisis',
ms: 'Ms',
},
}
export default translation

305
web/i18n/id-ID/app.ts Normal file
View File

@ -0,0 +1,305 @@
const translation = {
types: {
chatbot: 'Chatbot',
workflow: 'Alur Kerja',
advanced: 'Alur obrolan',
agent: 'Agen',
completion: 'Penyelesaian',
basic: 'Dasar',
all: 'Semua',
},
mermaid: {
handDrawn: 'Digambar Tangan',
classic: 'Klasik',
},
dslUploader: {
browse: 'Ramban',
button: 'Seret dan lepas file, atau',
},
newApp: {
chatbotUserDescription: 'Bangun chatbot berbasis LLM dengan cepat dengan konfigurasi sederhana. Anda dapat beralih ke Chatflow nanti.',
agentShortDescription: 'Agen cerdas dengan penalaran dan penggunaan alat otonom',
noTemplateFound: 'Tidak ada templat yang ditemukan',
appCreated: 'Aplikasi dibuat',
appNamePlaceholder: 'Beri nama aplikasi Anda',
appCreateDSLErrorPart3: 'Versi DSL aplikasi saat ini:',
Cancel: 'Membatalkan',
previewDemo: 'Pratinjau demo',
appCreateDSLWarning: 'Perhatian: Perbedaan versi DSL dapat memengaruhi fitur tertentu',
appCreateDSLErrorPart1: 'Perbedaan yang signifikan dalam versi DSL telah terdeteksi. Memaksa impor dapat menyebabkan aplikasi tidak berfungsi.',
chatApp: 'Asisten',
workflowWarning: 'Saat ini dalam versi beta',
completionShortDescription: 'Asisten AI untuk tugas pembuatan teks',
startFromBlank: 'Buat dari Kosong',
captionDescription: 'Deskripsi',
forBeginners: 'Jenis aplikasi yang lebih dasar',
noIdeaTip: 'Tidak ada ide? Lihat templat kami',
completionUserDescription: 'Buat asisten AI dengan cepat untuk tugas pembuatan teks dengan konfigurasi sederhana.',
forAdvanced: 'UNTUK PENGGUNA TINGKAT LANJUT',
workflowUserDescription: 'Bangun alur kerja AI otonom secara visual dengan kesederhanaan seret dan lepas.',
learnMore: 'Pelajari lebih lanjut',
agentUserDescription: 'Agen cerdas yang mampu penalaran berulang dan penggunaan alat otonom untuk mencapai tujuan tugas.',
noAppsFound: 'Tidak ada aplikasi yang ditemukan',
startFromTemplate: 'Buat dari Template',
appDescriptionPlaceholder: 'Masukkan deskripsi aplikasi',
captionName: 'Nama & Ikon Aplikasi',
showTemplates: 'Saya ingin memilih dari templat',
caution: 'Hati',
chatbotShortDescription: 'Chatbot berbasis LLM dengan pengaturan sederhana',
Confirm: 'Mengkonfirmasi',
agentAssistant: 'Asisten Agen Baru',
appCreateFailed: 'Gagal membuat aplikasi',
appCreateDSLErrorTitle: 'Ketidakcocokan Versi',
chatAppIntro: 'Saya ingin membangun aplikasi berbasis obrolan. Aplikasi ini menggunakan format tanya jawab, memungkinkan beberapa putaran percakapan berkelanjutan.',
nameNotEmpty: 'Nama tidak boleh kosong',
appTemplateNotSelected: 'Silakan pilih templat',
noTemplateFoundTip: 'Coba cari menggunakan kata kunci yang berbeda.',
appCreateDSLErrorPart4: 'Versi DSL yang didukung sistem:',
appTypeRequired: 'Silakan pilih jenis aplikasi',
advancedShortDescription: 'Alur kerja disempurnakan untuk obrolan multi-giliran',
completeAppIntro: 'Saya ingin membuat aplikasi yang menghasilkan teks berkualitas tinggi berdasarkan petunjuk, seperti menghasilkan artikel, ringkasan, terjemahan, dan banyak lagi.',
Create: 'Menciptakan',
advancedUserDescription: 'Alur kerja dengan fitur memori tambahan dan antarmuka chatbot.',
dropDSLToCreateApp: 'Jatuhkan file DSL di sini untuk membuat aplikasi',
completeApp: 'Pembuat Teks',
optional: 'Fakultatif',
workflowShortDescription: 'Aliran agen untuk otomatisasi cerdas',
chooseAppType: 'Pilih Jenis App',
hideTemplates: 'Kembali ke pemilihan mode',
useTemplate: 'Gunakan template ini',
appCreateDSLErrorPart2: 'Apakah Anda ingin melanjutkan?',
},
newAppFromTemplate: {
sidebar: {
HR: 'HR',
Programming: 'Pemrograman',
Recommended: 'Direkomendasikan',
Workflow: 'Alur Kerja',
Assistant: 'Asisten',
Writing: 'Tulisan',
Agent: 'Agen',
},
byCategories: 'BERDASARKAN KATEGORI',
searchAllTemplate: 'Cari semua templat...',
},
iconPicker: {
cancel: 'Membatalkan',
emoji: 'Emoji',
image: 'Citra',
ok: 'OKE',
},
answerIcon: {
title: 'Gunakan ikon aplikasi web untuk mengganti 🤖',
description: 'Apakah akan menggunakan ikon aplikasi web untuk mengganti 🤖 di aplikasi bersama',
descriptionInExplore: 'Apakah akan menggunakan ikon aplikasi web untuk mengganti 🤖 di Jelajahi',
},
typeSelector: {
agent: 'Agen',
advanced: 'Alur obrolan',
completion: 'Penyelesaian',
all: 'Semua Jenis',
workflow: 'Alur Kerja',
chatbot: 'Chatbot',
},
tracing: {
configProviderTitle: {
notConfigured: 'Penyedia konfigurasi untuk mengaktifkan pelacakan',
configured: 'Dikonfigurasi',
moreProvider: 'Lebih Banyak Penyedia',
},
arize: {
title: 'Arize',
description: 'Observabilitas LLM tingkat perusahaan, evaluasi, pemantauan, dan eksperimen online & offline—didukung oleh OpenTelemetry. Dibuat khusus untuk LLM & aplikasi berbasis agen.',
},
phoenix: {
title: 'Phoenix',
description: 'Observabilitas, evaluasi, rekayasa cepat, dan platform eksperimen berbasis sumber terbuka & OpenTelemetri untuk alur kerja dan agen LLM Anda.',
},
langsmith: {
title: 'LangSmith',
description: 'Platform pengembang all-in-one untuk setiap langkah siklus hidup aplikasi yang didukung LLM.',
},
langfuse: {
title: 'Langfuse',
description: 'Observabilitas LLM sumber terbuka, evaluasi, manajemen prompt, dan metrik untuk men-debug dan meningkatkan aplikasi LLM Anda.',
},
opik: {
title: 'Opik',
description: 'Opik adalah platform sumber terbuka untuk mengevaluasi, menguji, dan memantau aplikasi LLM.',
},
weave: {
description: 'Weave adalah platform sumber terbuka untuk mengevaluasi, menguji, dan memantau aplikasi LLM.',
title: 'Anyam',
},
aliyun: {
title: 'Monitor Awan',
description: 'Platform observabilitas yang dikelola sepenuhnya dan bebas perawatan yang disediakan oleh Alibaba Cloud, memungkinkan pemantauan, pelacakan, dan evaluasi aplikasi Dify yang out-of-the-box.',
},
configProvider: {
project: 'Proyek',
publicKey: 'Kunci Publik',
removeConfirmContent: 'Konfigurasi saat ini sedang digunakan, menghapusnya akan mematikan fitur Pelacakan.',
title: 'Konfigurasi',
secretKey: 'Kunci Rahasia',
},
expand: 'Memperluas',
disabledTip: 'Silakan konfigurasi penyedia terlebih dahulu',
view: 'Melihat',
collapse: 'Roboh',
tracing: 'Menelusuri',
title: 'Melacak performa aplikasi',
disabled: 'Cacat',
enabled: 'Dalam Layanan',
config: 'Konfigurasi',
description: 'Mengonfigurasi penyedia LLMOps Pihak Ketiga dan melacak performa aplikasi.',
inUse: 'Sedang digunakan',
tracingDescription: 'Tangkap konteks lengkap eksekusi aplikasi, termasuk panggilan LLM, konteks, perintah, permintaan HTTP, dan lainnya, ke platform pelacakan pihak ketiga.',
},
appSelector: {
placeholder: 'Pilih aplikasi...',
params: 'PARAMETER APLIKASI',
noParams: 'Tidak perlu parameter',
label: 'APP',
},
structOutput: {
notConfiguredTip: 'Output terstruktur belum dikonfigurasi',
required: 'Diperlukan',
structured: 'Terstruktur',
modelNotSupported: 'Model tidak didukung',
structuredTip: 'Output Terstruktur adalah fitur yang memastikan model akan selalu menghasilkan respons yang mematuhi Skema JSON yang Anda sediakan',
LLMResponse: 'Tanggapan LLM',
modelNotSupportedTip: 'Model saat ini tidak mendukung fitur ini dan secara otomatis diturunkan ke injeksi minta.',
configure: 'Mengkonfigurasi',
moreFillTip: 'Menampilkan maksimal 10 tingkat bersarang',
},
accessItemsDescription: {
anyone: 'Siapa pun dapat mengakses aplikasi web (tidak perlu login)',
organization: 'Semua anggota dalam platform dapat mengakses aplikasi web',
specific: 'Hanya anggota tertentu dalam platform yang dapat mengakses aplikasi web',
external: 'Hanya pengguna eksternal yang diautentikasi yang dapat mengakses aplikasi web',
},
accessControlDialog: {
accessItems: {
organization: 'Semua anggota dalam platform',
external: 'Pengguna eksternal yang diautentikasi',
anyone: 'Siapa pun yang memiliki tautan',
specific: 'Anggota tertentu dalam platform',
},
operateGroupAndMember: {
expand: 'Memperluas',
searchPlaceholder: 'Cari grup dan anggota',
noResult: 'Tidak ada hasil',
allMembers: 'Semua anggota',
},
updateSuccess: 'Update berhasil',
noGroupsOrMembers: 'Tidak ada grup atau anggota yang dipilih',
webAppSSONotEnabledTip: 'Hubungi administrator organisasi Anda untuk mengonfigurasi autentikasi eksternal untuk aplikasi web.',
description: 'Menetapkan izin akses aplikasi web',
title: 'Kontrol Akses Aplikasi Web',
accessLabel: 'Siapa yang memiliki akses',
},
publishApp: {
notSetDesc: 'Saat ini tidak ada yang dapat mengakses aplikasi web. Silakan atur izin.',
notSet: 'Tidak diatur',
title: 'Siapa yang dapat mengakses aplikasi web',
},
gotoAnything: {
actions: {
themeDark: 'Tema Gelap',
themeCategoryDesc: 'Ganti tema aplikasi',
themeCategoryTitle: 'Tema',
searchWorkflowNodesHelp: 'Fitur ini hanya berfungsi saat melihat alur kerja. Navigasikan ke alur kerja terlebih dahulu.',
searchApplicationsDesc: 'Cari dan navigasikan ke aplikasi Anda',
searchPlugins: 'Cari Plugin',
searchApplications: 'Cari Aplikasi',
languageCategoryTitle: 'Bahasa',
themeLight: 'Tema Cahaya',
communityDesc: 'Buka komunitas Discord',
searchWorkflowNodesDesc: 'Temukan dan lompat ke simpul dalam alur kerja saat ini berdasarkan nama atau jenis',
searchWorkflowNodes: 'Cari Node Alur Kerja',
runTitle: 'Perintah',
themeSystemDesc: 'Ikuti tampilan OS Anda',
languageCategoryDesc: 'Ganti bahasa antarmuka',
themeDarkDesc: 'Gunakan penampilan gelap',
searchPluginsDesc: 'Cari dan navigasikan ke plugin Anda',
accountDesc: 'Arahkan ke halaman akun',
searchKnowledgeBases: 'Cari Basis Pengetahuan',
runDesc: 'Jalankan perintah cepat (tema, bahasa, ...)',
docDesc: 'Buka dokumentasi bantuan',
themeLightDesc: 'Gunakan penampilan ringan',
feedbackDesc: 'Buka diskusi umpan balik komunitas',
slashDesc: 'Jalankan perintah (ketik / untuk melihat semua perintah yang tersedia)',
searchKnowledgeBasesDesc: 'Cari dan navigasikan ke basis pengetahuan Anda',
themeSystem: 'Tema Sistem',
languageChangeDesc: 'Mengubah bahasa UI',
},
emptyState: {
noWorkflowNodesFound: 'Tidak ada simpul alur kerja yang ditemukan',
noAppsFound: 'Tidak ada aplikasi yang ditemukan',
noPluginsFound: 'Tidak ada plugin yang ditemukan',
noKnowledgeBasesFound: 'Tidak ada basis pengetahuan yang ditemukan',
tryDifferentTerm: 'Coba istilah penelusuran lain',
},
groups: {
apps: 'Apps',
commands: 'Perintah',
plugins: 'Plugin',
knowledgeBases: 'Basis Pengetahuan',
workflowNodes: 'Node Alur Kerja',
},
searchTitle: 'Cari apa pun',
noResults: 'Tidak ada hasil yang ditemukan',
searchTemporarilyUnavailable: 'Penelusuran tidak tersedia untuk sementara',
selectSearchType: 'Pilih apa yang akan dicari',
someServicesUnavailable: 'Beberapa layanan penelusuran tidak tersedia',
searching: 'Mencari...',
searchPlaceholder: 'Cari atau ketik @ atau / untuk perintah...',
slashHint: 'Ketik / untuk melihat semua perintah yang tersedia',
commandHint: 'Ketik @ untuk menelusuri berdasarkan kategori',
useAtForSpecific: 'Gunakan @ untuk jenis tertentu',
clearToSearchAll: 'Hapus @ untuk mencari semua',
searchHint: 'Mulailah mengetik untuk mencari semuanya secara instan',
servicesUnavailableMessage: 'Beberapa layanan penelusuran mungkin mengalami masalah. Coba lagi sebentar lagi.',
tryDifferentSearch: 'Coba istilah penelusuran lain',
noMatchingCommands: 'Tidak ada perintah yang cocok ditemukan',
searchFailed: 'Pencarian gagal',
},
createApp: 'BUAT APLIKASI',
accessControl: 'Kontrol Akses Aplikasi Web',
maxActiveRequestsTip: 'Jumlah maksimum permintaan aktif bersamaan per aplikasi (0 untuk tidak terbatas)',
noAccessPermission: 'Tidak ada izin untuk mengakses aplikasi web',
maxActiveRequestsPlaceholder: 'Masukkan 0 untuk tidak terbatas',
join: 'Bergabunglah dengan komunitas',
deleteAppConfirmContent: 'Menghapus aplikasi tidak dapat diubah. Pengguna tidak akan dapat lagi mengakses aplikasi Anda, dan semua konfigurasi prompt serta log akan dihapus secara permanen.',
duplicate: 'Duplikat',
importDSL: 'Impor file DSL',
appDeleted: 'Aplikasi dihapus',
importFromDSLFile: 'Dari file DSL',
export: 'Ekspor DSL',
createFromConfigFile: 'Buat dari file DSL',
importFromDSLUrlPlaceholder: 'Tempel tautan DSL di sini',
exportFailed: 'Ekspor DSL gagal.',
importFromDSL: 'Impor dari DSL',
duplicateTitle: 'Aplikasi Duplikat',
roadmap: 'Lihat peta jalan kami',
editDone: 'Info aplikasi diperbarui',
deleteAppConfirmTitle: 'Hapus aplikasi ini?',
editFailed: 'Gagal memperbarui info aplikasi',
removeOriginal: 'Menghapus aplikasi asli',
importFromDSLUrl: 'Dari URL',
communityIntro: 'Berdiskusi dengan anggota tim, kontributor, dan pengembang di berbagai saluran.',
switchTip: 'tidak mengizinkan',
switchTipEnd: 'beralih kembali ke Basic Orchestrate.',
switch: 'Beralih ke Workflow Orchestrate',
editApp: 'Edit Info',
switchTipStart: 'Salinan aplikasi baru akan dibuat untuk Anda, dan salinan baru akan beralih ke Workflow Orchestrate. Salinan baru akan',
switchLabel: 'Salinan aplikasi yang akan dibuat',
editAppTitle: 'Edit Info Aplikasi',
maxActiveRequests: 'Permintaan bersamaan maksimum',
switchStart: 'Sakelar mulai',
openInExplore: 'Buka di Jelajahi',
showMyCreatedAppsOnly: 'Dibuat oleh saya',
appDeleteFailed: 'Gagal menghapus aplikasi',
}
export default translation

178
web/i18n/id-ID/billing.ts Normal file
View File

@ -0,0 +1,178 @@
const translation = {
usagePage: {
buildApps: 'Bangun Aplikasi',
vectorSpace: 'Penyimpanan Data Pengetahuan',
vectorSpaceTooltip: 'Dokumen dengan mode pengindeksan Kualitas Tinggi akan menggunakan sumber daya Penyimpanan Data Pengetahuan. Ketika Penyimpanan Data Pengetahuan mencapai batas, dokumen baru tidak akan diunggah.',
documentsUploadQuota: 'Kuota Unggah Dokumen',
teamMembers: 'Anggota Tim',
annotationQuota: 'Kuota Anotasi',
},
upgradeBtn: {
encourage: 'Tingkatkan Sekarang',
plain: 'Lihat Paket',
encourageShort: 'Upgrade',
},
plansCommon: {
planRange: {
yearly: 'Tahunan',
monthly: 'Bulanan',
},
priority: {
'priority': 'Prioritas',
'top-priority': 'Prioritas utama',
'standard': 'Standar',
},
supportItems: {
llmLoadingBalancing: 'Penyeimbangan Beban LLM',
communityForums: 'Forum komunitas',
agentMode: 'Mode Agen',
logoChange: 'Perubahan logo',
priorityEmail: 'Dukungan email & obrolan prioritas',
emailSupport: 'Dukungan email',
personalizedSupport: 'Dukungan yang dipersonalisasi',
bulkUpload: 'Mengunggah dokumen secara massal',
customIntegration: 'Integrasi dan dukungan khusus',
dedicatedAPISupport: 'Dukungan API khusus',
SSOAuthentication: 'Autentikasi SSO',
ragAPIRequest: 'Permintaan API RAG',
llmLoadingBalancingTooltip: 'Tambahkan beberapa kunci API ke model, secara efektif melewati batas kecepatan API.',
workflow: 'Alur Kerja',
},
messageRequest: {
tooltip: 'Kredit pesan disediakan untuk membantu Anda mencoba berbagai model OpenAI dengan mudah di Dify. Kredit digunakan berdasarkan jenis model. Setelah habis, Anda dapat beralih ke kunci API OpenAI Anda sendiri.',
},
annotatedResponse: {
tooltip: 'Pengeditan manual dan anotasi respons memberikan kemampuan menjawab pertanyaan berkualitas tinggi yang dapat disesuaikan untuk aplikasi. (Hanya berlaku di aplikasi Chat)',
},
title: 'Harga yang mendukung perjalanan AI Anda',
mostPopular: 'Populer',
free: 'Bebas',
freeTrialTipSuffix: 'Tidak perlu kartu kredit',
freeTrialTipPrefix: 'Daftar dan dapatkan',
month: 'bulan',
talkToSales: 'Bicaralah dengan Sales',
freeTrialTip: 'uji coba gratis 200 panggilan OpenAI.',
apiRateLimit: 'Batas Tarif API',
yearlyTip: 'Bayar selama 10 bulan, nikmati 1 tahun!',
contractOwner: 'Hubungi manajer tim',
save: 'Simpan',
self: 'Hosting Mandiri',
comparePlanAndFeatures: 'Bandingkan paket & fitur',
unavailable: 'Tidak tersedia',
support: 'Dukung',
cloud: 'Layanan Cloud',
annotationQuota: 'Kuota Anotasi',
contactSales: 'Hubungi Sales',
currentPlan: 'Rencana Saat Ini',
unlimitedApiRate: 'Tidak Ada Batas Tarif API',
priceTip: 'per ruang kerja/',
apiRateLimitTooltip: 'Batas Tarif API berlaku untuk semua permintaan yang dibuat melalui Dify API, termasuk pembuatan teks, percakapan obrolan, eksekusi alur kerja, dan pemrosesan dokumen.',
documentsTooltip: 'Kuota jumlah dokumen yang diimpor dari Sumber Data Pengetahuan.',
days: 'Hari',
vectorSpaceTooltip: 'Dokumen dengan mode pengindeksan Kualitas Tinggi akan menggunakan sumber daya Penyimpanan Data Pengetahuan. Ketika Penyimpanan Data Pengetahuan mencapai batas, dokumen baru tidak akan diunggah.',
memberAfter: 'Anggota',
year: 'tahun',
getStarted: 'Memulai',
comingSoon: 'Segera datang',
annualBilling: 'Penagihan Tahunan',
contractSales: 'Hubungi penjualan',
documentProcessingPriority: 'Pemrosesan Dokumen',
startForFree: 'Mulai Gratis',
documentsRequestQuotaTooltip: 'Menentukan jumlah total tindakan yang dapat dilakukan ruang kerja per menit dalam pangkalan pengetahuan, termasuk pembuatan, penghapusan, pembaruan, pengunggahan dokumen, modifikasi, pengarsipan, dan kueri basis pengetahuan himpunan data. Metrik ini digunakan untuk mengevaluasi performa permintaan basis pengetahuan. Misalnya, jika pengguna Sandbox melakukan 10 pengujian hit berturut-turut dalam satu menit, ruang kerja mereka akan dibatasi sementara untuk melakukan tindakan berikut selama menit berikutnya: pembuatan, penghapusan, pembaruan, dan unggahan atau modifikasi himpunan data.',
unlimited: 'Unlimited',
documentProcessingPriorityUpgrade: 'Proses lebih banyak data dengan akurasi yang lebih tinggi pada kecepatan yang lebih tinggi.',
ragAPIRequestTooltip: 'Mengacu pada jumlah panggilan API yang hanya memanggil kemampuan pemrosesan basis pengetahuan Dify.',
receiptInfo: 'Hanya pemilik tim dan admin tim yang dapat berlangganan dan melihat informasi penagihan',
customTools: 'Alat Kustom',
modelProviders: 'Mendukung OpenAI/Anthropic/Llama2/Azure OpenAI/Hugging Face/Replite',
member: 'Anggota',
},
plans: {
sandbox: {
name: 'Sandbox',
description: 'Uji Coba Gratis Kemampuan Inti',
for: 'Uji Coba Gratis Kemampuan Inti',
},
professional: {
name: 'Profesional',
for: 'Untuk Pengembang Independen/Tim Kecil',
description: 'Untuk Pengembang Independen/Tim Kecil',
},
team: {
name: 'Tim',
description: 'Untuk Tim Berukuran Menengah',
for: 'Untuk Tim Berukuran Menengah',
},
community: {
features: {
0: 'Semua fitur inti dirilis di bawah repositori publik',
1: 'Ruang Kerja Tunggal',
2: 'Sesuai dengan Lisensi Sumber Terbuka Dify',
},
price: 'Bebas',
for: 'Untuk Pengguna Individu, Tim Kecil, atau Proyek Non-Komersial',
name: 'Masyarakat',
description: 'Untuk Pengguna Individu, Tim Kecil, atau Proyek Non-Komersial',
btnText: 'Mulai dengan Komunitas',
includesTitle: 'Fitur Gratis:',
},
premium: {
features: {
2: 'Kustomisasi Logo & Branding WebApp',
3: 'Dukungan Email & Obrolan Prioritas',
0: 'Keandalan yang dikelola sendiri oleh berbagai penyedia cloud',
1: 'Ruang Kerja Tunggal',
},
name: 'Premi',
price: 'Scalable',
priceTip: 'Berdasarkan Cloud Marketplace',
for: 'Untuk Organisasi dan Tim Menengah',
comingSoon: 'Dukungan Microsoft Azure & Google Cloud Segera Hadir',
includesTitle: 'Semuanya dari Komunitas, ditambah:',
description: 'Untuk Organisasi dan Tim Menengah',
btnText: 'Dapatkan Premium di',
},
enterprise: {
features: {
5: 'SLA yang Dinegosiasikan oleh Mitra Dify',
3: 'Beberapa Ruang Kerja & Manajemen Perusahaan',
4: 'SSO',
2: 'Fitur Eksklusif Enterprise',
7: 'Pembaruan dan Pemeliharaan oleh Dify Secara Resmi',
1: 'Otorisasi Lisensi Komersial',
8: 'Dukungan Teknis Profesional',
0: 'Solusi Penerapan yang Dapat Diskalakan Tingkat Perusahaan',
6: 'Keamanan & Kontrol Tingkat Lanjut',
},
includesTitle: 'Semuanya mulai dari Premium, ditambah:',
btnText: 'Hubungi Sales',
price: 'Adat',
for: 'Untuk Tim berukuran besar',
name: 'Usaha',
priceTip: 'Hanya Penagihan Tahunan',
description: 'Untuk perusahaan, memerlukan keamanan, kepatuhan, skalabilitas, kontrol, dan fitur yang lebih canggih di seluruh organisasi',
},
},
vectorSpace: {
fullSolution: 'Tingkatkan paket Anda untuk mendapatkan lebih banyak ruang.',
fullTip: 'Ruang Vektor penuh.',
},
apps: {
contactUs: 'Hubungi',
fullTip2des: 'Disarankan untuk membersihkan aplikasi yang tidak aktif untuk mengosongkan penggunaan, atau hubungi kami.',
fullTip1des: 'Anda telah mencapai batas build app pada paket ini',
fullTip1: 'Tingkatkan untuk membuat lebih banyak aplikasi',
fullTip2: 'Batas paket tercapai',
},
annotatedResponse: {
fullTipLine2: 'Anotasi lebih banyak percakapan.',
fullTipLine1: 'Tingkatkan paket Anda ke',
quotaTitle: 'Kuota Balasan Anotasi',
},
currentPlan: 'Rencana Saat Ini',
buyPermissionDeniedTip: 'Hubungi administrator perusahaan Anda untuk berlangganan',
viewBilling: 'Mengelola penagihan dan langganan',
teamMembers: 'Anggota Tim',
}
export default translation

722
web/i18n/id-ID/common.ts Normal file
View File

@ -0,0 +1,722 @@
const translation = {
theme: {
theme: 'Tema',
light: 'ringan',
auto: 'sistem',
dark: 'gelap',
},
api: {
success: 'Keberhasilan',
saved: 'Disimpan',
remove: 'Dihapus',
actionSuccess: 'Aksi berhasil',
create: 'Dibuat',
},
operation: {
setup: 'Setup',
download: 'Mengunduh',
getForFree: 'Dapatkan gratis',
reload: 'Reload',
lineBreak: 'Istirahat baris',
learnMore: 'Pelajari lebih lanjut',
saveAndRegenerate: 'Simpan & Buat Ulang Potongan Anak',
zoomOut: 'Perkecil',
openInNewTab: 'Buka di tab baru',
viewMore: 'LIHAT LEBIH BANYAK',
selectAll: 'Pilih Semua',
in: 'di',
skip: 'Lewat',
remove: 'Buka',
rename: 'Ubah nama',
close: 'Tutup',
ok: 'OKE',
regenerate: 'Regenerasi',
settings: 'Pengaturan',
log: 'Batang',
delete: 'Menghapus',
viewDetails: 'Lihat Detail',
view: 'Melihat',
clear: 'Jelas',
deleteApp: 'Hapus Aplikasi',
downloadSuccess: 'Unduh Selesai.',
change: 'Ubah',
params: 'Parameter',
search: 'Mencari',
copied: 'Disalin',
deSelectAll: 'Batalkan pilihan Semua',
saveAndEnable: 'Simpan & Aktifkan',
refresh: 'Restart',
downloadFailed: 'Unduhan gagal. Silakan coba lagi nanti.',
edit: 'Mengedit',
send: 'Kirim',
copyImage: 'Salin Gambar',
confirm: 'Mengkonfirmasi',
format: 'Format',
create: 'Menciptakan',
add: 'Tambah',
copy: 'Menyalin',
audioSourceUnavailable: 'AudioSource tidak tersedia',
submit: 'Tunduk',
duplicate: 'Duplikat',
save: 'Simpan',
added: 'Ditambahkan',
more: 'Lebih',
zoomIn: 'Perbesar',
reset: 'Reset',
cancel: 'Membatalkan',
sure: 'Saya yakin',
imageCopied: 'Gambar yang disalin',
config: 'Konfigurasi',
},
errorMsg: {
urlError: 'URL harus dimulai dengan http:// atau https://',
},
placeholder: {
select: 'Silakan pilih',
input: 'Silakan masuk',
},
voice: {
language: {
zhHans: 'Cina',
roRO: 'Rumania',
jaJP: 'Jepang',
plPL: 'Polandia',
ptBR: 'Portugis',
esES: 'Spanyol',
idID: 'Indonesia',
enUS: 'Inggris',
trTR: 'Turki',
hiIN: 'Hindi',
koKR: 'Korea',
viVN: 'Vietnam',
ukUA: 'Ukraina',
faIR: 'Farsi',
itIT: 'Italia',
zhHant: 'Mandarin Tradisional',
thTH: 'Thai',
ruRU: 'Rusia',
deDE: 'Jerman',
frFR: 'Prancis',
},
},
unit: {
char: 'Tank',
},
actionMsg: {
noModification: 'Tidak ada modifikasi saat ini.',
generatedUnsuccessfully: 'Dihasilkan tidak berhasil',
modifiedUnsuccessfully: 'Dimodifikasi tidak berhasil',
modifiedSuccessfully: 'Berhasil dimodifikasi',
copySuccessfully: 'Berhasil disalin',
payCancelled: 'Pembayaran dibatalkan',
paySucceeded: 'Pembayaran berhasil',
generatedSuccessfully: 'Berhasil dihasilkan',
},
model: {
params: {
temperature: 'Suhu',
max_tokensTip: 'Digunakan untuk membatasi panjang maksimum balasan, dalam token. \nNilai yang lebih besar dapat membatasi ruang yang tersisa untuk kata prompt, log obrolan, dan Pengetahuan. \nDisarankan untuk menetapkannya di bawah dua pertiga\nGPT-4-1106-Preview, GPT-4-Vision-Preview Max Token (input 128K output 4K)',
stop_sequencesTip: 'Hingga empat urutan di mana API akan berhenti menghasilkan token lebih lanjut. Teks yang ditampilkan tidak akan berisi urutan berhenti.',
frequency_penaltyTip: 'Berapa banyak yang harus menghukum token baru berdasarkan frekuensi yang ada dalam teks sejauh ini.\nMengurangi kemungkinan model untuk mengulangi baris yang sama kata demi kata.',
max_tokens: 'Token maks',
stop_sequences: 'Urutan berhenti',
frequency_penalty: 'Penalti frekuensi',
top_p: 'P Teratas',
maxTokenSettingTip: 'Pengaturan token maks Anda tinggi, berpotensi membatasi ruang untuk perintah, kueri, dan data. Pertimbangkan untuk mengaturnya di bawah 2/3.',
stop_sequencesPlaceholder: 'Masukkan urutan dan tekan Tab',
top_pTip: 'Mengontrol keragaman melalui pengambilan sampel nukleus: 0,5 berarti setengah dari semua opsi berbobot kemungkinan dipertimbangkan.',
presence_penalty: 'Penalti kehadiran',
presence_penaltyTip: 'Berapa banyak yang harus menghukum token baru berdasarkan apakah mereka muncul dalam teks sejauh ini.\nMeningkatkan kemungkinan model untuk membicarakan topik baru.',
temperatureTip: 'Mengontrol keacakan: Menurunkan menghasilkan lebih sedikit penyelesaian acak. Saat suhu mendekati nol, model akan menjadi deterministik dan berulang.',
},
tone: {
Precise: 'Tepat',
Balanced: 'Seimbang',
Custom: 'Adat',
Creative: 'Kreatif',
},
addMoreModel: 'Buka pengaturan untuk menambahkan lebih banyak model',
capabilities: 'Kemampuan MultiModal',
settingsLink: 'Pengaturan Penyedia Model',
},
menus: {
plugins: 'Plugin',
pluginsTips: 'Integrasikan plugin pihak ketiga atau buat AI-Plugin yang kompatibel dengan ChatGPT.',
datasetsTips: 'SEGERA HADIR: Impor data teks Anda sendiri atau tulis data secara real-time melalui Webhook untuk peningkatan konteks LLM.',
appDetail: 'Detail Aplikasi',
datasets: 'Pengetahuan',
account: 'Rekening',
newApp: 'Aplikasi Baru',
explore: 'Menjelajahi',
apps: 'Belajar',
status: 'beta',
tools: 'Perkakas',
exploreMarketplace: 'Jelajahi Marketplace',
newDataset: 'Ciptakan Pengetahuan',
},
userProfile: {
emailSupport: 'Dukungan Email',
helpCenter: 'Docs',
compliance: 'Kepatuhan',
community: 'Masyarakat',
communityFeedback: 'Umpan balik',
roadmap: 'Peta jalan',
logout: 'Keluar',
settings: 'Pengaturan',
support: 'Dukung',
github: 'GitHub',
about: 'Sekitar',
workspace: 'Workspace',
createWorkspace: 'Membuat Ruang Kerja',
},
compliance: {
soc2Type2: 'Laporan SOC 2 Tipe II',
professionalUpgradeTooltip: 'Hanya tersedia dengan paket Tim atau lebih tinggi.',
gdpr: 'GDPR DPA',
soc2Type1: 'Laporan SOC 2 Tipe I',
sandboxUpgradeTooltip: 'Hanya tersedia dengan paket Profesional atau Tim.',
iso27001: 'Sertifikasi ISO 27001:2022',
},
settings: {
generalGroup: 'UMUM',
billing: 'Penagihan',
plugin: 'Plugin',
members: 'Anggota',
workplaceGroup: 'WORKSPACE',
dataSource: 'Sumber Data',
integrations: 'Integrasi',
provider: 'Penyedia Model',
language: 'Bahasa',
accountGroup: 'UMUM',
account: 'Akun saya',
apiBasedExtension: 'Ekstensi API',
},
account: {
changeEmail: {
title: 'Ubah Email',
verifyNew: 'Memverifikasi email baru Anda',
content3: 'Masukkan email baru dan kami akan mengirimkan kode verifikasi kepada Anda.',
emailPlaceholder: 'Masukkan email baru',
existingEmail: 'Pengguna dengan email ini sudah ada.',
codeLabel: 'Kode verifikasi',
resendTip: 'Tidak menerima kode?',
continue: 'Terus',
newEmail: 'Menyiapkan alamat email baru',
resend: 'Kirim Ulang',
codePlaceholder: 'Tempel kode 6 digit',
emailLabel: 'Email baru',
sendVerifyCode: 'Kirim kode verifikasi',
verifyEmail: 'Memverifikasi email Anda saat ini',
unAvailableEmail: 'Email ini tidak tersedia untuk sementara.',
authTip: 'Setelah email Anda diubah, akun Google atau GitHub yang ditautkan ke email lama Anda tidak akan dapat lagi masuk ke akun ini.',
},
account: 'Rekening',
langGeniusAccount: 'Data akun',
email: 'Email',
studio: 'Belajar',
resetPassword: 'Setel ulang kata sandi',
sendVerificationButton: 'Kirim Kode Verifikasi',
editName: 'Edit Nama',
setPassword: 'Menetapkan kata sandi',
passwordTip: 'Anda dapat mengatur kata sandi permanen jika tidak ingin menggunakan kode login sementara',
confirmPassword: 'Konfirmasi kata sandi',
delete: 'Hapus Akun',
verificationPlaceholder: 'Tempel kode 6 digit',
editWorkspaceInfo: 'Edit Info Ruang Kerja',
currentPassword: 'Kata sandi saat ini',
newPassword: 'Kata sandi baru',
name: 'Nama',
notEqual: 'Dua kata sandi berbeda.',
avatar: 'Avatar',
myAccount: 'Akun Saya',
deletePlaceholder: 'Silakan masukkan email Anda',
permanentlyDeleteButton: 'Hapus Akun Secara Permanen',
feedbackPlaceholder: 'Fakultatif',
deletePrivacyLink: 'Kebijakan Privasi.',
feedbackTitle: 'Umpan balik',
workspaceName: 'Nama Ruang Kerja',
deletePrivacyLinkTip: 'Untuk informasi lebih lanjut tentang bagaimana kami menangani data Anda, silakan lihat',
deleteLabel: 'Untuk mengonfirmasi, silakan ketik email Anda di bawah ini',
feedbackLabel: 'Beri tahu kami mengapa Anda menghapus akun Anda?',
langGeniusAccountTip: 'Data pengguna akun Anda.',
deleteSuccessTip: 'Akun Anda membutuhkan waktu untuk menyelesaikan penghapusan. Kami akan mengirimkan email kepada Anda setelah semuanya selesai.',
verificationLabel: 'Kode Verifikasi',
password: 'Kata sandi',
deleteTip: 'Harap dicatat, setelah dikonfirmasi, sebagai Pemilik Ruang Kerja apa pun, ruang kerja Anda akan dijadwalkan dalam antrean untuk penghapusan permanen, dan semua data pengguna Anda akan diantri untuk penghapusan permanen.',
workspaceIcon: 'Ikon Ruang Kerja',
},
members: {
transferModal: {
warningTip: 'Anda akan menjadi anggota admin, dan pemilik baru akan memiliki kendali penuh.',
codePlaceholder: 'Tempel kode 6 digit',
title: 'Mentransfer kepemilikan ruang kerja',
codeLabel: 'Kode verifikasi',
sendVerifyCode: 'Kirim kode verifikasi',
continue: 'Terus',
transfer: 'Mentransfer kepemilikan ruang kerja',
resendTip: 'Tidak menerima kode?',
verifyContent2: 'Kami akan mengirimkan kode verifikasi sementara ke email ini untuk autentikasi ulang.',
verifyEmail: 'Memverifikasi email Anda saat ini',
transferLabel: 'Mentransfer kepemilikan ruang kerja ke',
resend: 'Kirim Ulang',
transferPlaceholder: 'Pilih anggota ruang kerja...',
},
lastActive: 'TERAKHIR AKTIF',
owner: 'Pemilik',
normal: 'Biasa',
team: 'Tim',
adminTip: 'Dapat membangun aplikasi & mengelola pengaturan tim',
emailNotSetup: 'Server email tidak disiapkan, sehingga email undangan tidak dapat dikirim. Harap beri tahu pengguna tentang tautan undangan yang akan dikeluarkan setelah undangan.',
editor: 'Editor',
setAdmin: 'Tetapkan sebagai administrator',
failedInvitationEmails: 'Pengguna di bawah ini tidak berhasil diundang',
emailInvalid: 'Format Email Tidak Valid',
setEditor: 'Tetapkan sebagai editor',
datasetOperatorTip: 'Hanya dapat mengelola basis pengetahuan',
builderTip: 'Dapat membangun & mengedit aplikasi sendiri',
datasetOperator: 'Admin Pengetahuan',
pending: 'Tertunda...',
setBuilder: 'Tetapkan sebagai pembuat',
invitationSentTip: 'Undangan terkirim, dan mereka dapat masuk ke Dify untuk mengakses data tim Anda.',
role: 'PERAN',
ok: 'OKE',
setMember: 'Atur ke anggota biasa',
deleteMember: 'Hapus Anggota',
name: 'NAMA',
invite: 'Tambah',
inviteTeamMemberTip: 'Mereka dapat mengakses data tim Anda langsung setelah masuk.',
transferOwnership: 'Pengalihan Kepemilikan',
sendInvite: 'Kirim Undangan',
email: 'Email',
removeFromTeamTip: 'Akan menghapus akses tim',
invitationLink: 'Tautan Undangan',
you: '(Anda)',
removeFromTeam: 'Hapus dari tim',
emailPlaceholder: 'Silakan masukkan email',
inviteTeamMember: 'Tambahkan anggota tim',
builder: 'Pembangun',
disInvite: 'Batalkan undangan',
invitationSent: 'Undangan terkirim',
editorTip: 'Dapat membangun & mengedit aplikasi',
admin: 'Admin',
normalTip: 'Hanya dapat menggunakan aplikasi, tidak dapat membuat aplikasi',
},
feedback: {
subtitle: 'Tolong beri tahu kami apa yang salah dengan tanggapan ini',
placeholder: 'Tolong jelaskan apa yang salah atau bagaimana kami dapat meningkatkan...',
content: 'Konten Umpan Balik',
title: 'Berikan Umpan Balik',
},
integrations: {
github: 'GitHub',
connected: 'Terhubung',
connect: 'Sambung',
githubAccount: 'Masuk dengan akun GitHub',
googleAccount: 'Masuk dengan akun Google',
google: 'Google',
},
language: {
displayLanguage: 'Bahasa Tampilan',
timezone: 'Zona Waktu',
},
provider: {
azure: {
helpTip: 'Pelajari Azure OpenAI Service',
apiKey: 'Kunci API',
apiBase: 'Basis API',
apiKeyPlaceholder: 'Masukkan kunci API Anda di sini',
apiBasePlaceholder: 'URL Dasar API Titik Akhir Azure OpenAI Anda.',
},
openaiHosted: {
desc: 'Layanan hosting OpenAI yang disediakan oleh Dify memungkinkan Anda menggunakan model seperti GPT-3.5. Sebelum kuota uji coba habis, Anda harus menyiapkan penyedia model lain.',
callTimes: 'Waktu panggilan',
useYourModel: 'Saat ini menggunakan Penyedia Model sendiri.',
usedUp: 'Kuota percobaan habis. Tambahkan Penyedia Model sendiri.',
openaiHosted: 'OpenAI yang Dihosting',
close: 'Tutup',
exhausted: 'KUOTA HABIS',
onTrial: 'SEDANG DIADILI',
},
anthropicHosted: {
trialQuotaTip: 'Kuota uji coba Anthropic Anda akan berakhir pada 17/03/2025 dan tidak akan tersedia lagi setelahnya. Silakan manfaatkan tepat waktu.',
callTimes: 'Waktu panggilan',
onTrial: 'SEDANG DIADILI',
anthropicHosted: 'Claude Antropis',
useYourModel: 'Saat ini menggunakan Penyedia Model sendiri.',
desc: 'Model yang kuat, yang unggul dalam berbagai tugas mulai dari dialog canggih dan pembuatan konten kreatif hingga instruksi terperinci.',
close: 'Tutup',
exhausted: 'KUOTA HABIS',
usedUp: 'Kuota percobaan habis. Tambahkan Penyedia Model sendiri.',
},
anthropic: {
keyFrom: 'Dapatkan kunci API Anda dari Anthropic',
enableTip: 'Untuk mengaktifkan model Anthropic, Anda perlu mengikat ke OpenAI atau Azure OpenAI Service terlebih dahulu.',
using: 'Kemampuan penyematan menggunakan',
notEnabled: 'Tidak diaktifkan',
},
encrypted: {
front: 'API KEY Anda akan dienkripsi dan disimpan menggunakan',
back: 'Teknologi.',
},
validatedError: 'Validasi gagal:',
invalidApiKey: 'Kunci API tidak valid',
apiKeyExceedBill: 'API KEY ini tidak memiliki kuota yang tersedia, silakan baca',
apiKey: 'Kunci API',
saveFailed: 'Menyimpan kunci api gagal',
editKey: 'Mengedit',
addKey: 'Tambahkan Kunci',
invalidKey: 'Kunci API OpenAI tidak valid',
enterYourKey: 'Masukkan kunci API Anda di sini',
comingSoon: 'Segera datang',
validating: 'Memvalidasi kunci...',
},
modelProvider: {
systemReasoningModel: {
tip: 'Atur model inferensi default yang akan digunakan untuk membuat aplikasi, serta fitur seperti pembuatan nama dialog dan saran pertanyaan berikutnya juga akan menggunakan model inferensi default.',
key: 'Model Penalaran Sistem',
},
embeddingModel: {
tip: 'Atur model default untuk pemrosesan penyematan dokumen Pengetahuan, baik pengambilan maupun impor Pengetahuan menggunakan model Penyematan ini untuk pemrosesan vektorisasi. Pengalihan akan menyebabkan dimensi vektor antara Pengetahuan yang diimpor dan pertanyaan menjadi tidak konsisten, mengakibatkan kegagalan pengambilan. Untuk menghindari kegagalan pengambilan, jangan mengganti model ini sesuka hati.',
required: 'Model Penyematan diperlukan',
key: 'Menyematkan Model',
},
speechToTextModel: {
tip: 'Atur model default untuk input ucapan-ke-teks dalam percakapan.',
key: 'Model Ucapan-ke-Teks',
},
ttsModel: {
tip: 'Atur model default untuk input teks-ke-ucapan dalam percakapan.',
key: 'Model Teks-ke-Ucapan',
},
rerankModel: {
key: 'Peringkat ulang Model',
tip: 'Model rerank akan menyusun ulang daftar dokumen kandidat berdasarkan kecocokan semantik dengan kueri pengguna, meningkatkan hasil peringkat semantik',
},
selector: {
rerankTip: 'Silakan atur model Rerank',
emptyTip: 'Tidak ada model yang tersedia',
emptySetting: 'Silakan buka pengaturan untuk mengonfigurasi',
tip: 'Model ini telah dihapus. Silakan tambahkan model atau pilih model lain.',
},
card: {
onTrial: 'Sedang Diadili',
paid: 'Dibayar',
buyQuota: 'Beli Kuota',
removeKey: 'Menghapus Kunci API',
tokens: 'Token',
callTimes: 'Waktu panggilan',
priorityUse: 'Penggunaan prioritas',
quota: 'KUOTA',
tip: 'Prioritas akan diberikan pada kuota yang dibayarkan. Kuota Trial akan digunakan setelah kuota yang dibayarkan habis.',
quotaExhausted: 'Kuota habis',
},
item: {
freeQuota: 'KUOTA GRATIS',
},
encrypted: {
back: 'Teknologi.',
front: 'API KEY Anda akan dienkripsi dan disimpan menggunakan',
},
freeQuota: {
howToEarn: 'Cara mendapatkan penghasilan',
},
auth: {
apiKeyModal: {
addModel: 'Tambahkan model',
desc: 'Setelah mengonfigurasi kredensial, semua anggota dalam ruang kerja dapat menggunakan model ini saat mengatur aplikasi.',
title: 'Konfigurasi Otorisasi Kunci API',
},
addCredential: 'Tambahkan kredensial',
authorizationError: 'Kesalahan otorisasi',
addApiKey: 'Menambahkan Kunci API',
modelCredentials: 'Kredensial model',
authRemoved: 'Autentikasi dihapus',
configModel: 'Model konfigurasi',
providerManagedTip: 'Konfigurasi saat ini dihosting oleh penyedia.',
specifyModelCredentialTip: 'Gunakan kredensial model yang dikonfigurasi.',
apiKeys: 'Kunci API',
providerManaged: 'Penyedia dikelola',
addNewModel: 'Tambahkan model baru',
unAuthorized: 'Sah',
configLoadBalancing: 'Penyeimbangan Beban Konfigurasi',
addModelCredential: 'Menambahkan kredensial model',
specifyModelCredential: 'Tentukan kredensial model',
},
systemModelSettingsLink: 'Mengapa perlu menyiapkan model sistem?',
apiKey: 'API-KUNCI',
selectModel: 'Pilih model Anda',
showMoreModelProvider: 'Tampilkan lebih banyak penyedia model',
systemModelSettings: 'Pengaturan Model Sistem',
addModel: 'Tambahkan Model',
quota: 'Kuota',
setupModelFirst: 'Silakan atur model Anda terlebih dahulu',
loadBalancingDescription: 'Konfigurasikan beberapa kredensial untuk model dan panggil secara otomatis.',
loadBalancingInfo: 'Secara default, penyeimbangan beban menggunakan strategi Round-robin. Jika pembatasan kecepatan dipicu, periode cooldown 1 menit akan diterapkan.',
apiKeyStatusNormal: 'Status APIKey normal',
credits: 'Kredit Pesan',
confirmDelete: 'Mengkonfirmasi penghapusan?',
addMoreModelProvider: 'TAMBAHKAN PENYEDIA MODEL LAINNYA',
collapse: 'Roboh',
providerManaged: 'Penyedia dikelola',
parameters: 'PARAMETER',
notConfigured: 'Model sistem belum sepenuhnya dikonfigurasi',
priorityUsing: 'Prioritaskan penggunaan',
model: 'Pola',
buyQuota: 'Beli Kuota',
configureTip: 'Menyiapkan api-key atau menambahkan model untuk digunakan',
emptyProviderTip: 'Silakan instal penyedia model terlebih dahulu.',
loadBalancing: 'Penyeimbangan beban',
loadPresets: 'Muat Preset',
loadBalancingHeadline: 'Penyeimbangan Beban',
editConfig: 'Edit Konfigurasi',
discoverMore: 'Temukan lebih lanjut di',
modelAndParameters: 'Model dan Parameter',
upgradeForLoadBalancing: 'Tingkatkan paket Anda untuk mengaktifkan Penyeimbangan Beban.',
providerManagedDescription: 'Gunakan satu set kredensial yang disediakan oleh penyedia model.',
showModels: 'Tampilkan Model',
deprecated: 'Usang',
models: 'Model',
configLoadBalancing: 'Penyeimbangan Beban Konfigurasi',
emptyProviderTitle: 'Penyedia model tidak disiapkan',
loadBalancingLeastKeyWarning: 'Untuk mengaktifkan penyeimbangan beban, setidaknya 2 tombol harus diaktifkan.',
toBeConfigured: 'Untuk dikonfigurasi',
addApiKey: 'Menambahkan kunci API Anda',
modelHasBeenDeprecated: 'Model ini tidak digunakan lagi',
searchModel: 'Model pencarian',
addConfig: 'Tambahkan Konfigurasi',
invalidApiKey: 'Kunci API tidak valid',
defaultConfig: 'Konfigurasi Default',
config: 'Konfigurasi',
quotaTip: 'Token gratis yang masih tersedia',
installProvider: 'Menginstal penyedia model',
callTimes: 'Waktu panggilan',
getFreeTokens: 'Dapatkan Token gratis',
},
dataSource: {
notion: {
selector: {
searchPages: 'Halaman pencarian...',
addPages: 'Tambahkan halaman',
pageSelected: 'Halaman yang Dipilih',
preview: 'PRATAYANG',
noSearchResult: 'Tidak ada hasil pencarian',
},
integratedAlert: 'Notion terintegrasi melalui kredensial internal, tidak perlu mengotorisasi ulang.',
disconnected: 'Terputus',
remove: 'Buka',
addWorkspace: 'Menambahkan ruang kerja',
description: 'Menggunakan Notion sebagai sumber data untuk Pengetahuan.',
connected: 'Terhubung',
pagesAuthorized: 'Halaman yang disahkan',
changeAuthorizedPages: 'Mengubah halaman resmi',
title: 'Gagasan',
sync: 'Sync',
connectedWorkspace: 'Ruang kerja yang terhubung',
},
website: {
title: 'Situs web',
with: 'Dengan',
active: 'Aktif',
inactive: 'Aktif',
description: 'Impor konten dari situs menggunakan perayap web.',
configuredCrawlers: 'Perayap yang dikonfigurasi',
},
connect: 'Sambung',
configure: 'Mengkonfigurasi',
add: 'Menambahkan sumber data',
},
plugin: {
serpapi: {
apiKey: 'Kunci API',
apiKeyPlaceholder: 'Masukkan kunci API Anda',
keyFrom: 'Dapatkan kunci SerpAPI Anda dari Halaman Akun SerpAPI',
},
},
apiBasedExtension: {
selector: {
title: 'Ekstensi API',
placeholder: 'Silakan pilih ekstensi API',
manage: 'Kelola Ekstensi API',
},
modal: {
name: {
title: 'Nama',
placeholder: 'Silakan masukkan nama',
},
apiEndpoint: {
title: 'Titik Akhir API',
placeholder: 'Silakan masukkan titik akhir API',
},
apiKey: {
title: 'Kunci API',
lengthError: 'Panjang kunci API tidak boleh kurang dari 5 karakter',
placeholder: 'Silakan masukkan kunci API',
},
editTitle: 'Edit Ekstensi API',
title: 'Tambahkan Ekstensi API',
},
link: 'Pelajari cara mengembangkan Ekstensi API Anda sendiri.',
title: 'Ekstensi API menyediakan manajemen API terpusat, menyederhanakan konfigurasi agar mudah digunakan di seluruh aplikasi Dify.',
type: 'Jenis',
add: 'Tambahkan Ekstensi API',
},
about: {
changeLog: 'Log perubahan',
updateNow: 'Perbarui sekarang',
},
appMenus: {
logs: 'Log',
overview: 'Pemantauan',
logAndAnn: 'Log & Anotasi',
promptEng: 'Mengatur',
apiAccess: 'Akses API',
},
environment: {
development: 'PENGEMBANGAN',
testing: 'PENGUJIAN',
},
appModes: {
completionApp: 'Pembuat Teks',
chatApp: 'Aplikasi Obrolan',
},
datasetMenus: {
hitTesting: 'Pengujian Pengambilan',
relatedApp: 'Aplikasi tertaut',
emptyTip: 'Pengetahuan ini belum terintegrasi dalam aplikasi apa pun. Silakan lihat dokumen untuk panduan.',
documents: 'Dokumen',
settings: 'Pengaturan',
noRelatedApp: 'Tidak ada aplikasi yang ditautkan',
viewDoc: 'Lihat dokumentasi',
},
voiceInput: {
speaking: 'Bicaralah sekarang...',
notAllow: 'mikrofon tidak diizinkan',
converting: 'Mengonversi ke teks...',
},
modelName: {
'claude-2': 'Claude-2',
'gpt-3.5-turbo': 'GPT-3.5-Turbo',
'gpt-4': 'GPT-4',
'whisper-1': 'Bisikan-1',
'text-davinci-003': 'Teks-Davinci-003',
'gpt-4-32k': 'GPT-4-32K',
'gpt-3.5-turbo-16k': 'GPT-3.5-Turbo-16K',
'claude-instant-1': 'Claude-Instan',
'text-embedding-ada-002': 'Penyematan Teks-Ada-002',
},
chat: {
citation: {
linkToDataset: 'Tautan ke Pengetahuan',
characters: 'Karakter:',
vectorHash: 'Hash vektor:',
hitScore: 'Skor Pengambilan:',
title: 'KUTIPAN',
hitCount: 'Jumlah pengambilan:',
},
resend: 'Kirim Ulang',
conversationName: 'Nama percakapan',
thinking: 'Pikiran...',
conversationNameCanNotEmpty: 'Nama percakapan diperlukan',
thought: 'Pikiran',
renameConversation: 'Ganti Nama Percakapan',
conversationNamePlaceholder: 'Silakan masukkan nama percakapan',
},
promptEditor: {
context: {
item: {
title: 'Konteks',
desc: 'Sisipkan templat konteks',
},
modal: {
footer: 'Anda dapat mengelola konteks di bagian Konteks di bawah.',
add: 'Tambahkan Konteks',
},
},
history: {
item: {
title: 'Riwayat Percakapan',
desc: 'Menyisipkan templat pesan historis',
},
modal: {
edit: 'Mengedit Nama Peran Percakapan',
title: 'CONTOH',
user: 'Halo',
assistant: 'Halo! Bagaimana saya dapat membantu Anda hari ini?',
},
},
variable: {
item: {
title: 'Variabel & Alat Eksternal',
desc: 'Sisipkan Variabel & Alat Eksternal',
},
outputToolDisabledItem: {
desc: 'Sisipkan Variabel',
title: 'Variabel',
},
modal: {
addTool: 'Alat baru',
add: 'Variabel baru',
},
},
query: {
item: {
desc: 'Menyisipkan templat kueri pengguna',
title: 'Kueri',
},
},
placeholder: 'Tulis kata prompt Anda di sini, masukkan \'{\' untuk menyisipkan variabel, masukkan \'/\' untuk menyisipkan blok konten prompt',
existed: 'Sudah ada di prompt',
},
imageUploader: {
pasteImageLink: 'Tempel tautan gambar',
uploadFromComputer: 'Unggah dari Komputer',
uploadFromComputerUploadError: 'Unggahan gambar gagal, silakan unggah lagi.',
imageUpload: 'Unggah Gambar',
uploadFromComputerReadError: 'Pembacaan gambar gagal, silakan coba lagi.',
pasteImageLinkInvalid: 'Tautan gambar tidak valid',
pasteImageLinkInputPlaceholder: 'Tempel tautan gambar di sini',
},
fileUploader: {
fileExtensionNotSupport: 'Ekstensi file tidak didukung',
uploadFromComputer: 'Unggahan lokal',
pasteFileLink: 'Tempel tautan file',
uploadFromComputerUploadError: 'Unggahan file gagal, silakan unggah lagi.',
pasteFileLinkInvalid: 'Tautan file tidak valid',
pasteFileLinkInputPlaceholder: 'Masukkan URL...',
uploadFromComputerReadError: 'Pembacaan file gagal, silakan coba lagi.',
},
tag: {
noTag: 'Tidak ada tag',
manageTags: 'Kelola Tag',
created: 'Tag berhasil dibuat',
delete: 'Hapus tag',
editTag: 'Edit tag',
addTag: 'Tambahkan tag',
create: 'Menciptakan',
addNew: 'Tambahkan tag baru',
failed: 'Pembuatan tag gagal',
selectorPlaceholder: 'Ketik untuk mencari atau membuat',
deleteTip: 'Tag sedang digunakan, hapus?',
placeholder: 'Semua Tag',
noTagYet: 'Belum ada tag',
},
license: {
unlimited: 'Unlimited',
expiring: 'Kedaluwarsa dalam satu hari',
},
pagination: {
perPage: 'Item per halaman',
},
avatar: {
deleteTitle: 'Hapus Avatar',
deleteDescription: 'Apakah Anda yakin ingin menghapus gambar profil Anda? Akun Anda akan menggunakan avatar awal default.',
},
imageInput: {
browse: 'ramban',
supportedFormats: 'Mendukung PNG, JPG, JPEG, WEBP dan GIF',
dropImageHere: 'Letakkan gambar Anda di sini, atau',
},
you: 'Kamu',
}
export default translation

32
web/i18n/id-ID/custom.ts Normal file
View File

@ -0,0 +1,32 @@
const translation = {
upgradeTip: {
prefix: 'Tingkatkan paket Anda ke',
des: 'Tingkatkan paket Anda untuk menyesuaikan merek Anda',
suffix: 'Sesuaikan merek Anda.',
title: 'Tingkatkan paket Anda',
},
webapp: {
changeLogoTip: 'Format SVG atau PNG dengan ukuran minimum 40x40px',
removeBrand: 'Hapus Didukung oleh Dify',
changeLogo: 'Perubahan Didukung oleh Citra Merek',
title: 'Sesuaikan merek aplikasi web',
},
app: {
title: 'Menyesuaikan merek header aplikasi',
changeLogoTip: 'Format SVG atau PNG dengan ukuran minimal 80x80px',
},
customize: {
suffix: 'untuk meningkatkan ke edisi Enterprise.',
prefix: 'Untuk menyesuaikan logo merek di dalam aplikasi, silakan',
contactUs: 'Hubungi',
},
custom: 'Kustomisasi',
uploading: 'Meng',
upload: 'Unggah',
change: 'Ubah',
restore: 'Pulihkan Default',
apply: 'Berlaku',
uploadedFail: 'Unggahan gambar gagal, silakan unggah ulang.',
}
export default translation

View File

@ -0,0 +1,209 @@
const translation = {
steps: {
header: {
fallbackRoute: 'Pengetahuan',
},
two: 'Pemrosesan Dokumen',
three: 'Eksekusi & Selesaikan',
one: 'Sumber Data',
},
error: {
unavailable: 'Pengetahuan ini tidak tersedia',
},
firecrawl: {
getApiKeyLinkText: 'Dapatkan kunci API Anda dari firecrawl.dev',
configFirecrawl: 'Mengonfigurasi 🔥Firecrawl',
apiKeyPlaceholder: 'Kunci API dari firecrawl.dev',
},
watercrawl: {
configWatercrawl: 'Mengonfigurasi Watercrawl',
getApiKeyLinkText: 'Dapatkan kunci API Anda dari watercrawl.dev',
apiKeyPlaceholder: 'Kunci API dari watercrawl.dev',
},
jinaReader: {
apiKeyPlaceholder: 'Kunci API dari jina.ai',
getApiKeyLinkText: 'Dapatkan kunci API gratis Anda di jina.ai',
configJinaReader: 'Konfigurasikan Jina Reader',
},
stepOne: {
dataSourceType: {
file: 'Impor dari file',
notion: 'Sinkronkan dari Notion',
web: 'Sinkronkan dari situs web',
},
uploader: {
validation: {
count: 'Beberapa file tidak didukung',
typeError: 'Jenis file tidak didukung',
},
buttonSingleFile: 'Seret dan lepas file, atau',
title: 'Unggah file',
browse: 'Ramban',
cancel: 'Membatalkan',
button: 'Seret dan lepas file atau folder, atau',
change: 'Ubah',
failed: 'Upload gagal',
},
modal: {
title: 'Buat Pengetahuan kosong',
placeholder: 'Silakan masukkan',
confirmButton: 'Menciptakan',
cancelButton: 'Membatalkan',
nameNotEmpty: 'Nama tidak boleh kosong',
input: 'Nama pengetahuan',
failed: 'Pembuatan gagal',
nameLengthInvalid: 'Nama harus antara 1 hingga 40 karakter',
tip: 'Pengetahuan kosong tidak akan berisi dokumen, dan Anda dapat mengunggah dokumen kapan saja.',
},
website: {
configure: 'Mengkonfigurasi',
fireCrawlNotConfigured: 'Firecrawl tidak dikonfigurasi',
chooseProvider: 'Pilih penyedia',
configureFirecrawl: 'Mengonfigurasi Firecrawl',
watercrawlDoc: 'Dokumen Watercrawl',
options: 'Pilihan',
firecrawlTitle: 'Mengekstrak konten web dengan 🔥Firecrawl',
jinaReaderNotConfigured: 'Jina Reader tidak dikonfigurasi',
preview: 'Pratayang',
resetAll: 'Atur Ulang Semua',
run: 'Lari',
limit: 'Batas',
useSitemap: 'Menggunakan peta situs',
jinaReaderDoc: 'Pelajari lebih lanjut tentang Jina Reader',
configureJinaReader: 'Konfigurasikan Jina Reader',
watercrawlTitle: 'Ekstrak konten web dengan Watercrawl',
crawlSubPage: 'Merayapi sub-halaman',
totalPageScraped: 'Total halaman yang dikikis:',
waterCrawlNotConfigured: 'Watercrawl tidak dikonfigurasi',
fireCrawlNotConfiguredDescription: 'Konfigurasikan Firecrawl dengan kunci API untuk menggunakannya.',
exceptionErrorTitle: 'Pengecualian terjadi saat menjalankan pekerjaan crawling:',
jinaReaderNotConfiguredDescription: 'Siapkan Jina Reader dengan memasukkan kunci API gratis Anda untuk akses.',
includeOnlyPaths: 'Sertakan hanya jalur',
jinaReaderTitle: 'Konversi seluruh situs ke Markdown',
excludePaths: 'Kecualikan jalur',
unknownError: 'Kesalahan tidak diketahui',
extractOnlyMainContent: 'Ekstrak hanya konten utama (tanpa header, navigasi, footer, dll.)',
useSitemapTooltip: 'Ikuti peta situs untuk meng-crawl situs. Jika tidak, Jina Reader akan merayapi secara berulang berdasarkan relevansi halaman, menghasilkan halaman yang lebih sedikit tetapi berkualitas lebih tinggi.',
maxDepth: 'Kedalaman maks',
jinaReaderDocLink: 'https://jina.ai/reader',
selectAll: 'Pilih Semua',
maxDepthTooltip: 'Kedalaman maksimum untuk di-crawl relatif terhadap URL yang dimasukkan. Kedalaman 0 hanya mengikis halaman url yang dimasukkan, kedalaman 1 mengikis url dan semuanya setelah dimasukkanURL satu /, dan seterusnya.',
waterCrawlNotConfiguredDescription: 'Konfigurasikan Watercrawl dengan kunci API untuk menggunakannya.',
firecrawlDoc: 'Dokumen Firecrawl',
configureWatercrawl: 'Mengonfigurasi Watercrawl',
},
pagePreview: 'Pratinjau Halaman',
notionSyncTitle: 'Gagasan tidak terhubung',
filePreview: 'Pratinjau File',
cancel: 'Membatalkan',
emptyDatasetCreation: 'Saya ingin membuat Pengetahuan kosong',
button: 'Depan',
notionSyncTip: 'Untuk menyinkronkan dengan Notion, koneksi ke Notion harus dibuat terlebih dahulu.',
connect: 'Buka terhubung',
},
stepTwo: {
paragraph: 'Paragraf',
QATitle: 'Segmentasi dalam format Tanya Jawab',
QALanguage: 'Segmen menggunakan',
custom: 'Adat',
fullDoc: 'Dokumen Lengkap',
overlapCheck: 'Tumpang tindih potongan tidak boleh lebih besar dari panjang potongan maksimum',
economical: 'Ekonomis',
parentChunkForContext: 'Parent-chunk untuk Konteks',
estimateCost: 'Kira',
other: 'dan lainnya',
autoDescription: 'Atur aturan potongan dan prapemrosesan secara otomatis. Pengguna yang tidak dikenal disarankan untuk memilih ini.',
reset: 'Reset',
auto: 'Otomatis',
removeExtraSpaces: 'Mengganti spasi berturut-turut, baris baru, dan tab',
indexMode: 'Metode Indeks',
useQALanguage: 'Potongan menggunakan format Tanya Jawab di',
previousStep: 'Langkah sebelumnya',
warning: 'Silakan siapkan kunci API penyedia model terlebih dahulu.',
paragraphTip: 'Mode ini membagi teks menjadi paragraf berdasarkan pembatas dan panjang potongan maksimum, menggunakan teks terpisah sebagai potongan induk untuk pengambilan.',
separatorTip: 'Pemisah adalah karakter yang digunakan untuk memisahkan teks. \\n\\n dan \\n adalah pembatas yang biasa digunakan untuk memisahkan paragraf dan baris. Dikombinasikan dengan koma (\\n\\n,\\n), paragraf akan disegmentasikan berdasarkan baris saat melebihi panjang potongan maksimum. Anda juga dapat menggunakan pembatas khusus yang ditentukan sendiri (misalnya ***).',
notionSource: 'Halaman praproses',
overlap: 'Tumpang tindih potongan',
click: 'Buka pengaturan',
highQualityTip: 'Setelah selesai menyematkan dalam mode Kualitas Tinggi, kembali ke mode Ekonomis tidak tersedia.',
previewChunk: 'Pratinjau Potongan',
webpageUnit: 'Halaman',
fullDocTip: 'Seluruh dokumen digunakan sebagai potongan induk dan diambil secara langsung. Harap dicatat bahwa untuk alasan kinerja, teks yang melebihi 10000 token akan dipotong secara otomatis.',
maxLength: 'Panjang potongan maksimum',
fileUnit: 'File',
parentChild: 'Orang tua-anak',
estimateSegment: 'Perkiraan potongan',
calculating: 'Menghitung...',
qualified: 'Kualitas Tinggi',
segmentation: 'Pengaturan Potongan',
generalTip: 'Mode pemotongan teks umum, potongan yang diambil dan ditarik kembali sama.',
characters: 'Karakter',
fileSource: 'Praproses dokumen',
parentChildDelimiterTip: 'Pemisah adalah karakter yang digunakan untuk memisahkan teks. \\n\\n direkomendasikan untuk membagi dokumen asli menjadi potongan induk yang besar. Anda juga dapat menggunakan pembatas khusus yang ditentukan sendiri.',
separatorPlaceholder: '\\n\\n untuk paragraf; \\n untuk baris',
qualifiedTip: 'Memanggil model penyematan untuk memproses dokumen untuk pengambilan yang lebih tepat membantu LLM menghasilkan jawaban berkualitas tinggi.',
previewTitle: 'Pratayang',
economicalTip: 'Menggunakan 10 kata kunci per potongan untuk pengambilan, tidak ada token yang dikonsumsi dengan mengorbankan penurunan akurasi pengambilan.',
recommend: 'Merekomendasikan',
segmentCount: 'Potongan',
removeUrlEmails: 'Menghapus semua URL dan alamat email',
childChunkForRetrieval: 'Potongan anak untuk Pengambilan',
cancel: 'Membatalkan',
notionUnit: 'Halaman',
removeStopwords: 'Hapus kata henti seperti "a", "an", "the"',
customDescription: 'Sesuaikan aturan potongan, panjang potongan, dan aturan prapemrosesan, dll.',
overlapTip: 'Mengatur tumpang tindih potongan dapat mempertahankan relevansi semantik di antara mereka, meningkatkan efek pengambilan. Disarankan untuk mengatur 10% -25% dari ukuran potongan maksimum.',
previewSwitchTipStart: 'Pratinjau potongan saat ini dalam format teks, beralih ke pratinjau format tanya jawab akan',
nextStep: 'Simpan & Proses',
previewButton: 'Beralih ke format Tanya Jawab',
notAvailableForQA: 'Tidak tersedia untuk Indeks Tanya Jawab',
previewSwitchTipEnd: 'Konsumsi token tambahan',
retrievalSettingTip: 'Untuk mengubah pengaturan pengambilan, silakan buka',
previewChunkTip: 'Klik tombol \'Pratinjau Potongan\' di sebelah kiri untuk memuat pratinjau',
sideTipP4: 'Potongan dan pembersihan yang tepat meningkatkan kinerja model, memberikan hasil yang lebih akurat dan berharga.',
previewTitleButton: 'Pratayang',
switch: 'Sakelar',
datasetSettingLink: 'Pengaturan pengetahuan.',
rules: 'Aturan Pra-pemrosesan Teks',
sideTipP2: 'Segmentasi membagi teks panjang menjadi paragraf sehingga model dapat memahami dengan lebih baik. Ini meningkatkan kualitas dan relevansi hasil model.',
sideTipP1: 'Saat memproses data teks, potongan dan pembersihan adalah dua langkah prapemrosesan yang penting.',
QATip: 'Aktifkan opsi ini akan menggunakan lebih banyak token',
qaSwitchHighQualityTipTitle: 'Format Q&A Membutuhkan Metode Pengindeksan Berkualitas Tinggi',
sideTipTitle: 'Mengapa chunk dan praproses?',
parentChildTip: 'Saat menggunakan mode induk-anak, potongan anak digunakan untuk pengambilan dan potongan induk digunakan untuk penarikan kembali sebagai konteks.',
sideTipP3: 'Pembersihan menghapus karakter dan format yang tidak perlu, membuat Pengetahuan lebih bersih dan lebih mudah diuraikan.',
notAvailableForParentChild: 'Tidak tersedia untuk Indeks Induk-anak',
separator: 'Pembatas',
save: 'Simpan & Proses',
preview: 'Pratayang',
websiteSource: 'Situs web praproses',
parentChildChunkDelimiterTip: 'Pemisah adalah karakter yang digunakan untuk memisahkan teks. \\n direkomendasikan untuk membagi potongan induk menjadi potongan anak kecil. Anda juga dapat menggunakan pembatas khusus yang ditentukan sendiri.',
qaSwitchHighQualityTipContent: 'Saat ini, hanya metode indeks berkualitas tinggi yang mendukung pemotongan format Tanya Jawab. Apakah Anda ingin beralih ke mode berkualitas tinggi?',
general: 'Umum',
indexSettingTip: 'Untuk mengubah metode indeks & model penyematan, silakan pergi ke',
},
stepThree: {
modelTitle: 'Apakah Anda yakin untuk berhenti menyematkan?',
sideTipTitle: 'Apa selanjutnya',
additionTitle: '🎉 Dokumen diunggah',
creationTitle: '🎉 Pengetahuan yang diciptakan',
additionP2: ', Anda dapat menemukannya di daftar dokumen Pengetahuan.',
additionP1: 'Dokumen telah diunggah ke Knowledge',
navTo: 'Buka dokumen',
resume: 'Melanjutkan pemrosesan',
stop: 'Hentikan pemrosesan',
creationContent: 'Kami secara otomatis menamai Pengetahuan, Anda dapat memodifikasinya kapan saja.',
modelButtonConfirm: 'Mengkonfirmasi',
sideTipContent: 'Setelah dokumen selesai diindeks, Pengetahuan dapat diintegrasikan ke dalam aplikasi sebagai konteks, Anda dapat menemukan pengaturan konteks di halaman orkestrasi perintah. Anda juga dapat membuatnya sebagai plugin pengindeksan ChatGPT independen untuk dirilis.',
modelButtonCancel: 'Membatalkan',
label: 'Nama pengetahuan',
modelContent: 'Jika Anda perlu melanjutkan pemrosesan nanti, Anda akan melanjutkan dari bagian terakhir yang Anda tinggalkan.',
},
otherDataSource: {
title: 'Terhubung ke sumber data lain?',
learnMore: 'Pelajari lebih lanjut',
description: 'Saat ini, basis pengetahuan Dify hanya memiliki sumber data yang terbatas. Menyumbangkan sumber data ke basis pengetahuan Dify adalah cara yang fantastis untuk membantu meningkatkan fleksibilitas dan kekuatan platform bagi semua pengguna. Panduan kontribusi kami memudahkan untuk memulai. Silakan klik tautan di bawah ini untuk mempelajari lebih lanjut.',
},
}
export default translation

View File

@ -0,0 +1,398 @@
const translation = {
list: {
table: {
header: {
hitCount: 'HITUNGAN PENGAMBILAN',
uploadTime: 'WAKTU UNGGAH',
fileName: 'NAMA',
words: 'KATA',
status: 'KEADAAN',
action: 'PERBUATAN',
chunkingMode: 'CHUNKING MODE',
},
rename: 'Ubah nama',
name: 'Nama',
},
action: {
add: 'Tambahkan potongan',
uploadFile: 'Unggah file baru',
addButton: 'Tambahkan potongan',
delete: 'Menghapus',
archive: 'Mengarsipkan',
pause: 'Jeda',
settings: 'Pengaturan Chunking',
enableWarning: 'File yang diarsipkan tidak dapat diaktifkan',
resume: 'Melanjutkan',
batchAdd: 'Tambahkan batch',
unarchive: 'Batalkan arsip',
sync: 'Sync',
},
index: {
all: 'Semua',
enable: 'Mengaktifkan',
enableTip: 'File dapat diindeks',
disableTip: 'File tidak dapat diindeks',
disable: 'Menonaktifkan',
},
status: {
error: 'Kesalahan',
archived: 'Diarsipkan',
paused: 'Berhenti',
queuing: 'Antrian',
enabled: 'Diaktifkan',
disabled: 'Cacat',
available: 'Tersedia',
indexing: 'Pengindeksan',
},
empty: {
upload: {
tip: 'Anda dapat mengunggah file, menyinkronkan dari situs web, atau dari aplikasi web seperti Notion, GitHub, dll.',
},
sync: {
tip: 'Dify akan mengunduh file dari Notion Anda secara berkala dan menyelesaikan pemrosesan.',
},
title: 'Belum ada dokumentasi',
},
delete: {
title: 'Apakah Anda yakin Hapus?',
content: 'Jika Anda perlu melanjutkan pemrosesan nanti, Anda akan melanjutkan dari bagian terakhir',
},
batchModal: {
cancel: 'Membatalkan',
run: 'Jalankan Batch',
template: 'Unduh templat di sini',
csvUploadTitle: 'Seret dan lepas file CSV Anda di sini, atau',
question: 'pertanyaan',
tip: 'File CSV harus sesuai dengan struktur berikut:',
contentTitle: 'konten potongan',
processing: 'Dalam pemrosesan batch',
content: 'puas',
ok: 'OKE',
error: 'Kesalahan Impor',
completed: 'Impor selesai',
answer: 'menjawab',
runError: 'Menjalankan batch gagal',
browse: 'ramban',
title: 'Tambahkan potongan batch',
},
desc: 'Semua file Pengetahuan ditampilkan di sini, dan seluruh Pengetahuan dapat ditautkan ke kutipan Dify atau diindeks melalui plugin Obrolan.',
addPages: 'Tambahkan Halaman',
addFile: 'Tambahkan file',
learnMore: 'Pelajari lebih lanjut',
addUrl: 'Tambahkan URL',
title: 'Dokumen',
},
metadata: {
placeholder: {
select: 'Pilih',
add: 'Tambah',
},
source: {
notion: 'Sinkronkan formulir Notion',
upload_file: 'Unggah File',
github: 'Sinkronkan formulir Github',
},
type: {
book: 'Buku',
github: 'Sinkronkan formulir Github',
webPage: 'Halaman Web',
paper: 'Kertas',
technicalParameters: 'Parameter teknis',
personalDocument: 'Dokumen Pribadi',
wikipediaEntry: 'Entri Wikipedia',
socialMediaPost: 'Postingan Media Sosial',
notion: 'Sinkronkan formulir Notion',
IMChat: 'Obrolan IM',
businessDocument: 'Dokumen Bisnis',
},
field: {
processRule: {
processClean: 'Proses Teks Bersih',
segmentRule: 'Aturan Potongan',
processDoc: 'Dokumen Proses',
segmentLength: 'Panjang Potongan',
},
book: {
publicationDate: 'Tanggal Publikasi',
language: 'Bahasa',
category: 'Golongan',
publisher: 'Penerbit',
ISBN: 'ISBN',
author: 'Pengarang',
title: 'Titel',
},
webPage: {
language: 'Bahasa',
authorPublisher: 'Penulis/Penerbit',
description: 'Deskripsi',
topicKeywords: 'Topik/Kata Kunci',
url: 'URL',
title: 'Titel',
publishDate: 'Tanggal Publikasi',
},
paper: {
title: 'Titel',
DOI: 'DOI',
volumeIssuePage: 'Volume/Edisi/Halaman',
topicsKeywords: 'Topik/Kata Kunci',
abstract: 'Abstrak',
author: 'Pengarang',
journalConferenceName: 'Nama Jurnal/Konferensi',
language: 'Bahasa',
publishDate: 'Tanggal Publikasi',
},
socialMediaPost: {
postURL: 'URL posting',
topicsTags: 'Topik/Tag',
publishDate: 'Tanggal Publikasi',
platform: 'Balei-balei',
authorUsername: 'Penulis/Nama Pengguna',
},
personalDocument: {
author: 'Pengarang',
title: 'Titel',
tagsCategory: 'Tags/Kategori',
documentType: 'Jenis Dokumen',
creationDate: 'Tanggal Pembuatan',
lastModifiedDate: 'Tanggal Terakhir Diubah',
},
businessDocument: {
creationDate: 'Tanggal Pembuatan',
author: 'Pengarang',
title: 'Titel',
departmentTeam: 'Departemen/Tim',
lastModifiedDate: 'Tanggal Terakhir Diubah',
documentType: 'Jenis Dokumen',
},
IMChat: {
endDate: 'Tanggal Berakhir',
participants: 'Peserta',
topicsKeywords: 'Topik/Kata Kunci',
startDate: 'Tanggal Mulai',
fileType: 'Jenis File',
chatPlatform: 'Platform Obrolan',
chatPartiesGroupName: 'Chat Party/Nama Grup',
},
wikipediaEntry: {
webpageURL: 'URL halaman web',
title: 'Titel',
language: 'Bahasa',
lastEditDate: 'Tanggal Edit Terakhir',
editorContributor: 'Editor/Kontributor',
summaryIntroduction: 'Ringkasan/Pendahuluan',
},
notion: {
url: 'URL',
createdTime: 'Waktu yang Diciptakan',
description: 'Deskripsi',
title: 'Titel',
author: 'Pengarang',
lastModifiedTime: 'Waktu Terakhir Dimodifikasi',
language: 'Bahasa',
tag: 'Tag',
},
github: {
repoDesc: 'Deskripsi Repo',
lastCommitTime: 'Waktu Komitmen Terakhir',
lastCommitAuthor: 'Penulis Komitmen Terakhir',
url: 'URL',
fileName: 'Nama File',
programmingLang: 'Bahasa pemrograman',
repoName: 'Nama Repo',
filePath: 'Jalur File',
repoOwner: 'Pemilik Repo',
license: 'Lisensi',
},
originInfo: {
source: 'Sumber',
originalFilename: 'Nama file asli',
lastUpdateDate: 'Tanggal pembaruan terakhir',
originalFileSize: 'Ukuran file asli',
uploadDate: 'Tanggal upload',
},
technicalParameters: {
avgParagraphLength: 'Rata-rata panjang paragraf',
paragraphs: 'Paragraf',
segmentLength: 'Panjang potongan',
segmentSpecification: 'Spesifikasi potongan',
embeddingTime: 'Waktu penyematan',
embeddedSpend: 'Pengeluaran tertanam',
hitCount: 'Jumlah pengambilan',
},
},
languageMap: {
en: 'Inggris',
es: 'Spanyol',
ar: 'Arab',
fr: 'Prancis',
ko: 'Korea',
pl: 'Polandia',
no: 'Norwegia',
tr: 'Turki',
id: 'Indonesia',
ja: 'Jepang',
nl: 'Belanda',
zh: 'Cina',
fi: 'Finlandia',
hi: 'Hindi',
el: 'Yunani',
th: 'Thai',
it: 'Italia',
pt: 'Portugis',
ru: 'Rusia',
he: 'Ibrani',
hu: 'Hongaria',
de: 'Jerman',
da: 'Denmark',
cs: 'Ceko',
sv: 'Swedia',
},
categoryMap: {
book: {
biography: 'Biografi',
businessEconomics: 'BisnisEkonomi',
health: 'Kesehatan',
religion: 'Agama',
poetry: 'Puisi',
technology: 'Teknologi',
travel: 'Bepergian',
science: 'Ilmu',
comicsGraphicNovels: 'KomikNovel Grafis',
philosophy: 'Filsafat',
socialSciences: 'Ilmu Sosial',
cooking: 'Memasak',
other: 'Lain',
selfHelp: 'Bantuan Mandiri',
art: 'Seni',
history: 'Sejarah',
fiction: 'Fiksi',
drama: 'Drama',
childrenYoungAdults: 'Anak-anakMudaDewasa',
education: 'Pendidikan',
},
personalDoc: {
bookExcerpt: 'Kutipan Buku',
projectOverview: 'Ikhtisar Proyek',
designDraft: 'Draf Desain',
blogDraft: 'Draf Blog',
codeSnippet: 'Cuplikan Kode',
notes: 'Catatan',
diary: 'Buku harian',
schedule: 'Jadwal',
creativeWriting: 'Penulisan Kreatif',
other: 'Lain',
researchReport: 'Laporan Penelitian',
list: 'Daftar',
personalResume: 'Resume Pribadi',
photoCollection: 'Koleksi Foto',
},
businessDoc: {
meetingMinutes: 'Risalah Rapat',
emailCorrespondence: 'Korespondensi Email',
researchReport: 'Laporan Penelitian',
designDocument: 'Dokumen Desain',
productSpecification: 'Spesifikasi Produk',
projectPlan: 'Rencana Proyek',
requirementsDocument: 'Dokumen Persyaratan',
financialReport: 'Laporan Keuangan',
contractsAgreements: 'Kontrak & Perjanjian',
proposal: 'Proposal',
employeeHandbook: 'Buku Pegangan Karyawan',
marketAnalysis: 'Analisis Pasar',
trainingMaterials: 'Materi Pelatihan',
teamStructure: 'Struktur Tim',
other: 'Lain',
policiesProcedures: 'Kebijakan & Prosedur',
},
},
docTypeSelectWarning: 'Jika jenis dokumen diubah, metadata yang sekarang terisi tidak akan lagi dipertahankan',
firstMetaAction: 'Ayo',
docTypeChangeTitle: 'Mengubah jenis dokumen',
dateTimeFormat: 'MMMM D, YYYY hh:mm A',
docTypeSelectTitle: 'Silakan pilih jenis dokumen',
desc: 'Pelabelan metadata untuk dokumen memungkinkan AI mengaksesnya tepat waktu dan mengekspos sumber referensi bagi pengguna.',
title: 'Metadata',
},
embedding: {
completed: 'Penyematan selesai',
processing: 'Pemrosesan penyematan...',
pause: 'Jeda',
textCleaning: 'Aturan Prapemrosesan Teks',
automatic: 'Otomatis',
estimate: 'Perkiraan konsumsi',
economy: 'Mode ekonomi',
error: 'Kesalahan penyematan',
docName: 'Dokumen pra-pemrosesan',
previewTip: 'Pratinjau paragraf akan tersedia setelah penyematan selesai',
mode: 'Pengaturan Chunking',
parentMaxTokens: 'Ortu',
hierarchical: 'Orang tua-anak',
highQuality: 'Mode berkualitas tinggi',
childMaxTokens: 'Anak',
segments: 'Paragraf',
segmentLength: 'Panjang Potongan Maksimum',
paused: 'Penyematan dijeda',
stop: 'Hentikan pemrosesan',
custom: 'Adat',
resume: 'Melanjutkan',
},
segment: {
searchResults_one: 'HASIL',
editParentChunk: 'Edit Potongan Induk',
contentEmpty: 'Konten tidak boleh kosong',
keywordEmpty: 'Kata kunci tidak boleh kosong',
chunkDetail: 'Detail Potongan',
chunkAdded: '1 potongan ditambahkan',
collapseChunks: 'Ciutkan potongan',
delete: 'Hapus potongan ini ?',
addChunk: 'Tambahkan Potongan',
childChunks_one: 'POTONGAN ANAK',
empty: 'Tidak ada Potongan yang ditemukan',
regeneratingTitle: 'Meregenerasi potongan turunan',
keywords: 'KATA KUNCI',
answerPlaceholder: 'Tambahkan jawaban di sini',
parentChunks_other: 'POTONGAN INDUK',
childChunk: 'Potongan Anak',
chunk: 'Potongan',
childChunkAdded: '1 potongan anak ditambahkan',
characters_one: 'watak',
dateTimeFormat: 'MM / DD / YYYY h: mm',
edited: 'DIEDIT',
editChunk: 'Edit Potongan',
regenerationConfirmTitle: 'Apakah Anda ingin meregenerasi potongan turunan?',
searchResults_zero: 'HASIL',
questionPlaceholder: 'Tambahkan pertanyaan di sini',
addAnother: 'Tambahkan yang lain',
expandChunks: 'Perluas potongan',
regenerationSuccessTitle: 'Regenerasi selesai',
answerEmpty: 'Jawaban tidak bisa kosong',
regenerationConfirmMessage: 'Meregenerasi potongan turunan akan menimpa potongan turunan saat ini, termasuk potongan yang diedit dan potongan yang baru ditambahkan. Regenerasi tidak dapat dibatalkan.',
hitCount: 'Jumlah pengambilan',
regeneratingMessage: 'Ini mungkin perlu beberapa saat, harap tunggu ...',
vectorHash: 'Hash vektor:',
editChildChunk: 'Edit Potongan Anak',
keywordError: 'Panjang maksimum kata kunci adalah 20',
addKeyWord: 'Tambahkan kata kunci',
chunks_one: 'POTONGAN',
childChunks_other: 'POTONGAN ANAK',
regenerationSuccessMessage: 'Anda dapat menutup jendela ini.',
editedAt: 'Diedit di',
clearFilter: 'Hapus filter',
characters_other: 'Karakter',
contentPlaceholder: 'Tambahkan konten di sini',
newChunk: 'Potongan Baru',
newTextSegment: 'Segmen Teks Baru',
newChildChunk: 'Potongan Anak Baru',
chunks_other: 'POTONGAN',
questionEmpty: 'Pertanyaan tidak bisa kosong',
parentChunk: 'Potongan Induk',
addChildChunk: 'Tambahkan Potongan Anak',
parentChunks_one: 'POTONGAN INDUK',
keywordDuplicate: 'Kata kunci sudah ada',
paragraphs: 'Paragraf',
newQaSegment: 'Segmen Tanya Jawab Baru',
searchResults_other: 'HASIL',
},
}
export default translation

View File

@ -0,0 +1,32 @@
const translation = {
table: {
header: {
text: 'Teks',
source: 'Sumber',
time: 'Waktu',
},
},
input: {
title: 'Teks sumber',
countWarning: 'Hingga 200 karakter.',
placeholder: 'Silakan masukkan teks, disarankan untuk memasukkan kalimat deklaratif singkat.',
indexWarning: 'Pengetahuan berkualitas tinggi saja.',
testing: 'Ujian',
},
hit: {
emptyTip: 'Hasil Pengujian Pengambilan akan ditampilkan di sini',
},
keyword: 'Kata kunci',
noRecentTip: 'Tidak ada hasil kueri terbaru di sini',
records: 'Catatan',
open: 'Buka',
settingTitle: 'Pengaturan Pengambilan',
dateTimeFormat: 'MM / DD / YYYY hh: mm A',
desc: 'Uji efek pukulan Pengetahuan berdasarkan teks kueri yang diberikan.',
viewDetail: 'Lihat Detail',
viewChart: 'Lihat GRAFIK VAKTOR',
chunkDetail: 'Detail Potongan',
title: 'Tes Pengambilan',
}
export default translation

View File

@ -0,0 +1,43 @@
const translation = {
form: {
retrievalSetting: {
title: 'Pengaturan Pengambilan',
description: 'tentang metode pengambilan.',
longDescription: 'tentang metode pengambilan, Anda dapat mengudagnya kapan saja di pengaturan Pengetahuan.',
method: 'Metode Pengambilan',
learnMore: 'Pelajari lebih lanjut',
},
save: 'Simpan',
embeddingModel: 'Menyematkan Model',
namePlaceholder: 'Silakan masukkan nama Pengetahuan',
permissions: 'Izin',
embeddingModelTipLink: 'Pengaturan',
descInfo: 'Silakan tulis deskripsi tekstual yang jelas untuk menguraikan isi Pengetahuan. Deskripsi ini akan digunakan sebagai dasar untuk pencocokan saat memilih dari beberapa Pengetahuan untuk inferensi.',
searchModel: 'Model pencarian',
nameError: 'Nama tidak boleh kosong',
indexMethodEconomy: 'Ekonomis',
indexMethodHighQuality: 'Kualitas Tinggi',
permissionsInvitedMembers: 'Anggota tim parsial',
permissionsAllMember: 'Semua anggota tim',
desc: 'Deskripsi Pengetahuan',
upgradeHighQualityTip: 'Setelah memutakhirkan ke mode Kualitas Tinggi, kembali ke mode Ekonomis tidak tersedia',
descPlaceholder: 'Jelaskan apa yang ada dalam kumpulan data ini. Deskripsi terperinci memungkinkan AI mengakses konten kumpulan data tepat waktu. Jika kosong, Dify akan menggunakan strategi hit default.',
retrievalSettings: 'Pengaturan Pengambilan',
indexMethodChangeToEconomyDisabledTip: 'Tidak tersedia untuk downgrade dari HQ ke ECO',
indexMethod: 'Metode Indeks',
me: '(Anda)',
externalKnowledgeID: 'ID Pengetahuan Eksternal',
descWrite: 'Pelajari cara menulis deskripsi Pengetahuan yang baik.',
name: 'Nama Pengetahuan',
embeddingModelTip: 'Ubah model yang disematkan, silakan buka',
externalKnowledgeAPI: 'API Pengetahuan Eksternal',
helpText: 'Pelajari cara menulis deskripsi himpunan data yang baik.',
indexMethodHighQualityTip: 'Memanggil model penyematan untuk memproses dokumen untuk pengambilan yang lebih tepat membantu LLM menghasilkan jawaban berkualitas tinggi.',
permissionsOnlyMe: 'Hanya saya',
indexMethodEconomyTip: 'Menggunakan 10 kata kunci per potongan untuk pengambilan, tidak ada token yang dikonsumsi dengan mengorbankan penurunan akurasi pengambilan.',
},
desc: 'Di sini Anda dapat memodifikasi properti dan pengaturan pengambilan Pengetahuan ini.',
title: 'Setelan pengetahuan',
}
export default translation

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