From 8b601a983cf5402c1c5895d27a7e4e7ed33f2132 Mon Sep 17 00:00:00 2001 From: Yongtao Huang Date: Sat, 16 Aug 2025 11:08:30 +0800 Subject: [PATCH 01/78] Fix missing user_id in trace_manager (#24024) --- api/core/app/apps/chat/app_generator.py | 4 +++- api/core/app/apps/completion/app_generator.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/api/core/app/apps/chat/app_generator.py b/api/core/app/apps/chat/app_generator.py index 0c76cc39ae..c273776eb1 100644 --- a/api/core/app/apps/chat/app_generator.py +++ b/api/core/app/apps/chat/app_generator.py @@ -140,7 +140,9 @@ class ChatAppGenerator(MessageBasedAppGenerator): ) # get tracing instance - trace_manager = TraceQueueManager(app_id=app_model.id) + trace_manager = TraceQueueManager( + app_id=app_model.id, user_id=user.id if isinstance(user, Account) else user.session_id + ) # init application generate entity application_generate_entity = ChatAppGenerateEntity( diff --git a/api/core/app/apps/completion/app_generator.py b/api/core/app/apps/completion/app_generator.py index 9356bd1cea..64dade2968 100644 --- a/api/core/app/apps/completion/app_generator.py +++ b/api/core/app/apps/completion/app_generator.py @@ -124,7 +124,9 @@ class CompletionAppGenerator(MessageBasedAppGenerator): ) # get tracing instance - trace_manager = TraceQueueManager(app_model.id) + trace_manager = TraceQueueManager( + app_id=app_model.id, user_id=user.id if isinstance(user, Account) else user.session_id + ) # init application generate entity application_generate_entity = CompletionAppGenerateEntity( From af10b3c5fa49b93b56468370fd3986ea3c7586d3 Mon Sep 17 00:00:00 2001 From: Yongtao Huang Date: Sat, 16 Aug 2025 19:28:31 +0800 Subject: [PATCH 02/78] Fix: add 'api_key' alias for backward compatibility (#24022) Signed-off-by: Yongtao Huang Co-authored-by: Yongtao Huang <99629139+hyongtao-db@users.noreply.github.com> --- api/core/tools/entities/tool_entities.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/api/core/tools/entities/tool_entities.py b/api/core/tools/entities/tool_entities.py index 5377cbbb69..5ffba07b44 100644 --- a/api/core/tools/entities/tool_entities.py +++ b/api/core/tools/entities/tool_entities.py @@ -108,10 +108,18 @@ class ApiProviderAuthType(Enum): :param value: mode value :return: mode """ + # 'api_key' deprecated in PR #21656 + # normalize & tiny alias for backward compatibility + v = (value or "").strip().lower() + if v == "api_key": + v = cls.API_KEY_HEADER.value + for mode in cls: - if mode.value == value: + if mode.value == v: return mode - raise ValueError(f"invalid mode value {value}") + + valid = ", ".join(m.value for m in cls) + raise ValueError(f"invalid mode value '{value}', expected one of: {valid}") class ToolInvokeMessage(BaseModel): From 32fa817eaa3642b8d1d4d6f65c605c6f8cf6e9cd Mon Sep 17 00:00:00 2001 From: Yongtao Huang Date: Sat, 16 Aug 2025 19:29:19 +0800 Subject: [PATCH 03/78] Update mypy.ini (#24014) Co-authored-by: Yongtao Huang <99629139+hyongtao-db@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/events/{event_handlers => }/document_index_event.py | 0 api/events/event_handlers/create_document_index.py | 2 +- api/mypy.ini | 3 +-- 3 files changed, 2 insertions(+), 3 deletions(-) rename api/events/{event_handlers => }/document_index_event.py (100%) diff --git a/api/events/event_handlers/document_index_event.py b/api/events/document_index_event.py similarity index 100% rename from api/events/event_handlers/document_index_event.py rename to api/events/document_index_event.py diff --git a/api/events/event_handlers/create_document_index.py b/api/events/event_handlers/create_document_index.py index bdb69945f0..c607161e2a 100644 --- a/api/events/event_handlers/create_document_index.py +++ b/api/events/event_handlers/create_document_index.py @@ -5,7 +5,7 @@ import click from werkzeug.exceptions import NotFound from core.indexing_runner import DocumentIsPausedError, IndexingRunner -from events.event_handlers.document_index_event import document_index_created +from events.document_index_event import document_index_created from extensions.ext_database import db from libs.datetime_utils import naive_utc_now from models.dataset import Document diff --git a/api/mypy.ini b/api/mypy.ini index 6836b2602b..3a6a54afe1 100644 --- a/api/mypy.ini +++ b/api/mypy.ini @@ -5,8 +5,7 @@ check_untyped_defs = True cache_fine_grained = True sqlite_cache = True exclude = (?x)( - core/model_runtime/model_providers/ - | tests/ + tests/ | migrations/ ) From d92ddc4dd4f4ee0cda17b956c0142ce08d8859f4 Mon Sep 17 00:00:00 2001 From: kurokobo Date: Sun, 17 Aug 2025 10:32:57 +0900 Subject: [PATCH 04/78] chore(i18n): correct japanese translation (#24041) --- web/i18n/ja-JP/app-annotation.ts | 2 +- web/i18n/ja-JP/common.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/i18n/ja-JP/app-annotation.ts b/web/i18n/ja-JP/app-annotation.ts index 6d7edf7077..3ccbcc30c6 100644 --- a/web/i18n/ja-JP/app-annotation.ts +++ b/web/i18n/ja-JP/app-annotation.ts @@ -17,7 +17,7 @@ const translation = { bulkImport: '一括インポート', bulkExport: '一括エクスポート', clearAll: 'すべて削除', - clearAllConfirm: 'すべての寸法を削除?', + clearAllConfirm: 'すべての注釈を削除しますか?', }, }, editModal: { diff --git a/web/i18n/ja-JP/common.ts b/web/i18n/ja-JP/common.ts index d0a6b64d6e..74246270e8 100644 --- a/web/i18n/ja-JP/common.ts +++ b/web/i18n/ja-JP/common.ts @@ -565,7 +565,7 @@ const translation = { overview: '監視', promptEng: 'オーケストレート', apiAccess: 'API アクセス', - logAndAnn: 'ログ&アナウンス', + logAndAnn: 'ログ&注釈', logs: 'ログ', }, environment: { From 0a9af4519462e78548142e689f2a6998ea63687e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=9B=E9=9C=B2=E5=85=88=E7=94=9F?= Date: Sun, 17 Aug 2025 09:33:22 +0800 Subject: [PATCH 05/78] no used function for message_queue. (#24027) Signed-off-by: zhanluxianshen Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/core/app/apps/message_based_app_queue_manager.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/api/core/app/apps/message_based_app_queue_manager.py b/api/core/app/apps/message_based_app_queue_manager.py index 8507f23f17..4100a0d5a9 100644 --- a/api/core/app/apps/message_based_app_queue_manager.py +++ b/api/core/app/apps/message_based_app_queue_manager.py @@ -6,7 +6,6 @@ from core.app.entities.queue_entities import ( MessageQueueMessage, QueueAdvancedChatMessageEndEvent, QueueErrorEvent, - QueueMessage, QueueMessageEndEvent, QueueStopEvent, ) @@ -22,15 +21,6 @@ class MessageBasedAppQueueManager(AppQueueManager): self._app_mode = app_mode self._message_id = str(message_id) - def construct_queue_message(self, event: AppQueueEvent) -> QueueMessage: - return MessageQueueMessage( - task_id=self._task_id, - message_id=self._message_id, - conversation_id=self._conversation_id, - app_mode=self._app_mode, - event=event, - ) - def _publish(self, event: AppQueueEvent, pub_from: PublishFrom) -> None: """ Publish event to queue From c69634085deb300ed9c8927405dc95718c880597 Mon Sep 17 00:00:00 2001 From: kurokobo Date: Mon, 18 Aug 2025 00:14:37 +0900 Subject: [PATCH 06/78] Revert "Fix: Correct file variable handling for custom tools in workflow (#24061) --- api/core/workflow/nodes/tool/tool_node.py | 27 ----------------------- 1 file changed, 27 deletions(-) diff --git a/api/core/workflow/nodes/tool/tool_node.py b/api/core/workflow/nodes/tool/tool_node.py index df89b2476d..4c8e13de70 100644 --- a/api/core/workflow/nodes/tool/tool_node.py +++ b/api/core/workflow/nodes/tool/tool_node.py @@ -318,33 +318,6 @@ class ToolNode(BaseNode): json.append(message.message.json_object) elif message.type == ToolInvokeMessage.MessageType.LINK: assert isinstance(message.message, ToolInvokeMessage.TextMessage) - - if message.meta: - transfer_method = message.meta.get("transfer_method", FileTransferMethod.TOOL_FILE) - else: - transfer_method = FileTransferMethod.TOOL_FILE - - tool_file_id = message.message.text.split("/")[-1].split(".")[0] - - with Session(db.engine) as session: - stmt = select(ToolFile).where(ToolFile.id == tool_file_id) - tool_file = session.scalar(stmt) - if tool_file is None: - raise ToolFileError(f"Tool file {tool_file_id} does not exist") - - mapping = { - "tool_file_id": tool_file_id, - "type": file_factory.get_file_type_by_mime_type(tool_file.mimetype), - "transfer_method": transfer_method, - "url": message.message.text, - } - - file = file_factory.build_from_mapping( - mapping=mapping, - tenant_id=self.tenant_id, - ) - files.append(file) - stream_text = f"Link: {message.message.text}\n" text += stream_text yield RunStreamChunkEvent(chunk_content=stream_text, from_variable_selector=[node_id, "text"]) From ff52a54fef8d6bb9da539f840490b9a398734acd Mon Sep 17 00:00:00 2001 From: Yongtao Huang Date: Mon, 18 Aug 2025 09:22:59 +0800 Subject: [PATCH 07/78] Restore useLabelStore mistakenly removed in commit 403e2d58 (#24052) Co-authored-by: Yongtao Huang <99629139+hyongtao-db@users.noreply.github.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> --- web/app/components/tools/labels/store.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 web/app/components/tools/labels/store.ts diff --git a/web/app/components/tools/labels/store.ts b/web/app/components/tools/labels/store.ts new file mode 100644 index 0000000000..c19991dfd4 --- /dev/null +++ b/web/app/components/tools/labels/store.ts @@ -0,0 +1,15 @@ +import { create } from 'zustand' +import type { Label } from './constant' + +type State = { + labelList: Label[] +} + +type Action = { + setLabelList: (labelList?: Label[]) => void +} + +export const useStore = create(set => ({ + labelList: [], + setLabelList: labelList => set(() => ({ labelList })), +})) From b475a6b257b6d5d609b6bdaf1e9e49d6b97f32cb Mon Sep 17 00:00:00 2001 From: lyzno1 <92089059+lyzno1@users.noreply.github.com> Date: Mon, 18 Aug 2025 09:29:52 +0800 Subject: [PATCH 08/78] chore: synchronize translations (#24044) --- web/i18n/fa-IR/billing.ts | 40 +++++++++++++---------------------- web/i18n/fr-FR/billing.ts | 34 +++++++++++------------------- web/i18n/hi-IN/billing.ts | 26 +++++++---------------- web/i18n/it-IT/billing.ts | 42 +++++++++++++------------------------ web/i18n/ja-JP/workflow.ts | 1 + web/i18n/pl-PL/billing.ts | 42 +++++++++++++------------------------ web/i18n/pt-BR/billing.ts | 39 +++++++++++++--------------------- web/i18n/ro-RO/billing.ts | 32 ++++++++++------------------ web/i18n/ru-RU/billing.ts | 34 +++++++++++------------------- web/i18n/sl-SI/billing.ts | 38 +++++++++++++-------------------- web/i18n/th-TH/billing.ts | 40 +++++++++++++---------------------- web/i18n/tr-TR/billing.ts | 34 +++++++++++------------------- web/i18n/uk-UA/billing.ts | 36 ++++++++++++------------------- web/i18n/vi-VN/billing.ts | 36 ++++++++++++------------------- web/i18n/zh-Hant/billing.ts | 16 ++++++++++++++ 15 files changed, 185 insertions(+), 305 deletions(-) diff --git a/web/i18n/fa-IR/billing.ts b/web/i18n/fa-IR/billing.ts index e4de29ced5..68eff70426 100644 --- a/web/i18n/fa-IR/billing.ts +++ b/web/i18n/fa-IR/billing.ts @@ -23,19 +23,14 @@ const translation = { contractSales: 'تماس با فروش', contractOwner: 'تماس با مدیر تیم', startForFree: 'رایگان شروع کنید', - getStartedWith: 'شروع کنید با ', contactSales: 'تماس با فروش', talkToSales: 'صحبت با فروش', modelProviders: 'ارائه‌دهندگان مدل', - teamMembers: 'اعضای تیم', annotationQuota: 'سهمیه حاشیه‌نویسی', buildApps: 'ساخت اپلیکیشن‌ها', vectorSpace: 'فضای وکتور', - vectorSpaceBillingTooltip: 'هر 1 مگابایت می‌تواند حدود 1.2 میلیون کاراکتر از داده‌های وکتور شده را ذخیره کند (براساس تخمین با استفاده از OpenAI Embeddings، متفاوت بر اساس مدل‌ها).', vectorSpaceTooltip: 'فضای وکتور سیستم حافظه بلند مدت است که برای درک داده‌های شما توسط LLM‌ها مورد نیاز است.', - documentsUploadQuota: 'سهمیه بارگذاری مستندات', documentProcessingPriority: 'اولویت پردازش مستندات', - documentProcessingPriorityTip: 'برای اولویت پردازش بالاتر مستندات، لطفاً طرح خود را ارتقاء دهید.', documentProcessingPriorityUpgrade: 'داده‌های بیشتری را با دقت بالاتر و سرعت بیشتر پردازش کنید.', priority: { 'standard': 'استاندارد', @@ -103,19 +98,16 @@ const translation = { sandbox: { name: 'محیط آزمایشی', description: '200 بار آزمایش رایگان GPT', - includesTitle: 'شامل:', for: 'دوره آزمایشی رایگان قابلیت‌های اصلی', }, professional: { name: 'حرفه‌ای', description: 'برای افراد و تیم‌های کوچک برای باز کردن قدرت بیشتر به طور مقرون به صرفه.', - includesTitle: 'همه چیز در طرح رایگان، به علاوه:', for: 'برای توسعه‌دهندگان مستقل/تیم‌های کوچک', }, team: { name: 'تیم', description: 'همکاری بدون محدودیت و لذت بردن از عملکرد برتر.', - includesTitle: 'همه چیز در طرح حرفه‌ای، به علاوه:', for: 'برای تیم‌های متوسط', }, enterprise: { @@ -123,15 +115,15 @@ const translation = { description: 'دریافت کامل‌ترین قابلیت‌ها و پشتیبانی برای سیستم‌های بزرگ و بحرانی.', includesTitle: 'همه چیز در طرح تیم، به علاوه:', features: { - 0: 'راهکارهای استقرار مقیاس‌پذیر در سطح سازمانی', - 8: 'پشتیبانی فنی حرفه‌ای', - 3: 'چندین فضای کاری و مدیریت سازمانی', - 5: 'SLA های توافق شده توسط شرکای Dify', - 4: 'SSO', - 2: 'ویژگی‌های انحصاری سازمانی', - 1: 'مجوز صدور مجوز تجاری', - 6: 'امنیت و کنترل‌های پیشرفته', - 7: 'به‌روزرسانی‌ها و نگهداری توسط دیفی به‌طور رسمی', + 4: 'Sso', + 1: 'مجوز جواز تجاری', + 2: 'ویژگی های انحصاری سازمانی', + 8: 'پشتیبانی فنی حرفه ای', + 5: 'SLA های مذاکره شده توسط Dify Partners', + 6: 'امنیت و کنترل پیشرفته', + 3: 'فضاهای کاری چندگانه و مدیریت سازمانی', + 7: 'به روز رسانی و نگهداری توسط Dify به طور رسمی', + 0: 'راه حل های استقرار مقیاس پذیر در سطح سازمانی', }, price: 'سفارشی', btnText: 'تماس با فروش', @@ -140,9 +132,9 @@ const translation = { }, community: { features: { - 0: 'تمام ویژگی‌های اصلی منتشر شده در مخزن عمومی', - 2: 'با رعایت مجوز منبع باز دیفی', 1: 'فضای کاری واحد', + 2: 'با مجوز منبع باز Dify مطابقت دارد', + 0: 'تمام ویژگی های اصلی در مخزن عمومی منتشر شده است', }, btnText: 'شروع کنید با جامعه', price: 'رایگان', @@ -153,10 +145,10 @@ const translation = { }, premium: { features: { - 1: 'محل کار واحد', - 0: 'قابل اطمینان خودمدیریتی توسط ارائه‌دهندگان مختلف ابر', - 2: 'شعار و سفارشی‌سازی برند وب‌اپلیکیشن', - 3: 'پشتیبانی اولویت ایمیل و چت', + 1: 'فضای کاری واحد', + 3: 'پشتیبانی از ایمیل و چت اولویت دار', + 2: 'لوگوی وب اپلیکیشن و سفارشی سازی برندینگ', + 0: 'قابلیت اطمینان خود مدیریت شده توسط ارائه دهندگان مختلف ابر', }, btnText: 'گرفتن نسخه پریمیوم در', description: 'برای سازمان‌ها و تیم‌های میان‌رده', @@ -173,8 +165,6 @@ const translation = { fullSolution: 'طرح خود را ارتقاء دهید تا فضای بیشتری دریافت کنید.', }, apps: { - fullTipLine1: 'طرح خود را ارتقاء دهید تا', - fullTipLine2: 'اپلیکیشن‌های بیشتری بسازید.', fullTip2: 'محدودیت طرح به پایان رسید', contactUs: 'با ما تماس بگیرید', fullTip1: 'به‌روزرسانی کنید تا برنامه‌های بیشتری ایجاد کنید', diff --git a/web/i18n/fr-FR/billing.ts b/web/i18n/fr-FR/billing.ts index 879a067941..262df37954 100644 --- a/web/i18n/fr-FR/billing.ts +++ b/web/i18n/fr-FR/billing.ts @@ -23,18 +23,13 @@ const translation = { contractSales: 'Contactez les ventes', contractOwner: 'Contacter le chef d\'équipe', startForFree: 'Commencez gratuitement', - getStartedWith: 'Commencez avec', contactSales: 'Contacter les ventes', talkToSales: 'Parlez aux Ventes', modelProviders: 'Fournisseurs de Modèles', - teamMembers: 'Membres de l\'équipe', buildApps: 'Construire des Applications', vectorSpace: 'Espace Vectoriel', - vectorSpaceBillingTooltip: 'Chaque 1MB peut stocker environ 1,2 million de caractères de données vectorisées (estimé en utilisant les embeddings OpenAI, varie selon les modèles).', vectorSpaceTooltip: 'L\'espace vectoriel est le système de mémoire à long terme nécessaire pour que les LLMs comprennent vos données.', - documentsUploadQuota: 'Quota de téléchargement de documents', documentProcessingPriority: 'Priorité de Traitement de Document', - documentProcessingPriorityTip: 'Pour une priorité de traitement de documents plus élevée, veuillez mettre à niveau votre plan.', documentProcessingPriorityUpgrade: 'Traitez plus de données avec une précision plus élevée à des vitesses plus rapides.', priority: { 'standard': 'Standard', @@ -103,19 +98,16 @@ const translation = { sandbox: { name: 'Bac à sable', description: '200 essais gratuits de GPT', - includesTitle: 'Inclus :', for: 'Essai gratuit des fonctionnalités principales', }, professional: { name: 'Professionnel', description: 'Pour les individus et les petites équipes afin de débloquer plus de puissance à un prix abordable.', - includesTitle: 'Tout ce qui est dans le plan gratuit, plus :', for: 'Pour les développeurs indépendants / petites équipes', }, team: { name: 'Équipe', description: 'Collaborez sans limites et profitez d\'une performance de premier ordre.', - includesTitle: 'Tout ce qui est inclus dans le plan Professionnel, plus :', for: 'Pour les équipes de taille moyenne', }, enterprise: { @@ -123,14 +115,14 @@ const translation = { description: 'Obtenez toutes les capacités et le support pour les systèmes à grande échelle et critiques pour la mission.', includesTitle: 'Tout ce qui est inclus dans le plan Équipe, plus :', features: { - 5: 'SLA négociés par Dify Partners', - 1: 'Autorisation de Licence Commerciale', - 2: 'Fonctionnalités exclusives pour les entreprises', - 4: 'SSO', - 8: 'Support Technique Professionnel', - 3: 'Gestion de plusieurs espaces de travail et d\'entreprise', 6: 'Sécurité et contrôles avancés', - 7: 'Mises à jour et maintenance par Dify Officiellement', + 3: 'Espaces de travail multiples et gestion d’entreprise', + 4: 'SSO', + 1: 'Autorisation de licence commerciale', + 2: 'Fonctionnalités exclusives à l’entreprise', + 5: 'SLA négociés par les partenaires Dify', + 8: 'Assistance technique professionnelle', + 7: 'Mises à jour et maintenance par Dify officiellement', 0: 'Solutions de déploiement évolutives de niveau entreprise', }, for: 'Pour les équipes de grande taille', @@ -140,9 +132,9 @@ const translation = { }, community: { features: { - 2: 'Conforme à la licence open source de Dify', 1: 'Espace de travail unique', - 0: 'Toutes les fonctionnalités principales publiées dans le référentiel public', + 0: 'Toutes les fonctionnalités de base publiées dans le dépôt public', + 2: 'Conforme à la licence Open Source Dify', }, name: 'Communauté', btnText: 'Commencez avec la communauté', @@ -153,10 +145,10 @@ const translation = { }, premium: { features: { - 3: 'Support par e-mail et chat prioritaire', + 2: 'Personnalisation du logo et de l’image de marque WebApp', 1: 'Espace de travail unique', - 0: 'Fiabilité autogérée par divers fournisseurs de cloud', - 2: 'Personnalisation du logo et de la marque de l\'application Web', + 3: 'Assistance prioritaire par e-mail et chat', + 0: 'Fiabilité autogérée par différents fournisseurs de cloud', }, for: 'Pour les organisations et les équipes de taille moyenne', includesTitle: 'Tout de la communauté, en plus :', @@ -173,8 +165,6 @@ const translation = { fullSolution: 'Mettez à niveau votre plan pour obtenir plus d\'espace.', }, apps: { - fullTipLine1: 'Mettez à jour votre plan pour', - fullTipLine2: 'construire plus d\'applications.', fullTip2: 'Limite de plan atteinte', contactUs: 'Contactez-nous', fullTip1: 'Mettez à niveau pour créer plus d\'applications', diff --git a/web/i18n/hi-IN/billing.ts b/web/i18n/hi-IN/billing.ts index 1f8b29587c..25c4298628 100644 --- a/web/i18n/hi-IN/billing.ts +++ b/web/i18n/hi-IN/billing.ts @@ -24,22 +24,15 @@ const translation = { contractSales: 'बिक्री से संपर्क करें', contractOwner: 'टीम प्रबंधक से संपर्क करें', startForFree: 'मुफ्त में शुरू करें', - getStartedWith: 'इसके साथ शुरू करें ', contactSales: 'बिक्री से संपर्क करें', talkToSales: 'बिक्री से बात करें', modelProviders: 'मॉडल प्रदाता', - teamMembers: 'टीम के सदस्य', annotationQuota: 'एनोटेशन कोटा', buildApps: 'ऐप्स बनाएं', vectorSpace: 'वेक्टर स्पेस', - vectorSpaceBillingTooltip: - 'प्रत्येक 1MB लगभग 1.2 मिलियन वर्णों के वेक्टराइज्ड डेटा को संग्रहीत कर सकता है (OpenAI एम्बेडिंग का उपयोग करके अनुमानित, मॉडल में भिन्नता होती है)।', vectorSpaceTooltip: 'वेक्टर स्पेस वह दीर्घकालिक स्मृति प्रणाली है जिसकी आवश्यकता LLMs को आपके डेटा को समझने के लिए होती है।', - documentsUploadQuota: 'दस्तावेज़ अपलोड कोटा', documentProcessingPriority: 'दस्तावेज़ प्रसंस्करण प्राथमिकता', - documentProcessingPriorityTip: - 'उच्च दस्तावेज़ प्रसंस्करण प्राथमिकता के लिए, कृपया अपनी योजना अपग्रेड करें।', documentProcessingPriorityUpgrade: 'तेजी से गति पर उच्च सटीकता के साथ अधिक डेटा संसाधित करें।', priority: { @@ -113,21 +106,18 @@ const translation = { sandbox: { name: 'सैंडबॉक्स', description: '200 बार GPT मुफ्त ट्रायल', - includesTitle: 'शामिल हैं:', for: 'कोर क्षमताओं का मुफ्त परीक्षण', }, professional: { name: 'प्रोफेशनल', description: 'व्यक्तियों और छोटे टीमों के लिए अधिक शक्ति सस्ती दर पर खोलें।', - includesTitle: 'मुफ्त योजना में सब कुछ, साथ में:', for: 'स्वतंत्र डेवलपर्स/छोटे टीमों के लिए', }, team: { name: 'टीम', description: 'बिना सीमा के सहयोग करें और शीर्ष स्तरीय प्रदर्शन का आनंद लें।', - includesTitle: 'प्रोफेशनल योजना में सब कुछ, साथ में:', for: 'मध्यम आकार की टीमों के लिए', }, enterprise: { @@ -136,15 +126,15 @@ const translation = { 'बड़े पैमाने पर मिशन-क्रिटिकल सिस्टम के लिए पूर्ण क्षमताएं और समर्थन प्राप्त करें।', includesTitle: 'टीम योजना में सब कुछ, साथ में:', features: { - 0: 'उद्योग स्तर के बड़े पैमाने पर वितरण समाधान', - 3: 'अनेक कार्यक्षेत्र और उद्यम प्रबंधक', - 8: 'प्रोफेशनल तकनीकी समर्थन', - 6: 'उन्नत सुरक्षा और नियंत्रण', - 2: 'विशेष उद्यम सुविधाएँ', 1: 'Commercial License Authorization', 4: 'SSO', + 6: 'उन्नत सुरक्षा और नियंत्रण', + 2: 'विशेष उद्यम सुविधाएँ', + 3: 'अनेक कार्यक्षेत्र और उद्यम प्रबंधक', 5: 'डिफाई पार्टनर्स द्वारा बातचीत किए गए एसएलए', + 8: 'प्रोफेशनल तकनीकी समर्थन', 7: 'डीफाई द्वारा आधिकारिक रूप से अपडेट और रखरखाव', + 0: 'उद्योग स्तर के बड़े पैमाने पर वितरण समाधान', }, price: 'कस्टम', btnText: 'बिक्री से संपर्क करें', @@ -153,9 +143,9 @@ const translation = { }, community: { features: { + 1: 'एकल कार्यक्षेत्र', 2: 'डिफी ओपन सोर्स लाइसेंस के अनुपालन में', 0: 'सभी मुख्य सुविधाएं सार्वजनिक संग्रह के तहत जारी की गई हैं।', - 1: 'एकल कार्यक्षेत्र', }, description: 'व्यक्तिगत उपयोगकर्ताओं, छोटे टीमों, या गैर-व्यावसायिक परियोजनाओं के लिए', for: 'व्यक्तिगत उपयोगकर्ताओं, छोटे टीमों, या गैर-व्यावसायिक परियोजनाओं के लिए', @@ -166,9 +156,9 @@ const translation = { }, premium: { features: { + 1: 'एकल कार्यक्षेत्र', 2: 'वेब ऐप लोगो और ब्रांडिंग कस्टमाइजेशन', 3: 'प्राथमिकता ईमेल और चैट समर्थन', - 1: 'एकल कार्यक्षेत्र', 0: 'विभिन्न क्लाउड प्रदाताओं द्वारा आत्म-प्रबंधित विश्वसनीयता', }, priceTip: 'क्लाउड मार्केटप्लेस के आधार पर', @@ -186,8 +176,6 @@ const translation = { fullSolution: 'अधिक स्थान प्राप्त करने के लिए अपनी योजना अपग्रेड करें।', }, apps: { - fullTipLine1: 'अधिक ऐप्स बनाने के लिए', - fullTipLine2: 'अपनी योजना अपग्रेड करें।', fullTip1: 'अधिक ऐप्स बनाने के लिए अपग्रेड करें', fullTip2: 'योजना की सीमा पहुँच गई', contactUs: 'हमसे संपर्क करें', diff --git a/web/i18n/it-IT/billing.ts b/web/i18n/it-IT/billing.ts index 69adc34569..43d285f652 100644 --- a/web/i18n/it-IT/billing.ts +++ b/web/i18n/it-IT/billing.ts @@ -24,22 +24,15 @@ const translation = { contractSales: 'Contatta vendite', contractOwner: 'Contatta il responsabile del team', startForFree: 'Inizia gratis', - getStartedWith: 'Inizia con ', contactSales: 'Contatta le vendite', talkToSales: 'Parla con le vendite', modelProviders: 'Fornitori di Modelli', - teamMembers: 'Membri del Team', annotationQuota: 'Quota di Annotazione', buildApps: 'Crea App', vectorSpace: 'Spazio Vettoriale', - vectorSpaceBillingTooltip: - 'Ogni 1MB può memorizzare circa 1,2 milioni di caratteri di dati vettoriali (stimato utilizzando OpenAI Embeddings, varia tra i modelli).', vectorSpaceTooltip: 'Lo Spazio Vettoriale è il sistema di memoria a lungo termine necessario per permettere agli LLM di comprendere i tuoi dati.', - documentsUploadQuota: 'Quota di Caricamento Documenti', documentProcessingPriority: 'Priorità di Elaborazione Documenti', - documentProcessingPriorityTip: - 'Per una maggiore priorità di elaborazione dei documenti, aggiorna il tuo piano.', documentProcessingPriorityUpgrade: 'Elabora più dati con maggiore precisione a velocità più elevate.', priority: { @@ -113,21 +106,18 @@ const translation = { sandbox: { name: 'Sandbox', description: '200 prove gratuite di GPT', - includesTitle: 'Include:', for: 'Prova gratuita delle capacità principali', }, professional: { name: 'Professional', description: 'Per individui e piccoli team per sbloccare più potenza a prezzi accessibili.', - includesTitle: 'Tutto nel piano gratuito, più:', for: 'Per sviluppatori indipendenti / piccoli team', }, team: { name: 'Team', description: 'Collabora senza limiti e goditi prestazioni di alto livello.', - includesTitle: 'Tutto nel piano Professional, più:', for: 'Per team di medie dimensioni', }, enterprise: { @@ -136,15 +126,15 @@ const translation = { 'Ottieni tutte le capacità e il supporto per sistemi mission-critical su larga scala.', includesTitle: 'Tutto nel piano Team, più:', features: { - 6: 'Sicurezza e Controlli Avanzati', - 2: 'Funzionalità esclusive per le imprese', + 3: 'Spazi di lavoro multipli e gestione aziendale', + 2: 'Funzionalità esclusive per le aziende', + 1: 'Autorizzazione Licenza Commerciale', + 5: 'SLA negoziati dai partner Dify', 4: 'SSO', - 8: 'Supporto Tecnico Professionale', - 5: 'SLA negoziati da Dify Partners', - 0: 'Soluzioni di distribuzione scalabili di livello enterprise', - 7: 'Aggiornamenti e manutenzione di Dify ufficialmente', - 1: 'Autorizzazione alla Licenza Commerciale', - 3: 'Gestione di più spazi di lavoro e imprese', + 6: 'Sicurezza e controlli avanzati', + 8: 'Supporto tecnico professionale', + 7: 'Aggiornamenti e manutenzione da parte di Dify ufficialmente', + 0: 'Soluzioni di distribuzione scalabili di livello aziendale', }, price: 'Personalizzato', for: 'Per team di grandi dimensioni', @@ -153,9 +143,9 @@ const translation = { }, community: { features: { - 1: 'Spazio di Lavoro Unico', - 2: 'Rispetta la Licenza Open Source di Dify', - 0: 'Tutte le funzionalità principali rilasciate sotto il repository pubblico', + 1: 'Area di lavoro singola', + 2: 'Conforme alla licenza Open Source Dify', + 0: 'Tutte le funzionalità principali rilasciate nel repository pubblico', }, name: 'Comunità', btnText: 'Inizia con la comunità', @@ -166,10 +156,10 @@ const translation = { }, premium: { features: { - 0: 'Affidabilità autogestita da vari fornitori di cloud', - 3: 'Supporto prioritario via Email e Chat', - 2: 'Personalizzazione del logo e del marchio dell\'app web', - 1: 'Spazio di Lavoro Unico', + 3: 'Supporto prioritario via e-mail e chat', + 1: 'Area di lavoro singola', + 2: 'Personalizzazione del logo e del marchio WebApp', + 0: 'Affidabilità autogestita da vari fornitori di servizi cloud', }, name: 'Premium', priceTip: 'Basato su Cloud Marketplace', @@ -186,8 +176,6 @@ const translation = { fullSolution: 'Aggiorna il tuo piano per ottenere più spazio.', }, apps: { - fullTipLine1: 'Aggiorna il tuo piano per', - fullTipLine2: 'creare più app.', fullTip1des: 'Hai raggiunto il limite di costruzione delle app su questo piano.', fullTip2des: 'Si consiglia di disinstallare le applicazioni inattive per liberare spazio, o contattarci.', contactUs: 'Contattaci', diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index 2c88b54b8a..99da1bc6c6 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -995,6 +995,7 @@ const translation = { noLastRunFound: '以前の実行が見つかりませんでした。', copyLastRunError: '最後の実行の入力をコピーできませんでした', noMatchingInputsFound: '前回の実行から一致する入力が見つかりませんでした。', + lastRunInputsCopied: '前回の実行から{{count}}個の入力をコピーしました', }, } diff --git a/web/i18n/pl-PL/billing.ts b/web/i18n/pl-PL/billing.ts index 00284109e8..09e213df8d 100644 --- a/web/i18n/pl-PL/billing.ts +++ b/web/i18n/pl-PL/billing.ts @@ -24,21 +24,14 @@ const translation = { contractSales: 'Skontaktuj się z działem sprzedaży', contractOwner: 'Skontaktuj się z zarządcą zespołu', startForFree: 'Zacznij za darmo', - getStartedWith: 'Rozpocznij z ', contactSales: 'Kontakt z działem sprzedaży', talkToSales: 'Porozmawiaj z działem sprzedaży', modelProviders: 'Dostawcy modeli', - teamMembers: 'Członkowie zespołu', buildApps: 'Twórz aplikacje', vectorSpace: 'Przestrzeń wektorowa', - vectorSpaceBillingTooltip: - 'Każdy 1MB może przechowywać około 1,2 miliona znaków z wektoryzowanych danych (szacowane na podstawie OpenAI Embeddings, różni się w zależności od modelu).', vectorSpaceTooltip: 'Przestrzeń wektorowa jest systemem pamięci długoterminowej wymaganym dla LLM, aby zrozumieć Twoje dane.', - documentsUploadQuota: 'Limit przesyłanych dokumentów', documentProcessingPriority: 'Priorytet przetwarzania dokumentów', - documentProcessingPriorityTip: - 'Dla wyższego priorytetu przetwarzania dokumentów, ulepsz swój plan.', documentProcessingPriorityUpgrade: 'Przetwarzaj więcej danych z większą dokładnością i w szybszym tempie.', priority: { @@ -112,21 +105,18 @@ const translation = { sandbox: { name: 'Sandbox', description: '200 razy darmowa próba GPT', - includesTitle: 'Zawiera:', for: 'Darmowy okres próbny podstawowych funkcji', }, professional: { name: 'Profesjonalny', description: 'Dla osób fizycznych i małych zespołów, aby odblokować więcej mocy w przystępnej cenie.', - includesTitle: 'Wszystko w darmowym planie, plus:', for: 'Dla niezależnych deweloperów/małych zespołów', }, team: { name: 'Zespół', description: 'Współpracuj bez ograniczeń i ciesz się najwyższą wydajnością.', - includesTitle: 'Wszystko w planie Profesjonalnym, plus:', for: 'Dla średniej wielkości zespołów', }, enterprise: { @@ -135,15 +125,15 @@ const translation = { 'Uzyskaj pełne możliwości i wsparcie dla systemów o kluczowym znaczeniu dla misji.', includesTitle: 'Wszystko w planie Zespołowym, plus:', features: { - 3: 'Wiele przestrzeni roboczych i zarządzanie przedsiębiorstwem', - 5: 'Wynegocjowane SLA przez Dify Partners', - 0: 'Rozwiązania do wdrożeń na dużą skalę klasy przedsiębiorstw', - 8: 'Profesjonalne wsparcie techniczne', - 2: 'Ekskluzywne funkcje przedsiębiorstwa', - 6: 'Zaawansowane zabezpieczenia i kontrola', - 7: 'Aktualizacje i konserwacja przez Dify Oficjalnie', - 4: 'SSO', + 2: 'Wyjątkowe funkcje dla przedsiębiorstw', + 7: 'Aktualizacje i konserwacja przez Dify oficjalnie', + 4: 'Usługi rejestracji jednokrotnej', 1: 'Autoryzacja licencji komercyjnej', + 0: 'Skalowalne rozwiązania wdrożeniowe klasy korporacyjnej', + 5: 'Umowy SLA wynegocjowane przez Dify Partners', + 8: 'Profesjonalne wsparcie techniczne', + 3: 'Wiele przestrzeni roboczych i zarządzanie przedsiębiorstwem', + 6: 'Zaawansowane zabezpieczenia i kontrola', }, priceTip: 'Tylko roczne fakturowanie', btnText: 'Skontaktuj się z działem sprzedaży', @@ -152,9 +142,9 @@ const translation = { }, community: { features: { - 0: 'Wszystkie funkcje podstawowe wydane w publicznym repozytorium', - 1: 'Jedno Miejsce Pracy', - 2: 'Zgodne z licencją Dify Open Source', + 1: 'Pojedyncza przestrzeń robocza', + 2: 'Zgodny z licencją Dify Open Source', + 0: 'Wszystkie podstawowe funkcje udostępnione w repozytorium publicznym', }, includesTitle: 'Darmowe funkcje:', name: 'Społeczność', @@ -165,10 +155,10 @@ const translation = { }, premium: { features: { - 0: 'Samozarządzana niezawodność różnych dostawców chmury', - 1: 'Jedno miejsce pracy', - 3: 'Priorytetowe wsparcie przez e-mail i czat', - 2: 'Logo aplikacji internetowej i dostosowanie marki', + 1: 'Pojedyncza przestrzeń robocza', + 2: 'Personalizacja logo i brandingu aplikacji internetowej', + 3: 'Priorytetowa pomoc techniczna przez e-mail i czat', + 0: 'Niezawodność samodzielnego zarządzania przez różnych dostawców usług w chmurze', }, description: 'Dla średnich organizacji i zespołów', for: 'Dla średnich organizacji i zespołów', @@ -185,8 +175,6 @@ const translation = { fullSolution: 'Ulepsz swój plan, aby uzyskać więcej miejsca.', }, apps: { - fullTipLine1: 'Ulepsz swój plan, aby', - fullTipLine2: 'tworzyć więcej aplikacji.', fullTip1des: 'Osiągnąłeś limit tworzenia aplikacji w tym planie.', fullTip1: 'Zaktualizuj, aby stworzyć więcej aplikacji', fullTip2: 'Osiągnięto limit planu', diff --git a/web/i18n/pt-BR/billing.ts b/web/i18n/pt-BR/billing.ts index f26008c718..3ef93d9f91 100644 --- a/web/i18n/pt-BR/billing.ts +++ b/web/i18n/pt-BR/billing.ts @@ -22,17 +22,13 @@ const translation = { currentPlan: 'Plano Atual', contractOwner: 'Entre em contato com o gerente da equipe', startForFree: 'Comece de graça', - getStartedWith: 'Comece com', contactSales: 'Fale com a equipe de Vendas', talkToSales: 'Fale com a equipe de Vendas', modelProviders: 'Fornecedores de Modelos', - teamMembers: 'Membros da Equipe', buildApps: 'Construir Aplicações', vectorSpace: 'Espaço Vetorial', - vectorSpaceBillingTooltip: 'Cada 1MB pode armazenar cerca de 1,2 milhão de caracteres de dados vetorizados (estimado usando OpenAI Embeddings, varia entre os modelos).', vectorSpaceTooltip: 'O Espaço Vetorial é o sistema de memória de longo prazo necessário para que LLMs compreendam seus dados.', documentProcessingPriority: 'Prioridade no Processamento de Documentos', - documentProcessingPriorityTip: 'Para maior prioridade no processamento de documentos, faça o upgrade do seu plano.', documentProcessingPriorityUpgrade: 'Processe mais dados com maior precisão e velocidade.', priority: { 'standard': 'Padrão', @@ -53,7 +49,6 @@ const translation = { dedicatedAPISupport: 'Suporte dedicado à API', customIntegration: 'Integração e suporte personalizados', ragAPIRequest: 'Solicitações API RAG', - agentModel: 'Modelo de Agente', workflow: 'Fluxo de trabalho', llmLoadingBalancing: 'Balanceamento de carga LLM', bulkUpload: 'Upload em massa de documentos', @@ -75,7 +70,6 @@ const translation = { ragAPIRequestTooltip: 'Refere-se ao número de chamadas de API que invocam apenas as capacidades de processamento da base de conhecimento do Dify.', receiptInfo: 'Somente proprietários e administradores de equipe podem se inscrever e visualizar informações de cobrança', customTools: 'Ferramentas personalizadas', - documentsUploadQuota: 'Cota de upload de documentos', annotationQuota: 'Cota de anotação', contractSales: 'Entre em contato com a equipe de vendas', unavailable: 'Indisponível', @@ -104,19 +98,16 @@ const translation = { sandbox: { name: 'Sandbox', description: '200 vezes GPT de teste gratuito', - includesTitle: 'Inclui:', for: 'Teste gratuito das capacidades principais', }, professional: { name: 'Profissional', description: 'Para indivíduos e pequenas equipes desbloquearem mais poder de forma acessível.', - includesTitle: 'Tudo no plano gratuito, além de:', for: 'Para Desenvolvedores Independentes/Pequenas Equipes', }, team: { name: 'Equipe', description: 'Colabore sem limites e aproveite o desempenho de primeira linha.', - includesTitle: 'Tudo no plano Profissional, além de:', for: 'Para Equipes de Médio Porte', }, enterprise: { @@ -124,15 +115,15 @@ const translation = { description: 'Obtenha capacidades completas e suporte para sistemas críticos em larga escala.', includesTitle: 'Tudo no plano Equipe, além de:', features: { - 6: 'Segurança e Controles Avançados', - 7: 'Atualizações e Manutenção por Dify Oficialmente', - 5: 'Acordos de Nível de Serviço negociados pelos Parceiros Dify', - 1: 'Autorização de Licença Comercial', - 8: 'Suporte Técnico Profissional', + 3: 'Vários espaços de trabalho e gerenciamento corporativo', + 2: 'Recursos exclusivos da empresa', + 6: 'Segurança e controles avançados', 4: 'SSO', - 2: 'Recursos Exclusivos da Empresa', - 3: 'Múltiplos Espaços de Trabalho e Gestão Empresarial', - 0: 'Soluções de Implantação Escaláveis de Nível Empresarial', + 8: 'Suporte Técnico Profissional', + 0: 'Soluções de implantação escaláveis de nível empresarial', + 7: 'Atualizações e manutenção por Dify oficialmente', + 1: 'Autorização de Licença Comercial', + 5: 'SLAs negociados pela Dify Partners', }, btnText: 'Contate Vendas', priceTip: 'Faturamento Anual Apenas', @@ -141,9 +132,9 @@ const translation = { }, community: { features: { - 1: 'Espaço de Trabalho Único', - 0: 'Todos os recursos principais lançados sob o repositório público', - 2: 'Cumpre a Licença de Código Aberto Dify', + 0: 'Todos os principais recursos lançados no repositório público', + 2: 'Está em conformidade com a licença de código aberto Dify', + 1: 'Espaço de trabalho individual', }, name: 'Comunidade', description: 'Para Usuários Individuais, Pequenas Equipes ou Projetos Não Comerciais', @@ -154,10 +145,10 @@ const translation = { }, premium: { features: { - 1: 'Espaço de Trabalho Único', - 3: 'Suporte prioritário por e-mail e chat', - 2: 'Customização de Logo e Branding do WebApp', + 2: 'Personalização do logotipo e da marca do WebApp', + 1: 'Espaço de trabalho individual', 0: 'Confiabilidade autogerenciada por vários provedores de nuvem', + 3: 'Suporte prioritário por e-mail e bate-papo', }, includesTitle: 'Tudo da Comunidade, além de:', for: 'Para organizações e equipes de médio porte', @@ -174,8 +165,6 @@ const translation = { fullSolution: 'Faça o upgrade do seu plano para obter mais espaço.', }, apps: { - fullTipLine1: 'Faça o upgrade do seu plano para', - fullTipLine2: 'construir mais aplicativos.', fullTip1: 'Atualize para criar mais aplicativos', fullTip2: 'Limite do plano alcançado', fullTip1des: 'Você atingiu o limite de criar aplicativos neste plano.', diff --git a/web/i18n/ro-RO/billing.ts b/web/i18n/ro-RO/billing.ts index 682641372d..df35ec26fb 100644 --- a/web/i18n/ro-RO/billing.ts +++ b/web/i18n/ro-RO/billing.ts @@ -23,18 +23,13 @@ const translation = { contractSales: 'Contactați vânzările', contractOwner: 'Contactați managerul echipei', startForFree: 'Începe gratuit', - getStartedWith: 'Începe cu ', contactSales: 'Contactați vânzările', talkToSales: 'Vorbiți cu vânzările', modelProviders: 'Furnizori de modele', - teamMembers: 'Membri ai echipei', buildApps: 'Construiește aplicații', vectorSpace: 'Spațiu vectorial', - vectorSpaceBillingTooltip: 'Fiecare 1MB poate stoca aproximativ 1,2 milioane de caractere de date vectorizate (estimat folosind OpenAI Embeddings, variază în funcție de modele).', vectorSpaceTooltip: 'Spațiul vectorial este sistemul de memorie pe termen lung necesar pentru ca LLM-urile să înțeleagă datele dvs.', - documentsUploadQuota: 'Cotă de încărcare a documentelor', documentProcessingPriority: 'Prioritatea procesării documentelor', - documentProcessingPriorityTip: 'Pentru o prioritate mai mare a procesării documentelor, vă rugăm să actualizați planul.', documentProcessingPriorityUpgrade: 'Procesați mai multe date cu o acuratețe mai mare și la viteze mai rapide.', priority: { 'standard': 'Standard', @@ -103,19 +98,16 @@ const translation = { sandbox: { name: 'Sandbox', description: '200 de încercări gratuite GPT', - includesTitle: 'Include:', for: 'Proba gratuită a capacităților de bază', }, professional: { name: 'Professional', description: 'Pentru persoane fizice și echipe mici pentru a debloca mai multă putere la un preț accesibil.', - includesTitle: 'Tot ce este în planul gratuit, plus:', for: 'Pentru dezvoltatori independenți / echipe mici', }, team: { name: 'Echipă', description: 'Colaborați fără limite și bucurați-vă de performanțe de top.', - includesTitle: 'Tot ce este în planul Professional, plus:', for: 'Pentru echipe de dimensiuni medii', }, enterprise: { @@ -123,15 +115,15 @@ const translation = { description: 'Obțineți capacități și asistență complete pentru sisteme critice la scară largă.', includesTitle: 'Tot ce este în planul Echipă, plus:', features: { - 3: 'Multiple Spații de lucru și Management Enterprise', - 6: 'Securitate avansată și control', + 6: 'Securitate și controale avansate', + 1: 'Autorizare licență comercială', 2: 'Funcții exclusive pentru întreprinderi', + 0: 'Soluții de implementare scalabile la nivel de întreprindere', + 5: 'SLA-uri negociate de partenerii Dify', + 3: 'Mai multe spații de lucru și managementul întreprinderii', + 7: 'Actualizări și întreținere de către Dify oficial', 8: 'Asistență tehnică profesională', 4: 'SSO', - 7: 'Actualizări și întreținere de către Dify Oficial', - 1: 'Autorizare pentru licență comercială', - 5: 'SLA-uri negociate de partenerii Dify', - 0: 'Soluții de desfășurare scalabile de nivel enterprise', }, for: 'Pentru echipe de mari dimensiuni', price: 'Personalizat', @@ -140,9 +132,9 @@ const translation = { }, community: { features: { - 2: 'Se conformează Licenței Open Source Dify', + 0: 'Toate caracteristicile de bază lansate în depozitul public', + 2: 'Respectă licența Dify Open Source', 1: 'Spațiu de lucru unic', - 0: 'Toate funcțiile de bază lansate sub depozitul public', }, description: 'Pentru utilizatori individuali, echipe mici sau proiecte necomerciale', btnText: 'Începe cu Comunitatea', @@ -153,10 +145,10 @@ const translation = { }, premium: { features: { + 3: 'Asistență prioritară prin e-mail și chat', 1: 'Spațiu de lucru unic', - 0: 'Fiabilitate autogestionată de diferiți furnizori de cloud', - 2: 'Personalizarea logo-ului și branding-ului aplicației web', - 3: 'Suport prioritar prin email și chat', + 0: 'Fiabilitate autogestionată de diverși furnizori de cloud', + 2: 'Personalizarea logo-ului și brandingului WebApp', }, btnText: 'Obține Premium în', description: 'Pentru organizații și echipe de dimensiuni medii', @@ -173,8 +165,6 @@ const translation = { fullSolution: 'Actualizați-vă planul pentru a obține mai mult spațiu.', }, apps: { - fullTipLine1: 'Actualizați-vă planul pentru a', - fullTipLine2: 'construi mai multe aplicații.', fullTip2des: 'Se recomandă curățarea aplicațiilor inactive pentru a elibera resurse, sau contactați-ne.', fullTip2: 'Limita planului a fost atinsă', fullTip1des: 'Ați atins limita de aplicații construite pe acest plan', diff --git a/web/i18n/ru-RU/billing.ts b/web/i18n/ru-RU/billing.ts index 7a0560334c..7af47ee00b 100644 --- a/web/i18n/ru-RU/billing.ts +++ b/web/i18n/ru-RU/billing.ts @@ -23,19 +23,14 @@ const translation = { contractSales: 'Связаться с отделом продаж', contractOwner: 'Связаться с руководителем команды', startForFree: 'Начать бесплатно', - getStartedWith: 'Начать с ', contactSales: 'Связаться с отделом продаж', talkToSales: 'Поговорить с отделом продаж', modelProviders: 'Поставщики моделей', - teamMembers: 'Участники команды', annotationQuota: 'Квота аннотаций', buildApps: 'Создать приложения', vectorSpace: 'Векторное пространство', - vectorSpaceBillingTooltip: 'Каждый 1 МБ может хранить около 1,2 миллиона символов векторизованных данных (оценка с использованием Embeddings OpenAI, варьируется в зависимости от модели).', vectorSpaceTooltip: 'Векторное пространство - это система долговременной памяти, необходимая LLM для понимания ваших данных.', - documentsUploadQuota: 'Квота загрузки документов', documentProcessingPriority: 'Приоритет обработки документов', - documentProcessingPriorityTip: 'Для более высокого приоритета обработки документов, пожалуйста, обновите свой тарифный план.', documentProcessingPriorityUpgrade: 'Обрабатывайте больше данных с большей точностью и на более высоких скоростях.', priority: { 'standard': 'Стандартный', @@ -103,19 +98,16 @@ const translation = { sandbox: { name: 'Песочница', description: '200 бесплатных пробных использований GPT', - includesTitle: 'Включает:', for: 'Бесплатная пробная версия основных возможностей', }, professional: { name: 'Профессиональный', description: 'Для частных лиц и небольших команд, чтобы разблокировать больше возможностей по доступной цене.', - includesTitle: 'Все в бесплатном плане, плюс:', for: 'Для независимых разработчиков/малых команд', }, team: { name: 'Команда', description: 'Сотрудничайте без ограничений и наслаждайтесь высочайшей производительностью.', - includesTitle: 'Все в профессиональном плане, плюс:', for: 'Для команд среднего размера', }, enterprise: { @@ -123,15 +115,15 @@ const translation = { description: 'Получите полный набор возможностей и поддержку для крупномасштабных критически важных систем.', includesTitle: 'Все в командном плане, плюс:', features: { - 7: 'Обновления и обслуживание от Dify официально', 4: 'ССО', + 5: 'Согласованные SLA от Dify Partners', 8: 'Профессиональная техническая поддержка', - 6: 'Современная безопасность и контроль', - 2: 'Эксклюзивные функции для предприятий', - 1: 'Коммерческая лицензия', - 3: 'Множественные рабочие области и управление предприятием', - 0: 'Решения для масштабируемого развертывания корпоративного уровня', - 5: 'Согласованные Соглашения об Уровне Услуг от Dify Partners', + 2: 'Эксклюзивные корпоративные функции', + 6: 'Расширенная безопасность и контроль', + 7: 'Обновления и обслуживание от Dify официально', + 3: 'Несколько рабочих пространств и управление предприятием', + 0: 'Масштабируемые решения для развертывания корпоративного уровня', + 1: 'Разрешение на коммерческую лицензию', }, price: 'Пользовательский', priceTip: 'Только годовая подписка', @@ -140,9 +132,9 @@ const translation = { }, community: { features: { - 0: 'Все основные функции выпущены в публичном репозитории', 1: 'Единое рабочее пространство', - 2: 'Соблюдает Лицензию на открытое программное обеспечение Dify', + 2: 'Соответствует лицензии Dify с открытым исходным кодом', + 0: 'Все основные функции выпущены в общедоступном репозитории', }, name: 'Сообщество', btnText: 'Начните с сообщества', @@ -153,10 +145,10 @@ const translation = { }, premium: { features: { - 3: 'Приоритетная поддержка по электронной почте и чату', + 2: 'Настройка логотипа и брендинга WebApp', 1: 'Единое рабочее пространство', - 2: 'Настройка логотипа и брендинга веб-приложения', - 0: 'Самостоятельное управление надежностью различными облачными провайдерами', + 3: 'Приоритетная поддержка по электронной почте и в чате', + 0: 'Самостоятельное управление надежностью от различных поставщиков облачных услуг', }, description: 'Для средних организаций и команд', includesTitle: 'Всё из Сообщества, плюс:', @@ -173,8 +165,6 @@ const translation = { fullSolution: 'Обновите свой тарифный план, чтобы получить больше места.', }, apps: { - fullTipLine1: 'Обновите свой тарифный план, чтобы', - fullTipLine2: 'создавать больше приложений.', fullTip2des: 'Рекомендуется удалить неактивные приложения, чтобы освободить место, или свяжитесь с нами.', fullTip2: 'Достигнут лимит плана', contactUs: 'Свяжитесь с нами', diff --git a/web/i18n/sl-SI/billing.ts b/web/i18n/sl-SI/billing.ts index 9b7987293f..ffaa1b56e2 100644 --- a/web/i18n/sl-SI/billing.ts +++ b/web/i18n/sl-SI/billing.ts @@ -23,19 +23,14 @@ const translation = { contractSales: 'Kontaktirajte prodajo', contractOwner: 'Kontaktirajte upravitelja ekipe', startForFree: 'Začnite brezplačno', - getStartedWith: 'Začnite z ', contactSales: 'Kontaktirajte prodajo', talkToSales: 'Pogovorite se s prodajo', modelProviders: 'Ponudniki modelov', - teamMembers: 'Člani ekipe', annotationQuota: 'Kvote za označevanje', buildApps: 'Gradite aplikacije', vectorSpace: 'Prostor za vektorje', - vectorSpaceBillingTooltip: 'Vsak 1 MB lahko shrani približno 1,2 milijona znakov vektoriziranih podatkov (ocenjeno z uporabo OpenAI Embeddings, odvisno od modelov).', vectorSpaceTooltip: 'Prostor za vektorje je dolgoročni pomnilniški sistem, potreben za to, da LLM-ji razumejo vaše podatke.', - documentsUploadQuota: 'Kvote za nalaganje dokumentov', documentProcessingPriority: 'Prioriteta obdelave dokumentov', - documentProcessingPriorityTip: 'Za višjo prioriteto obdelave dokumentov nadgradite svoj načrt.', documentProcessingPriorityUpgrade: 'Obdelujte več podatkov z večjo natančnostjo in hitrostjo.', priority: { 'standard': 'Standard', @@ -103,19 +98,16 @@ const translation = { sandbox: { name: 'Peskovnik', description: '200 brezplačnih poskusov GPT', - includesTitle: 'Vključuje:', for: 'Brezplačno preizkušanje osnovnih zmogljivosti', }, professional: { name: 'Profesionalni', description: 'Za posameznike in male ekipe, da odklenete več zmogljivosti po ugodni ceni.', - includesTitle: 'Vse v brezplačnem načrtu, plus:', for: 'Za neodvisne razvijalce/male ekipe', }, team: { name: 'Ekipa', description: 'Sodelujte brez omejitev in uživajte v vrhunski zmogljivosti.', - includesTitle: 'Vse v profesionalnem načrtu, plus:', for: 'Za srednje velike ekipe', }, enterprise: { @@ -123,15 +115,15 @@ const translation = { description: 'Pridobite vse zmogljivosti in podporo za velike sisteme kritične za misijo.', includesTitle: 'Vse v načrtu Ekipa, plus:', features: { - 5: 'Pogajali smo se o SLAs s partnerji Dify', - 4: 'SSO', - 0: 'Rešitve za razširljivo uvedbo na ravni podjetij', - 1: 'Avtorizacija za komercialno licenco', + 0: 'Prilagodljive rešitve za uvajanje na ravni podjetij', 2: 'Ekskluzivne funkcije za podjetja', - 7: 'Posodobitve in vzdrževanje s strani Dify uradno', - 3: 'Več delovnih prostorov in upravljanje podjetij', + 7: 'Posodobitve in vzdrževanje s strani Dify Official', + 8: 'Strokovna tehnična podpora', + 1: 'Dovoljenje za komercialno licenco', + 3: 'Več delovnih prostorov in upravljanje podjetja', + 5: 'Dogovorjene pogodbe o ravni storitev s strani Dify Partners', 6: 'Napredna varnost in nadzor', - 8: 'Profesionalna tehnična podpora', + 4: 'SSO', }, priceTip: 'Letno zaračunavanje samo', price: 'Po meri', @@ -140,9 +132,9 @@ const translation = { }, community: { features: { - 2: 'Upošteva Dify odprtokodno licenco', - 0: 'Vse ključne funkcije so bile objavljene v javnem repozitoriju', - 1: 'Enotno delovno okolje', + 1: 'En delovni prostor', + 0: 'Vse osnovne funkcije, izdane v javnem repozitoriju', + 2: 'Skladen z odprtokodno licenco Dify', }, includesTitle: 'Brezplačne funkcije:', price: 'Brezplačno', @@ -153,10 +145,10 @@ const translation = { }, premium: { features: { - 2: 'Prilagoditev logotipa in blagovne znamke spletne aplikacije', - 1: 'Enotno delovno okolje', - 0: 'Samoobvladovana zanesljivost različnih ponudnikov oblačnih storitev', - 3: 'Prednostna e-pošta in podpora za klepet', + 1: 'En delovni prostor', + 3: 'Prednostna podpora po e-pošti in klepetu', + 2: 'Prilagajanje logotipa in blagovne znamke WebApp', + 0: 'Samostojna zanesljivost različnih ponudnikov storitev v oblaku', }, name: 'Premium', priceTip: 'Na podlagi oblaka Marketplace', @@ -173,8 +165,6 @@ const translation = { fullSolution: 'Nadgradite svoj načrt za več prostora.', }, apps: { - fullTipLine1: 'Nadgradite svoj načrt, da', - fullTipLine2: 'gradite več aplikacij.', fullTip1des: 'Dosegli ste omejitev za izdelavo aplikacij v tem načrtu.', fullTip1: 'Nadgradite za ustvarjanje več aplikacij', fullTip2: 'Dosežena meja načrta', diff --git a/web/i18n/th-TH/billing.ts b/web/i18n/th-TH/billing.ts index c58d61c112..afbe9318c4 100644 --- a/web/i18n/th-TH/billing.ts +++ b/web/i18n/th-TH/billing.ts @@ -23,19 +23,14 @@ const translation = { contractSales: 'ติดต่อฝ่ายขาย', contractOwner: 'ติดต่อผู้จัดการทีม', startForFree: 'เริ่มฟรี', - getStartedWith: 'เริ่มต้นใช้งาน', contactSales: 'ติดต่อฝ่ายขาย', talkToSales: 'พูดคุยกับฝ่ายขาย', modelProviders: 'ผู้ให้บริการโมเดล', - teamMembers: 'สมาชิกในทีม', annotationQuota: 'โควต้าคําอธิบายประกอบ', buildApps: 'สร้างแอพ', vectorSpace: 'พื้นที่เวกเตอร์', - vectorSpaceBillingTooltip: 'แต่ละ 1MB สามารถจัดเก็บข้อมูลแบบเวกเตอร์ได้ประมาณ 1.2 ล้านอักขระ (โดยประมาณโดยใช้ OpenAI Embeddings แตกต่างกันไปตามรุ่น)', vectorSpaceTooltip: 'Vector Space เป็นระบบหน่วยความจําระยะยาวที่จําเป็นสําหรับ LLM ในการทําความเข้าใจข้อมูลของคุณ', - documentsUploadQuota: 'โควต้าการอัปโหลดเอกสาร', documentProcessingPriority: 'ลําดับความสําคัญในการประมวลผลเอกสาร', - documentProcessingPriorityTip: 'สําหรับลําดับความสําคัญในการประมวลผลเอกสารที่สูงขึ้น โปรดอัปเกรดแผนของคุณ', documentProcessingPriorityUpgrade: 'ประมวลผลข้อมูลได้มากขึ้นด้วยความแม่นยําที่สูงขึ้นด้วยความเร็วที่เร็วขึ้น', priority: { 'standard': 'มาตรฐาน', @@ -103,19 +98,16 @@ const translation = { sandbox: { name: 'กระบะทราย', description: 'ทดลองใช้ GPT ฟรี 200 ครั้ง', - includesTitle: 'มี:', for: 'ทดลองใช้ฟรีของความสามารถหลัก', }, professional: { name: 'มืออาชีพ', description: 'สําหรับบุคคลและทีมขนาดเล็กเพื่อปลดล็อกพลังงานมากขึ้นในราคาย่อมเยา', - includesTitle: 'ทุกอย่างในแผนฟรี รวมถึง:', for: 'สำหรับนักพัฒนาที่เป็นอิสระ/ทีมขนาดเล็ก', }, team: { name: 'ทีม', description: 'ทํางานร่วมกันอย่างไร้ขีดจํากัดและเพลิดเพลินไปกับประสิทธิภาพระดับสูงสุด', - includesTitle: 'ทุกอย่างในแผน Professional รวมถึง:', for: 'สำหรับทีมขนาดกลาง', }, enterprise: { @@ -123,15 +115,15 @@ const translation = { description: 'รับความสามารถและการสนับสนุนเต็มรูปแบบสําหรับระบบที่สําคัญต่อภารกิจขนาดใหญ่', includesTitle: 'ทุกอย่างในแผนทีม รวมถึง:', features: { - 8: 'การสนับสนุนทางเทคนิคระดับมืออาชีพ', - 2: 'คุณสมบัติพิเศษขององค์กร', - 3: 'หลายพื้นที่ทำงานและการบริหารจัดการองค์กร', 4: 'SSO', - 6: 'ความปลอดภัยและการควบคุมขั้นสูง', - 5: 'เจรจาข้อตกลงบริการ (SLA) โดย Dify Partners', - 7: 'การอัปเดตและการบำรุงรักษาโดย Dify อย่างเป็นทางการ', - 1: 'ใบอนุญาตการใช้เชิงพาณิชย์', - 0: 'โซลูชันการปรับใช้ที่มีขนาดใหญ่และมีคุณภาพระดับองค์กร', + 2: 'คุณสมบัติพิเศษสําหรับองค์กร', + 5: 'SLA ที่เจรจาโดย Dify Partners', + 1: 'การอนุญาตใบอนุญาตเชิงพาณิชย์', + 8: 'การสนับสนุนด้านเทคนิคอย่างมืออาชีพ', + 0: 'โซลูชันการปรับใช้ที่ปรับขนาดได้ระดับองค์กร', + 7: 'การอัปเดตและบํารุงรักษาโดย Dify อย่างเป็นทางการ', + 3: 'พื้นที่ทํางานหลายแห่งและการจัดการองค์กร', + 6: 'การรักษาความปลอดภัยและการควบคุมขั้นสูง', }, btnText: 'ติดต่อฝ่ายขาย', price: 'ที่กำหนดเอง', @@ -140,9 +132,9 @@ const translation = { }, community: { features: { - 2: 'ปฏิบัติตามใบอนุญาตโอเพ่นซอร์สของ Dify', - 0: 'ฟีเจอร์หลักทั้งหมดถูกปล่อยออกภายใต้ที่เก็บสาธารณะ', - 1: 'พื้นที่ทำงานเดียว', + 1: 'พื้นที่ทํางานเดียว', + 2: 'สอดคล้องกับใบอนุญาตโอเพ่นซอร์ส Dify', + 0: 'คุณสมบัติหลักทั้งหมดที่เผยแพร่ภายใต้ที่เก็บสาธารณะ', }, name: 'ชุมชน', price: 'ฟรี', @@ -153,10 +145,10 @@ const translation = { }, premium: { features: { - 3: 'การสนับสนุนทางอีเมลและแชทที่มีความสำคัญ', - 1: 'พื้นที่ทำงานเดียว', - 2: 'การปรับแต่งโลโก้และแบรนดิ้งของเว็บแอป', - 0: 'การจัดการความน่าเชื่อถือด้วยตนเองโดยผู้ให้บริการคลาวด์ต่าง ๆ', + 2: 'โลโก้ WebApp และการปรับแต่งแบรนด์', + 3: 'การสนับสนุนทางอีเมลและแชทลําดับความสําคัญ', + 1: 'พื้นที่ทํางานเดียว', + 0: 'ความน่าเชื่อถือที่จัดการด้วยตนเองโดยผู้ให้บริการคลาวด์ต่างๆ', }, priceTip: 'อิงตามตลาดคลาวด์', for: 'สำหรับองค์กรและทีมขนาดกลาง', @@ -173,8 +165,6 @@ const translation = { fullSolution: 'อัปเกรดแผนของคุณเพื่อเพิ่มพื้นที่', }, apps: { - fullTipLine1: 'อัปเกรดแผนของคุณเป็น', - fullTipLine2: 'สร้างแอปเพิ่มเติม', contactUs: 'ติดต่อเรา', fullTip2: 'ถึงขีดจำกัดของแผนแล้ว', fullTip1: 'อัปเกรดเพื่อสร้างแอปเพิ่มเติม', diff --git a/web/i18n/tr-TR/billing.ts b/web/i18n/tr-TR/billing.ts index fd51bae648..d85de6b5a2 100644 --- a/web/i18n/tr-TR/billing.ts +++ b/web/i18n/tr-TR/billing.ts @@ -23,19 +23,14 @@ const translation = { contractSales: 'Satışla iletişime geçin', contractOwner: 'Takım yöneticisine başvurun', startForFree: 'Ücretsiz Başla', - getStartedWith: 'ile başlayın', contactSales: 'Satışlarla İletişime Geçin', talkToSales: 'Satışlarla Konuşun', modelProviders: 'Model Sağlayıcılar', - teamMembers: 'Takım Üyeleri', annotationQuota: 'Ek Açıklama Kotası', buildApps: 'Uygulamalar Oluştur', vectorSpace: 'Vektör Alanı', - vectorSpaceBillingTooltip: 'Her 1MB yaklaşık 1.2 milyon karakter vektörize veri depolayabilir (OpenAI Embeddings ile tahmin edilmiştir, modellere göre farklılık gösterebilir).', vectorSpaceTooltip: 'Vektör Alanı, LLM\'lerin verilerinizi anlaması için gerekli uzun süreli hafıza sistemidir.', - documentsUploadQuota: 'Doküman Yükleme Kotası', documentProcessingPriority: 'Doküman İşleme Önceliği', - documentProcessingPriorityTip: 'Daha yüksek doküman işleme önceliği için planınızı yükseltin.', documentProcessingPriorityUpgrade: 'Daha fazla veriyi daha yüksek doğrulukla ve daha hızlı işleyin.', priority: { 'standard': 'Standart', @@ -103,19 +98,16 @@ const translation = { sandbox: { name: 'Sandbox', description: '200 kez GPT ücretsiz deneme', - includesTitle: 'İçerdikleri:', for: 'Temel Yeteneklerin Ücretsiz Denemesi', }, professional: { name: 'Profesyonel', description: 'Bireyler ve küçük takımlar için daha fazla güç açın.', - includesTitle: 'Ücretsiz plandaki her şey, artı:', for: 'Bağımsız Geliştiriciler/Küçük Takımlar için', }, team: { name: 'Takım', description: 'Sınırsız işbirliği ve en üst düzey performans.', - includesTitle: 'Profesyonel plandaki her şey, artı:', for: 'Orta Boyutlu Takımlar İçin', }, enterprise: { @@ -123,15 +115,15 @@ const translation = { description: 'Büyük ölçekli kritik sistemler için tam yetenekler ve destek.', includesTitle: 'Takım plandaki her şey, artı:', features: { - 3: 'Birden Fazla Çalışma Alanı ve Kurumsal Yönetim', 8: 'Profesyonel Teknik Destek', - 4: 'SSO', - 2: 'Özel Şirket Özellikleri', 1: 'Ticari Lisans Yetkilendirmesi', - 7: 'Dify Tarafından Resmi Güncellemeler ve Bakım', - 5: 'Dify Ortakları tarafından müzakere edilen SLA\'lar', 6: 'Gelişmiş Güvenlik ve Kontroller', + 5: 'Dify Partners tarafından müzakere edilen SLA\'lar', + 4: 'SSO', + 2: 'Özel Kurumsal Özellikler', 0: 'Kurumsal Düzeyde Ölçeklenebilir Dağıtım Çözümleri', + 7: 'Resmi olarak Dify tarafından Güncellemeler ve Bakım', + 3: 'Çoklu Çalışma Alanları ve Kurumsal Yönetim', }, priceTip: 'Yıllık Faturalama Sadece', for: 'Büyük boyutlu Takımlar için', @@ -140,9 +132,9 @@ const translation = { }, community: { features: { - 1: 'Tek İş Alanı', - 0: 'Tüm Temel Özellikler Kamu Deposu Altında Yayınlandı', - 2: 'Dify Açık Kaynak Lisansına uyar', + 1: 'Tek Çalışma Alanı', + 0: 'Genel depo altında yayınlanan tüm temel özellikler', + 2: 'Dify Açık Kaynak Lisansı ile uyumludur', }, price: 'Ücretsiz', includesTitle: 'Ücretsiz Özellikler:', @@ -153,10 +145,10 @@ const translation = { }, premium: { features: { - 1: 'Tek İş Alanı', - 0: 'Çeşitli Bulut Sağlayıcıları Tarafından Kendiliğinden Yönetilen Güvenilirlik', - 3: 'Öncelikli Email ve Sohbet Desteği', - 2: 'Web Uygulaması Logo ve Markalaşma Özelleştirmesi', + 1: 'Tek Çalışma Alanı', + 0: 'Çeşitli Bulut Sağlayıcıları Tarafından Kendi Kendini Yöneten Güvenilirlik', + 2: 'WebApp Logosu ve Marka Özelleştirmesi', + 3: 'Öncelikli E-posta ve Sohbet Desteği', }, name: 'Premium', includesTitle: 'Topluluktan her şey, artı:', @@ -173,8 +165,6 @@ const translation = { fullSolution: 'Daha fazla alan için planınızı yükseltin.', }, apps: { - fullTipLine1: 'Daha fazla uygulama oluşturmak için', - fullTipLine2: 'planınızı yükseltin.', contactUs: 'Bizimle iletişime geçin', fullTip2des: 'Kullanımı serbest bırakmak için etkisiz uygulamaların temizlenmesi önerilir veya bizimle iletişime geçin.', fullTip1des: 'Bu planda uygulama oluşturma limitine ulaştınız.', diff --git a/web/i18n/uk-UA/billing.ts b/web/i18n/uk-UA/billing.ts index 56888531b0..a048fe67cd 100644 --- a/web/i18n/uk-UA/billing.ts +++ b/web/i18n/uk-UA/billing.ts @@ -23,17 +23,13 @@ const translation = { contractSales: 'Зв\'язатися з відділом продажів', contractOwner: 'Зв\'язатися з керівником команди', startForFree: 'Почніть безкоштовно', - getStartedWith: 'Почніть роботу з ', contactSales: 'Зв\'язатися з відділом продажів', talkToSales: 'Поговоріть зі службою продажів', modelProviders: 'Постачальники моделей', - teamMembers: 'Члени команди', buildApps: 'Створювати додатки', vectorSpace: 'Векторний простір', - vectorSpaceBillingTooltip: 'Кожен 1 МБ може зберігати близько 1,2 мільйона символів векторизованих даних (оцінка з використанням OpenAI Embeddings, відрізняється в залежності від моделей).', vectorSpaceTooltip: 'Векторний простір – це система довгострокової пам\'яті, необхідна LLM для розуміння ваших даних.', documentProcessingPriority: 'Пріоритет обробки документів', - documentProcessingPriorityTip: 'Для вищого пріоритету обробки документів оновіть свій план.', documentProcessingPriorityUpgrade: 'Обробляйте більше даних із вищою точністю та на більших швидкостях.', priority: { 'standard': 'Стандартний', @@ -77,7 +73,6 @@ const translation = { ragAPIRequestTooltip: 'Відноситься до кількості викликів API, що викликають лише можливості обробки бази знань Dify.', receiptInfo: 'Лише власник команди та адміністратор команди можуть підписуватися та переглядати інформацію про виставлення рахунків', annotationQuota: 'Квота анотацій', - documentsUploadQuota: 'Квота завантаження документів', teamMember_one: '{{count,number}} член команди', teamWorkspace: '{{count,number}} Командний Простір', apiRateLimit: 'Обмеження швидкості API', @@ -103,19 +98,16 @@ const translation = { sandbox: { name: 'Пісочниця', description: '200 безкоштовних пробних версій GPT', - includesTitle: 'Включає в себе:', for: 'Безкоштовна пробна версія основних функцій', }, professional: { name: 'Професійний', description: 'Щоб окремі особи та невеликі команди могли отримати більше можливостей за доступною ціною.', - includesTitle: 'Все у безкоштовному плані, плюс:', for: 'Для незалежних розробників/малих команд', }, team: { name: 'Команда', description: 'Співпрацюйте без обмежень і користуйтеся продуктивністю найвищого рівня.', - includesTitle: 'Все, що входить до плану Professional, плюс:', for: 'Для середніх команд', }, enterprise: { @@ -123,15 +115,15 @@ const translation = { description: 'Отримайте повні можливості та підтримку для масштабних критично важливих систем.', includesTitle: 'Все, що входить до плану Team, плюс:', features: { - 5: 'Угоди про рівень обслуговування, узгоджені партнерами Dify', - 2: 'Ексклюзивні підприємницькі функції', - 6: 'Розвинена безпека та контроль', + 4: 'Єдиний вхід', + 7: 'Оновлення та обслуговування від Dify Official', + 1: 'Авторизація комерційної ліцензії', 8: 'Професійна технічна підтримка', - 1: 'Комерційна ліцензія на авторизацію', - 3: 'Кілька робочих просторів та управління підприємством', - 4: 'ССО', - 0: 'Рішення для масштабованого розгортання підприємств', - 7: 'Оновлення та обслуговування від Dify Офіційно', + 2: 'Ексклюзивні функції підприємства', + 6: 'Розширені функції безпеки та керування', + 3: 'Кілька робочих областей і управління підприємством', + 5: 'Угода про рівень обслуговування за домовленістю від Dify Partners', + 0: 'Масштабовані рішення для розгортання корпоративного рівня', }, btnText: 'Зв\'язатися з відділом продажу', priceTip: 'Тільки річна оплата', @@ -140,9 +132,9 @@ const translation = { }, community: { features: { - 2: 'Відповідає ліцензії Dify Open Source', 1: 'Єдине робоче місце', - 0: 'Усі основні функції випущені під публічним репозиторієм', + 2: 'Відповідає ліцензії Dify з відкритим вихідним кодом', + 0: 'Усі основні функції випущено в загальнодоступному репозиторії', }, btnText: 'Розпочніть з громади', includesTitle: 'Безкоштовні можливості:', @@ -153,10 +145,10 @@ const translation = { }, premium: { features: { - 2: 'Логотип веб-додатку та налаштування брендингу', 1: 'Єдине робоче місце', - 3: 'Пріоритетна email та чат підтримка', - 0: 'Самостійно керовані надійність різних хмарних постачальників', + 2: 'Налаштування логотипу WebApp та брендингу', + 3: 'Пріоритетна підтримка електронною поштою та в чаті', + 0: 'Самокерована надійність різними хмарними провайдерами', }, description: 'Для середніх підприємств та команд', btnText: 'Отримайте Преміум у', @@ -173,8 +165,6 @@ const translation = { fullSolution: 'Оновіть свій план, щоб отримати більше місця.', }, apps: { - fullTipLine1: 'Оновіть свій план, щоб', - fullTipLine2: 'створити більше програм.', fullTip1des: 'Ви досягли межі створення додатків за цим планом.', fullTip2: 'Досягнуто ліміту плану', fullTip1: 'Оновіть, щоб створити більше додатків', diff --git a/web/i18n/vi-VN/billing.ts b/web/i18n/vi-VN/billing.ts index 3a8ac03ffb..69035dc595 100644 --- a/web/i18n/vi-VN/billing.ts +++ b/web/i18n/vi-VN/billing.ts @@ -23,18 +23,13 @@ const translation = { contractSales: 'Liên hệ bộ phận bán hàng', contractOwner: 'Liên hệ quản lý nhóm', startForFree: 'Bắt đầu miễn phí', - getStartedWith: 'Bắt đầu với ', contactSales: 'Liên hệ Bán hàng', talkToSales: 'Nói chuyện với Bộ phận Bán hàng', modelProviders: 'Nhà cung cấp Mô hình', - teamMembers: 'Thành viên Nhóm', buildApps: 'Xây dựng Ứng dụng', vectorSpace: 'Không gian Vector', - vectorSpaceBillingTooltip: 'Mỗi 1MB có thể lưu trữ khoảng 1.2 triệu ký tự dữ liệu vector hóa (ước tính sử dụng OpenAI Embeddings, thay đổi tùy theo các mô hình).', vectorSpaceTooltip: 'Không gian Vector là hệ thống bộ nhớ dài hạn cần thiết cho LLMs để hiểu dữ liệu của bạn.', - documentsUploadQuota: 'Hạn mức Tải lên Tài liệu', documentProcessingPriority: 'Ưu tiên Xử lý Tài liệu', - documentProcessingPriorityTip: 'Để có ưu tiên xử lý tài liệu cao hơn, vui lòng nâng cấp kế hoạch của bạn.', documentProcessingPriorityUpgrade: 'Xử lý nhiều dữ liệu với độ chính xác cao và tốc độ nhanh hơn.', priority: { 'standard': 'Tiêu chuẩn', @@ -103,19 +98,16 @@ const translation = { sandbox: { name: 'Hộp Cát', description: 'Thử nghiệm miễn phí 200 lần GPT', - includesTitle: 'Bao gồm:', for: 'Dùng thử miễn phí các tính năng cốt lõi', }, professional: { name: 'Chuyên nghiệp', description: 'Dành cho cá nhân và các nhóm nhỏ để mở khóa nhiều sức mạnh với giá cả phải chăng.', - includesTitle: 'Tất cả trong kế hoạch miễn phí, cộng thêm:', for: 'Dành cho các nhà phát triển độc lập/nhóm nhỏ', }, team: { name: 'Nhóm', description: 'Hợp tác mà không giới hạn và tận hưởng hiệu suất hạng nhất.', - includesTitle: 'Tất cả trong kế hoạch Chuyên nghiệp, cộng thêm:', for: 'Dành cho các đội nhóm vừa', }, enterprise: { @@ -123,15 +115,15 @@ const translation = { description: 'Nhận toàn bộ khả năng và hỗ trợ cho các hệ thống quan trọng cho nhiệm vụ quy mô lớn.', includesTitle: 'Tất cả trong kế hoạch Nhóm, cộng thêm:', features: { - 2: 'Tính năng Doanh nghiệp Độc quyền', - 1: 'Giấy phép kinh doanh', - 8: 'Hỗ trợ kỹ thuật chuyên nghiệp', - 7: 'Cập nhật và Bảo trì bởi Dify Chính thức', - 5: 'Thỏa thuận SLA bởi các đối tác Dify', - 6: 'An ninh nâng cao và kiểm soát', - 3: 'Nhiều không gian làm việc & Quản lý doanh nghiệp', - 0: 'Giải pháp triển khai mở rộng quy mô cấp doanh nghiệp', + 2: 'Các tính năng dành riêng cho doanh nghiệp', + 3: 'Nhiều không gian làm việc & quản lý doanh nghiệp', + 7: 'Cập nhật và bảo trì bởi Dify chính thức', 4: 'SSO', + 8: 'Hỗ trợ kỹ thuật chuyên nghiệp', + 5: 'SLA được đàm phán bởi Dify Partners', + 1: 'Ủy quyền giấy phép thương mại', + 6: 'Bảo mật & Kiểm soát nâng cao', + 0: 'Giải pháp triển khai có thể mở rộng cấp doanh nghiệp', }, price: 'Tùy chỉnh', for: 'Dành cho các đội lớn', @@ -140,9 +132,9 @@ const translation = { }, community: { features: { - 2: 'Tuân thủ Giấy phép Mã nguồn Mở Dify', - 0: 'Tất cả các tính năng cốt lõi được phát hành dưới Kho lưu trữ công khai', 1: 'Không gian làm việc đơn', + 0: 'Tất cả các tính năng cốt lõi được phát hành trong kho lưu trữ công cộng', + 2: 'Tuân thủ Giấy phép nguồn mở Dify', }, description: 'Dành cho người dùng cá nhân, nhóm nhỏ hoặc các dự án phi thương mại', name: 'Cộng đồng', @@ -153,10 +145,10 @@ const translation = { }, premium: { features: { - 3: 'Hỗ trợ qua Email & Chat Ưu tiên', - 2: 'Tùy chỉnh Logo & Thương hiệu Ứng dụng Web', 1: 'Không gian làm việc đơn', - 0: 'Độ tin cậy tự quản lý bởi các nhà cung cấp đám mây khác nhau', + 2: 'Logo WebApp & Tùy chỉnh thương hiệu', + 3: 'Hỗ trợ email & trò chuyện ưu tiên', + 0: 'Độ tin cậy tự quản lý của các nhà cung cấp đám mây khác nhau', }, comingSoon: 'Hỗ trợ Microsoft Azure & Google Cloud Sẽ Đến Sớm', priceTip: 'Dựa trên Thị trường Đám mây', @@ -173,8 +165,6 @@ const translation = { fullSolution: 'Nâng cấp kế hoạch của bạn để có thêm không gian.', }, apps: { - fullTipLine1: 'Nâng cấp kế hoạch của bạn để', - fullTipLine2: 'xây dựng thêm ứng dụng.', contactUs: 'Liên hệ với chúng tôi', fullTip2: 'Đã đạt giới hạn kế hoạch', fullTip1des: 'Bạn đã đạt đến giới hạn xây dựng ứng dụng trên kế hoạch này.', diff --git a/web/i18n/zh-Hant/billing.ts b/web/i18n/zh-Hant/billing.ts index f957bc4eab..bedf4550f8 100644 --- a/web/i18n/zh-Hant/billing.ts +++ b/web/i18n/zh-Hant/billing.ts @@ -115,6 +115,15 @@ const translation = { description: '獲得大規模關鍵任務系統的完整功能和支援。', includesTitle: 'Team 計劃中的一切,加上:', features: { + 8: '專業技術支持', + 3: '多個工作區和企業管理', + 0: '企業級可擴展部署解決方案', + 1: '商業許可證授權', + 7: 'Dify 官方更新和維護', + 6: '進階安全與控制', + 4: '單一登入', + 5: 'Dify 合作夥伴協商的 SLA', + 2: '獨家企業功能', }, price: '自訂', btnText: '聯繫銷售', @@ -123,6 +132,9 @@ const translation = { }, community: { features: { + 0: '所有核心功能在公共存儲庫下發布', + 1: '單一工作區', + 2: '符合 Dify 開源許可證', }, includesTitle: '免費功能:', btnText: '開始使用社區', @@ -133,6 +145,10 @@ const translation = { }, premium: { features: { + 3: '優先電子郵件和聊天支持', + 2: 'WebApp 標誌和品牌定制', + 0: '各種雲端供應商的自我管理可靠性', + 1: '單一工作區', }, for: '適用於中型組織和團隊', comingSoon: '微軟 Azure 與 Google Cloud 支持即將推出', From 97b24f48d587656e6c21f99ee67ff8e81ac6486f Mon Sep 17 00:00:00 2001 From: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com> Date: Mon, 18 Aug 2025 09:43:20 +0800 Subject: [PATCH 09/78] feat: add testcontainers based tests for metadata service (#24048) --- .../services/test_metadata_service.py | 1144 +++++++++++++++++ 1 file changed, 1144 insertions(+) create mode 100644 api/tests/test_containers_integration_tests/services/test_metadata_service.py diff --git a/api/tests/test_containers_integration_tests/services/test_metadata_service.py b/api/tests/test_containers_integration_tests/services/test_metadata_service.py new file mode 100644 index 0000000000..7fef572c14 --- /dev/null +++ b/api/tests/test_containers_integration_tests/services/test_metadata_service.py @@ -0,0 +1,1144 @@ +from unittest.mock import patch + +import pytest +from faker import Faker + +from core.rag.index_processor.constant.built_in_field import BuiltInField +from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole +from models.dataset import Dataset, DatasetMetadata, DatasetMetadataBinding, Document +from services.entities.knowledge_entities.knowledge_entities import MetadataArgs +from services.metadata_service import MetadataService + + +class TestMetadataService: + """Integration tests for MetadataService using testcontainers.""" + + @pytest.fixture + def mock_external_service_dependencies(self): + """Mock setup for external service dependencies.""" + with ( + patch("services.metadata_service.current_user") as mock_current_user, + patch("services.metadata_service.redis_client") as mock_redis_client, + patch("services.dataset_service.DocumentService") as mock_document_service, + ): + # Setup default mock returns + mock_redis_client.get.return_value = None + mock_redis_client.set.return_value = True + mock_redis_client.delete.return_value = 1 + + yield { + "current_user": mock_current_user, + "redis_client": mock_redis_client, + "document_service": mock_document_service, + } + + def _create_test_account_and_tenant(self, db_session_with_containers, mock_external_service_dependencies): + """ + Helper method to create a test account and tenant for testing. + + Args: + db_session_with_containers: Database session from testcontainers infrastructure + mock_external_service_dependencies: Mock dependencies + + Returns: + tuple: (account, tenant) - Created account and tenant instances + """ + fake = Faker() + + # Create account + account = Account( + email=fake.email(), + name=fake.name(), + interface_language="en-US", + status="active", + ) + + from extensions.ext_database import db + + db.session.add(account) + db.session.commit() + + # Create tenant for the account + tenant = Tenant( + name=fake.company(), + status="normal", + ) + db.session.add(tenant) + db.session.commit() + + # Create tenant-account join + join = TenantAccountJoin( + tenant_id=tenant.id, + account_id=account.id, + role=TenantAccountRole.OWNER.value, + current=True, + ) + db.session.add(join) + db.session.commit() + + # Set current tenant for account + account.current_tenant = tenant + + return account, tenant + + def _create_test_dataset(self, db_session_with_containers, mock_external_service_dependencies, account, tenant): + """ + Helper method to create a test dataset for testing. + + Args: + db_session_with_containers: Database session from testcontainers infrastructure + mock_external_service_dependencies: Mock dependencies + account: Account instance + tenant: Tenant instance + + Returns: + Dataset: Created dataset instance + """ + fake = Faker() + + dataset = Dataset( + tenant_id=tenant.id, + name=fake.company(), + description=fake.text(max_nb_chars=100), + data_source_type="upload_file", + created_by=account.id, + built_in_field_enabled=False, + ) + + from extensions.ext_database import db + + db.session.add(dataset) + db.session.commit() + + return dataset + + def _create_test_document(self, db_session_with_containers, mock_external_service_dependencies, dataset, account): + """ + Helper method to create a test document for testing. + + Args: + db_session_with_containers: Database session from testcontainers infrastructure + mock_external_service_dependencies: Mock dependencies + dataset: Dataset instance + account: Account instance + + Returns: + Document: Created document instance + """ + fake = Faker() + + document = Document( + tenant_id=dataset.tenant_id, + dataset_id=dataset.id, + position=1, + data_source_type="upload_file", + data_source_info="{}", + batch="test-batch", + name=fake.file_name(), + created_from="web", + created_by=account.id, + doc_form="text", + doc_language="en", + ) + + from extensions.ext_database import db + + db.session.add(document) + db.session.commit() + + return document + + def test_create_metadata_success(self, db_session_with_containers, mock_external_service_dependencies): + """ + Test successful metadata creation with valid parameters. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + metadata_args = MetadataArgs(type="string", name="test_metadata") + + # Act: Execute the method under test + result = MetadataService.create_metadata(dataset.id, metadata_args) + + # Assert: Verify the expected outcomes + assert result is not None + assert result.name == "test_metadata" + assert result.type == "string" + assert result.dataset_id == dataset.id + assert result.tenant_id == tenant.id + assert result.created_by == account.id + + # Verify database state + from extensions.ext_database import db + + db.session.refresh(result) + assert result.id is not None + assert result.created_at is not None + + def test_create_metadata_name_too_long(self, db_session_with_containers, mock_external_service_dependencies): + """ + Test metadata creation fails when name exceeds 255 characters. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + long_name = "a" * 256 # 256 characters, exceeding 255 limit + metadata_args = MetadataArgs(type="string", name=long_name) + + # Act & Assert: Verify proper error handling + with pytest.raises(ValueError, match="Metadata name cannot exceed 255 characters."): + MetadataService.create_metadata(dataset.id, metadata_args) + + def test_create_metadata_name_already_exists(self, db_session_with_containers, mock_external_service_dependencies): + """ + Test metadata creation fails when name already exists in the same dataset. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + # Create first metadata + first_metadata_args = MetadataArgs(type="string", name="duplicate_name") + MetadataService.create_metadata(dataset.id, first_metadata_args) + + # Try to create second metadata with same name + second_metadata_args = MetadataArgs(type="number", name="duplicate_name") + + # Act & Assert: Verify proper error handling + with pytest.raises(ValueError, match="Metadata name already exists."): + MetadataService.create_metadata(dataset.id, second_metadata_args) + + def test_create_metadata_name_conflicts_with_built_in_field( + self, db_session_with_containers, mock_external_service_dependencies + ): + """ + Test metadata creation fails when name conflicts with built-in field names. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + # Try to create metadata with built-in field name + built_in_field_name = BuiltInField.document_name.value + metadata_args = MetadataArgs(type="string", name=built_in_field_name) + + # Act & Assert: Verify proper error handling + with pytest.raises(ValueError, match="Metadata name already exists in Built-in fields."): + MetadataService.create_metadata(dataset.id, metadata_args) + + def test_update_metadata_name_success(self, db_session_with_containers, mock_external_service_dependencies): + """ + Test successful metadata name update with valid parameters. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + # Create metadata first + metadata_args = MetadataArgs(type="string", name="old_name") + metadata = MetadataService.create_metadata(dataset.id, metadata_args) + + # Act: Execute the method under test + new_name = "new_name" + result = MetadataService.update_metadata_name(dataset.id, metadata.id, new_name) + + # Assert: Verify the expected outcomes + assert result is not None + assert result.name == new_name + assert result.updated_by == account.id + assert result.updated_at is not None + + # Verify database state + from extensions.ext_database import db + + db.session.refresh(result) + assert result.name == new_name + + def test_update_metadata_name_too_long(self, db_session_with_containers, mock_external_service_dependencies): + """ + Test metadata name update fails when new name exceeds 255 characters. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + # Create metadata first + metadata_args = MetadataArgs(type="string", name="old_name") + metadata = MetadataService.create_metadata(dataset.id, metadata_args) + + # Try to update with too long name + long_name = "a" * 256 # 256 characters, exceeding 255 limit + + # Act & Assert: Verify proper error handling + with pytest.raises(ValueError, match="Metadata name cannot exceed 255 characters."): + MetadataService.update_metadata_name(dataset.id, metadata.id, long_name) + + def test_update_metadata_name_already_exists(self, db_session_with_containers, mock_external_service_dependencies): + """ + Test metadata name update fails when new name already exists in the same dataset. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + # Create two metadata entries + first_metadata_args = MetadataArgs(type="string", name="first_metadata") + first_metadata = MetadataService.create_metadata(dataset.id, first_metadata_args) + + second_metadata_args = MetadataArgs(type="number", name="second_metadata") + second_metadata = MetadataService.create_metadata(dataset.id, second_metadata_args) + + # Try to update first metadata with second metadata's name + with pytest.raises(ValueError, match="Metadata name already exists."): + MetadataService.update_metadata_name(dataset.id, first_metadata.id, "second_metadata") + + def test_update_metadata_name_conflicts_with_built_in_field( + self, db_session_with_containers, mock_external_service_dependencies + ): + """ + Test metadata name update fails when new name conflicts with built-in field names. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + # Create metadata first + metadata_args = MetadataArgs(type="string", name="old_name") + metadata = MetadataService.create_metadata(dataset.id, metadata_args) + + # Try to update with built-in field name + built_in_field_name = BuiltInField.document_name.value + + with pytest.raises(ValueError, match="Metadata name already exists in Built-in fields."): + MetadataService.update_metadata_name(dataset.id, metadata.id, built_in_field_name) + + def test_update_metadata_name_not_found(self, db_session_with_containers, mock_external_service_dependencies): + """ + Test metadata name update fails when metadata ID does not exist. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + # Try to update non-existent metadata + import uuid + + fake_metadata_id = str(uuid.uuid4()) # Use valid UUID format + new_name = "new_name" + + # Act: Execute the method under test + result = MetadataService.update_metadata_name(dataset.id, fake_metadata_id, new_name) + + # Assert: Verify the method returns None when metadata is not found + assert result is None + + def test_delete_metadata_success(self, db_session_with_containers, mock_external_service_dependencies): + """ + Test successful metadata deletion with valid parameters. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + # Create metadata first + metadata_args = MetadataArgs(type="string", name="to_be_deleted") + metadata = MetadataService.create_metadata(dataset.id, metadata_args) + + # Act: Execute the method under test + result = MetadataService.delete_metadata(dataset.id, metadata.id) + + # Assert: Verify the expected outcomes + assert result is not None + assert result.id == metadata.id + + # Verify metadata was deleted from database + from extensions.ext_database import db + + deleted_metadata = db.session.query(DatasetMetadata).filter_by(id=metadata.id).first() + assert deleted_metadata is None + + def test_delete_metadata_not_found(self, db_session_with_containers, mock_external_service_dependencies): + """ + Test metadata deletion fails when metadata ID does not exist. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + # Try to delete non-existent metadata + import uuid + + fake_metadata_id = str(uuid.uuid4()) # Use valid UUID format + + # Act: Execute the method under test + result = MetadataService.delete_metadata(dataset.id, fake_metadata_id) + + # Assert: Verify the method returns None when metadata is not found + assert result is None + + def test_delete_metadata_with_document_bindings( + self, db_session_with_containers, mock_external_service_dependencies + ): + """ + Test metadata deletion successfully removes document metadata bindings. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + document = self._create_test_document( + db_session_with_containers, mock_external_service_dependencies, dataset, account + ) + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + # Create metadata + metadata_args = MetadataArgs(type="string", name="test_metadata") + metadata = MetadataService.create_metadata(dataset.id, metadata_args) + + # Create metadata binding + binding = DatasetMetadataBinding( + tenant_id=tenant.id, + dataset_id=dataset.id, + metadata_id=metadata.id, + document_id=document.id, + created_by=account.id, + ) + + from extensions.ext_database import db + + db.session.add(binding) + db.session.commit() + + # Set document metadata + document.doc_metadata = {"test_metadata": "test_value"} + db.session.add(document) + db.session.commit() + + # Act: Execute the method under test + result = MetadataService.delete_metadata(dataset.id, metadata.id) + + # Assert: Verify the expected outcomes + assert result is not None + + # Verify metadata was deleted from database + deleted_metadata = db.session.query(DatasetMetadata).filter_by(id=metadata.id).first() + assert deleted_metadata is None + + # Note: The service attempts to update document metadata but may not succeed + # due to mock configuration. The main functionality (metadata deletion) is verified. + + def test_get_built_in_fields_success(self, db_session_with_containers, mock_external_service_dependencies): + """ + Test successful retrieval of built-in metadata fields. + """ + # Act: Execute the method under test + result = MetadataService.get_built_in_fields() + + # Assert: Verify the expected outcomes + assert result is not None + assert len(result) == 5 + + # Verify all expected built-in fields are present + field_names = [field["name"] for field in result] + field_types = [field["type"] for field in result] + + assert BuiltInField.document_name.value in field_names + assert BuiltInField.uploader.value in field_names + assert BuiltInField.upload_date.value in field_names + assert BuiltInField.last_update_date.value in field_names + assert BuiltInField.source.value in field_names + + # Verify field types + assert "string" in field_types + assert "time" in field_types + + def test_enable_built_in_field_success(self, db_session_with_containers, mock_external_service_dependencies): + """ + Test successful enabling of built-in fields for a dataset. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + document = self._create_test_document( + db_session_with_containers, mock_external_service_dependencies, dataset, account + ) + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + # Mock DocumentService.get_working_documents_by_dataset_id + mock_external_service_dependencies["document_service"].get_working_documents_by_dataset_id.return_value = [ + document + ] + + # Verify dataset starts with built-in fields disabled + assert dataset.built_in_field_enabled is False + + # Act: Execute the method under test + MetadataService.enable_built_in_field(dataset) + + # Assert: Verify the expected outcomes + from extensions.ext_database import db + + db.session.refresh(dataset) + assert dataset.built_in_field_enabled is True + + # Note: Document metadata update depends on DocumentService mock working correctly + # The main functionality (enabling built-in fields) is verified + + def test_enable_built_in_field_already_enabled( + self, db_session_with_containers, mock_external_service_dependencies + ): + """ + Test enabling built-in fields when they are already enabled. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + # Enable built-in fields first + dataset.built_in_field_enabled = True + from extensions.ext_database import db + + db.session.add(dataset) + db.session.commit() + + # Mock DocumentService.get_working_documents_by_dataset_id + mock_external_service_dependencies["document_service"].get_working_documents_by_dataset_id.return_value = [] + + # Act: Execute the method under test + MetadataService.enable_built_in_field(dataset) + + # Assert: Verify the method returns early without changes + db.session.refresh(dataset) + assert dataset.built_in_field_enabled is True + + def test_enable_built_in_field_with_no_documents( + self, db_session_with_containers, mock_external_service_dependencies + ): + """ + Test enabling built-in fields for a dataset with no documents. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + # Mock DocumentService.get_working_documents_by_dataset_id to return empty list + mock_external_service_dependencies["document_service"].get_working_documents_by_dataset_id.return_value = [] + + # Act: Execute the method under test + MetadataService.enable_built_in_field(dataset) + + # Assert: Verify the expected outcomes + from extensions.ext_database import db + + db.session.refresh(dataset) + assert dataset.built_in_field_enabled is True + + def test_disable_built_in_field_success(self, db_session_with_containers, mock_external_service_dependencies): + """ + Test successful disabling of built-in fields for a dataset. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + document = self._create_test_document( + db_session_with_containers, mock_external_service_dependencies, dataset, account + ) + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + # Enable built-in fields first + dataset.built_in_field_enabled = True + from extensions.ext_database import db + + db.session.add(dataset) + db.session.commit() + + # Set document metadata with built-in fields + document.doc_metadata = { + BuiltInField.document_name.value: document.name, + BuiltInField.uploader.value: "test_uploader", + BuiltInField.upload_date.value: 1234567890.0, + BuiltInField.last_update_date.value: 1234567890.0, + BuiltInField.source.value: "test_source", + } + db.session.add(document) + db.session.commit() + + # Mock DocumentService.get_working_documents_by_dataset_id + mock_external_service_dependencies["document_service"].get_working_documents_by_dataset_id.return_value = [ + document + ] + + # Act: Execute the method under test + MetadataService.disable_built_in_field(dataset) + + # Assert: Verify the expected outcomes + db.session.refresh(dataset) + assert dataset.built_in_field_enabled is False + + # Note: Document metadata update depends on DocumentService mock working correctly + # The main functionality (disabling built-in fields) is verified + + def test_disable_built_in_field_already_disabled( + self, db_session_with_containers, mock_external_service_dependencies + ): + """ + Test disabling built-in fields when they are already disabled. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + # Verify dataset starts with built-in fields disabled + assert dataset.built_in_field_enabled is False + + # Mock DocumentService.get_working_documents_by_dataset_id + mock_external_service_dependencies["document_service"].get_working_documents_by_dataset_id.return_value = [] + + # Act: Execute the method under test + MetadataService.disable_built_in_field(dataset) + + # Assert: Verify the method returns early without changes + from extensions.ext_database import db + + db.session.refresh(dataset) + assert dataset.built_in_field_enabled is False + + def test_disable_built_in_field_with_no_documents( + self, db_session_with_containers, mock_external_service_dependencies + ): + """ + Test disabling built-in fields for a dataset with no documents. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + # Enable built-in fields first + dataset.built_in_field_enabled = True + from extensions.ext_database import db + + db.session.add(dataset) + db.session.commit() + + # Mock DocumentService.get_working_documents_by_dataset_id to return empty list + mock_external_service_dependencies["document_service"].get_working_documents_by_dataset_id.return_value = [] + + # Act: Execute the method under test + MetadataService.disable_built_in_field(dataset) + + # Assert: Verify the expected outcomes + db.session.refresh(dataset) + assert dataset.built_in_field_enabled is False + + def test_update_documents_metadata_success(self, db_session_with_containers, mock_external_service_dependencies): + """ + Test successful update of documents metadata. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + document = self._create_test_document( + db_session_with_containers, mock_external_service_dependencies, dataset, account + ) + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + # Create metadata + metadata_args = MetadataArgs(type="string", name="test_metadata") + metadata = MetadataService.create_metadata(dataset.id, metadata_args) + + # Mock DocumentService.get_document + mock_external_service_dependencies["document_service"].get_document.return_value = document + + # Create metadata operation data + from services.entities.knowledge_entities.knowledge_entities import ( + DocumentMetadataOperation, + MetadataDetail, + MetadataOperationData, + ) + + metadata_detail = MetadataDetail(id=metadata.id, name=metadata.name, value="test_value") + + operation = DocumentMetadataOperation(document_id=document.id, metadata_list=[metadata_detail]) + + operation_data = MetadataOperationData(operation_data=[operation]) + + # Act: Execute the method under test + MetadataService.update_documents_metadata(dataset, operation_data) + + # Assert: Verify the expected outcomes + from extensions.ext_database import db + + # Verify document metadata was updated + db.session.refresh(document) + assert document.doc_metadata is not None + assert "test_metadata" in document.doc_metadata + assert document.doc_metadata["test_metadata"] == "test_value" + + # Verify metadata binding was created + binding = ( + db.session.query(DatasetMetadataBinding).filter_by(metadata_id=metadata.id, document_id=document.id).first() + ) + assert binding is not None + assert binding.tenant_id == tenant.id + assert binding.dataset_id == dataset.id + + def test_update_documents_metadata_with_built_in_fields_enabled( + self, db_session_with_containers, mock_external_service_dependencies + ): + """ + Test update of documents metadata when built-in fields are enabled. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + document = self._create_test_document( + db_session_with_containers, mock_external_service_dependencies, dataset, account + ) + + # Enable built-in fields + dataset.built_in_field_enabled = True + from extensions.ext_database import db + + db.session.add(dataset) + db.session.commit() + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + # Create metadata + metadata_args = MetadataArgs(type="string", name="test_metadata") + metadata = MetadataService.create_metadata(dataset.id, metadata_args) + + # Mock DocumentService.get_document + mock_external_service_dependencies["document_service"].get_document.return_value = document + + # Create metadata operation data + from services.entities.knowledge_entities.knowledge_entities import ( + DocumentMetadataOperation, + MetadataDetail, + MetadataOperationData, + ) + + metadata_detail = MetadataDetail(id=metadata.id, name=metadata.name, value="test_value") + + operation = DocumentMetadataOperation(document_id=document.id, metadata_list=[metadata_detail]) + + operation_data = MetadataOperationData(operation_data=[operation]) + + # Act: Execute the method under test + MetadataService.update_documents_metadata(dataset, operation_data) + + # Assert: Verify the expected outcomes + # Verify document metadata was updated with both custom and built-in fields + db.session.refresh(document) + assert document.doc_metadata is not None + assert "test_metadata" in document.doc_metadata + assert document.doc_metadata["test_metadata"] == "test_value" + + # Note: Built-in fields would be added if DocumentService mock works correctly + # The main functionality (custom metadata update) is verified + + def test_update_documents_metadata_document_not_found( + self, db_session_with_containers, mock_external_service_dependencies + ): + """ + Test update of documents metadata when document is not found. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + # Create metadata + metadata_args = MetadataArgs(type="string", name="test_metadata") + metadata = MetadataService.create_metadata(dataset.id, metadata_args) + + # Mock DocumentService.get_document to return None (document not found) + mock_external_service_dependencies["document_service"].get_document.return_value = None + + # Create metadata operation data + from services.entities.knowledge_entities.knowledge_entities import ( + DocumentMetadataOperation, + MetadataDetail, + MetadataOperationData, + ) + + metadata_detail = MetadataDetail(id=metadata.id, name=metadata.name, value="test_value") + + operation = DocumentMetadataOperation(document_id="non-existent-document-id", metadata_list=[metadata_detail]) + + operation_data = MetadataOperationData(operation_data=[operation]) + + # Act: Execute the method under test + # The method should handle the error gracefully and continue + MetadataService.update_documents_metadata(dataset, operation_data) + + # Assert: Verify the method completes without raising exceptions + # The main functionality (error handling) is verified + + def test_knowledge_base_metadata_lock_check_dataset_id( + self, db_session_with_containers, mock_external_service_dependencies + ): + """ + Test metadata lock check for dataset operations. + """ + # Arrange: Setup mocks + mock_external_service_dependencies["redis_client"].get.return_value = None + mock_external_service_dependencies["redis_client"].set.return_value = True + + dataset_id = "test-dataset-id" + + # Act: Execute the method under test + MetadataService.knowledge_base_metadata_lock_check(dataset_id, None) + + # Assert: Verify the expected outcomes + # Verify Redis lock was set + mock_external_service_dependencies["redis_client"].set.assert_called_once() + + # Verify lock key format + call_args = mock_external_service_dependencies["redis_client"].set.call_args + assert call_args[0][0] == f"dataset_metadata_lock_{dataset_id}" + + def test_knowledge_base_metadata_lock_check_document_id( + self, db_session_with_containers, mock_external_service_dependencies + ): + """ + Test metadata lock check for document operations. + """ + # Arrange: Setup mocks + mock_external_service_dependencies["redis_client"].get.return_value = None + mock_external_service_dependencies["redis_client"].set.return_value = True + + document_id = "test-document-id" + + # Act: Execute the method under test + MetadataService.knowledge_base_metadata_lock_check(None, document_id) + + # Assert: Verify the expected outcomes + # Verify Redis lock was set + mock_external_service_dependencies["redis_client"].set.assert_called_once() + + # Verify lock key format + call_args = mock_external_service_dependencies["redis_client"].set.call_args + assert call_args[0][0] == f"document_metadata_lock_{document_id}" + + def test_knowledge_base_metadata_lock_check_lock_exists( + self, db_session_with_containers, mock_external_service_dependencies + ): + """ + Test metadata lock check when lock already exists. + """ + # Arrange: Setup mocks to simulate existing lock + mock_external_service_dependencies["redis_client"].get.return_value = "1" # Lock exists + + dataset_id = "test-dataset-id" + + # Act & Assert: Verify proper error handling + with pytest.raises( + ValueError, match="Another knowledge base metadata operation is running, please wait a moment." + ): + MetadataService.knowledge_base_metadata_lock_check(dataset_id, None) + + def test_knowledge_base_metadata_lock_check_document_lock_exists( + self, db_session_with_containers, mock_external_service_dependencies + ): + """ + Test metadata lock check when document lock already exists. + """ + # Arrange: Setup mocks to simulate existing lock + mock_external_service_dependencies["redis_client"].get.return_value = "1" # Lock exists + + document_id = "test-document-id" + + # Act & Assert: Verify proper error handling + with pytest.raises(ValueError, match="Another document metadata operation is running, please wait a moment."): + MetadataService.knowledge_base_metadata_lock_check(None, document_id) + + def test_get_dataset_metadatas_success(self, db_session_with_containers, mock_external_service_dependencies): + """ + Test successful retrieval of dataset metadata information. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + # Create metadata + metadata_args = MetadataArgs(type="string", name="test_metadata") + metadata = MetadataService.create_metadata(dataset.id, metadata_args) + + # Create document and metadata binding + document = self._create_test_document( + db_session_with_containers, mock_external_service_dependencies, dataset, account + ) + + binding = DatasetMetadataBinding( + tenant_id=tenant.id, + dataset_id=dataset.id, + metadata_id=metadata.id, + document_id=document.id, + created_by=account.id, + ) + + from extensions.ext_database import db + + db.session.add(binding) + db.session.commit() + + # Act: Execute the method under test + result = MetadataService.get_dataset_metadatas(dataset) + + # Assert: Verify the expected outcomes + assert result is not None + assert "doc_metadata" in result + assert "built_in_field_enabled" in result + + # Verify metadata information + doc_metadata = result["doc_metadata"] + assert len(doc_metadata) == 1 + assert doc_metadata[0]["id"] == metadata.id + assert doc_metadata[0]["name"] == metadata.name + assert doc_metadata[0]["type"] == metadata.type + assert doc_metadata[0]["count"] == 1 # One document bound to this metadata + + # Verify built-in field status + assert result["built_in_field_enabled"] is False + + def test_get_dataset_metadatas_with_built_in_fields_enabled( + self, db_session_with_containers, mock_external_service_dependencies + ): + """ + Test retrieval of dataset metadata when built-in fields are enabled. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + + # Enable built-in fields + dataset.built_in_field_enabled = True + from extensions.ext_database import db + + db.session.add(dataset) + db.session.commit() + + # Setup mocks + mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id + mock_external_service_dependencies["current_user"].id = account.id + + # Create metadata + metadata_args = MetadataArgs(type="string", name="test_metadata") + metadata = MetadataService.create_metadata(dataset.id, metadata_args) + + # Act: Execute the method under test + result = MetadataService.get_dataset_metadatas(dataset) + + # Assert: Verify the expected outcomes + assert result is not None + assert "doc_metadata" in result + assert "built_in_field_enabled" in result + + # Verify metadata information + doc_metadata = result["doc_metadata"] + assert len(doc_metadata) == 1 # Only custom metadata, built-in fields are not included in this list + + # Verify built-in field status + assert result["built_in_field_enabled"] is True + + def test_get_dataset_metadatas_no_metadata(self, db_session_with_containers, mock_external_service_dependencies): + """ + Test retrieval of dataset metadata when no metadata exists. + """ + # Arrange: Create test data + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + dataset = self._create_test_dataset( + db_session_with_containers, mock_external_service_dependencies, account, tenant + ) + + # Act: Execute the method under test + result = MetadataService.get_dataset_metadatas(dataset) + + # Assert: Verify the expected outcomes + assert result is not None + assert "doc_metadata" in result + assert "built_in_field_enabled" in result + + # Verify metadata information + doc_metadata = result["doc_metadata"] + assert len(doc_metadata) == 0 # No metadata exists + + # Verify built-in field status + assert result["built_in_field_enabled"] is False From 80f0594f4bc232a3ffebafa59893761e6001ddf2 Mon Sep 17 00:00:00 2001 From: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com> Date: Mon, 18 Aug 2025 09:54:22 +0800 Subject: [PATCH 10/78] feat: add testcontainers based tests for model loadbalancing service (#24066) --- .../test_model_load_balancing_service.py | 474 ++++++++++++++++++ 1 file changed, 474 insertions(+) create mode 100644 api/tests/test_containers_integration_tests/services/test_model_load_balancing_service.py diff --git a/api/tests/test_containers_integration_tests/services/test_model_load_balancing_service.py b/api/tests/test_containers_integration_tests/services/test_model_load_balancing_service.py new file mode 100644 index 0000000000..a8a36b2565 --- /dev/null +++ b/api/tests/test_containers_integration_tests/services/test_model_load_balancing_service.py @@ -0,0 +1,474 @@ +from unittest.mock import MagicMock, patch + +import pytest +from faker import Faker + +from models.account import TenantAccountJoin, TenantAccountRole +from models.model import Account, Tenant +from models.provider import LoadBalancingModelConfig, Provider, ProviderModelSetting +from services.model_load_balancing_service import ModelLoadBalancingService + + +class TestModelLoadBalancingService: + """Integration tests for ModelLoadBalancingService using testcontainers.""" + + @pytest.fixture + def mock_external_service_dependencies(self): + """Mock setup for external service dependencies.""" + with ( + patch("services.model_load_balancing_service.ProviderManager") as mock_provider_manager, + patch("services.model_load_balancing_service.LBModelManager") as mock_lb_model_manager, + patch("services.model_load_balancing_service.ModelProviderFactory") as mock_model_provider_factory, + patch("services.model_load_balancing_service.encrypter") as mock_encrypter, + ): + # Setup default mock returns + mock_provider_manager_instance = mock_provider_manager.return_value + + # Mock provider configuration + mock_provider_config = MagicMock() + mock_provider_config.provider.provider = "openai" + mock_provider_config.custom_configuration.provider = None + + # Mock provider model setting + mock_provider_model_setting = MagicMock() + mock_provider_model_setting.load_balancing_enabled = False + + mock_provider_config.get_provider_model_setting.return_value = mock_provider_model_setting + + # Mock provider configurations dict + mock_provider_configs = {"openai": mock_provider_config} + mock_provider_manager_instance.get_configurations.return_value = mock_provider_configs + + # Mock LBModelManager + mock_lb_model_manager.get_config_in_cooldown_and_ttl.return_value = (False, 0) + + # Mock ModelProviderFactory + mock_model_provider_factory_instance = mock_model_provider_factory.return_value + + # Mock credential schemas + mock_credential_schema = MagicMock() + mock_credential_schema.credential_form_schemas = [] + + # Mock provider configuration methods + mock_provider_config.extract_secret_variables.return_value = [] + mock_provider_config.obfuscated_credentials.return_value = {} + mock_provider_config._get_credential_schema.return_value = mock_credential_schema + + yield { + "provider_manager": mock_provider_manager, + "lb_model_manager": mock_lb_model_manager, + "model_provider_factory": mock_model_provider_factory, + "encrypter": mock_encrypter, + "provider_config": mock_provider_config, + "provider_model_setting": mock_provider_model_setting, + "credential_schema": mock_credential_schema, + } + + def _create_test_account_and_tenant(self, db_session_with_containers, mock_external_service_dependencies): + """ + Helper method to create a test account and tenant for testing. + + Args: + db_session_with_containers: Database session from testcontainers infrastructure + mock_external_service_dependencies: Mock dependencies + + Returns: + tuple: (account, tenant) - Created account and tenant instances + """ + fake = Faker() + + # Create account + account = Account( + email=fake.email(), + name=fake.name(), + interface_language="en-US", + status="active", + ) + + from extensions.ext_database import db + + db.session.add(account) + db.session.commit() + + # Create tenant for the account + tenant = Tenant( + name=fake.company(), + status="normal", + ) + db.session.add(tenant) + db.session.commit() + + # Create tenant-account join + join = TenantAccountJoin( + tenant_id=tenant.id, + account_id=account.id, + role=TenantAccountRole.OWNER.value, + current=True, + ) + db.session.add(join) + db.session.commit() + + # Set current tenant for account + account.current_tenant = tenant + + return account, tenant + + def _create_test_provider_and_setting( + self, db_session_with_containers, tenant_id, mock_external_service_dependencies + ): + """ + Helper method to create a test provider and provider model setting. + + Args: + db_session_with_containers: Database session from testcontainers infrastructure + tenant_id: Tenant ID for the provider + mock_external_service_dependencies: Mock dependencies + + Returns: + tuple: (provider, provider_model_setting) - Created provider and setting instances + """ + fake = Faker() + + from extensions.ext_database import db + + # Create provider + provider = Provider( + tenant_id=tenant_id, + provider_name="openai", + provider_type="custom", + is_valid=True, + ) + db.session.add(provider) + db.session.commit() + + # Create provider model setting + provider_model_setting = ProviderModelSetting( + tenant_id=tenant_id, + provider_name="openai", + model_name="gpt-3.5-turbo", + model_type="text-generation", # Use the origin model type that matches the query + enabled=True, + load_balancing_enabled=False, + ) + db.session.add(provider_model_setting) + db.session.commit() + + return provider, provider_model_setting + + def test_enable_model_load_balancing_success(self, db_session_with_containers, mock_external_service_dependencies): + """ + Test successful model load balancing enablement. + + This test verifies: + - Proper provider configuration retrieval + - Successful enablement of model load balancing + - Correct method calls to provider configuration + """ + # Arrange: Create test data + fake = Faker() + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + provider, provider_model_setting = self._create_test_provider_and_setting( + db_session_with_containers, tenant.id, mock_external_service_dependencies + ) + + # Setup mocks for enable method + mock_provider_config = mock_external_service_dependencies["provider_config"] + mock_provider_config.enable_model_load_balancing = MagicMock() + + # Act: Execute the method under test + service = ModelLoadBalancingService() + service.enable_model_load_balancing( + tenant_id=tenant.id, provider="openai", model="gpt-3.5-turbo", model_type="llm" + ) + + # Assert: Verify the expected outcomes + mock_provider_config.enable_model_load_balancing.assert_called_once() + call_args = mock_provider_config.enable_model_load_balancing.call_args + assert call_args.kwargs["model"] == "gpt-3.5-turbo" + assert call_args.kwargs["model_type"].value == "llm" # ModelType enum value + + # Verify database state + from extensions.ext_database import db + + db.session.refresh(provider) + db.session.refresh(provider_model_setting) + assert provider.id is not None + assert provider_model_setting.id is not None + + def test_disable_model_load_balancing_success(self, db_session_with_containers, mock_external_service_dependencies): + """ + Test successful model load balancing disablement. + + This test verifies: + - Proper provider configuration retrieval + - Successful disablement of model load balancing + - Correct method calls to provider configuration + """ + # Arrange: Create test data + fake = Faker() + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + provider, provider_model_setting = self._create_test_provider_and_setting( + db_session_with_containers, tenant.id, mock_external_service_dependencies + ) + + # Setup mocks for disable method + mock_provider_config = mock_external_service_dependencies["provider_config"] + mock_provider_config.disable_model_load_balancing = MagicMock() + + # Act: Execute the method under test + service = ModelLoadBalancingService() + service.disable_model_load_balancing( + tenant_id=tenant.id, provider="openai", model="gpt-3.5-turbo", model_type="llm" + ) + + # Assert: Verify the expected outcomes + mock_provider_config.disable_model_load_balancing.assert_called_once() + call_args = mock_provider_config.disable_model_load_balancing.call_args + assert call_args.kwargs["model"] == "gpt-3.5-turbo" + assert call_args.kwargs["model_type"].value == "llm" # ModelType enum value + + # Verify database state + from extensions.ext_database import db + + db.session.refresh(provider) + db.session.refresh(provider_model_setting) + assert provider.id is not None + assert provider_model_setting.id is not None + + def test_enable_model_load_balancing_provider_not_found( + self, db_session_with_containers, mock_external_service_dependencies + ): + """ + Test error handling when provider does not exist. + + This test verifies: + - Proper error handling for non-existent provider + - Correct exception type and message + - No database state changes + """ + # Arrange: Create test data + fake = Faker() + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + + # Setup mocks to return empty provider configurations + mock_provider_manager = mock_external_service_dependencies["provider_manager"] + mock_provider_manager_instance = mock_provider_manager.return_value + mock_provider_manager_instance.get_configurations.return_value = {} + + # Act & Assert: Verify proper error handling + service = ModelLoadBalancingService() + with pytest.raises(ValueError) as exc_info: + service.enable_model_load_balancing( + tenant_id=tenant.id, provider="nonexistent_provider", model="gpt-3.5-turbo", model_type="llm" + ) + + # Verify correct error message + assert "Provider nonexistent_provider does not exist." in str(exc_info.value) + + # Verify no database state changes occurred + from extensions.ext_database import db + + db.session.rollback() + + def test_get_load_balancing_configs_success(self, db_session_with_containers, mock_external_service_dependencies): + """ + Test successful retrieval of load balancing configurations. + + This test verifies: + - Proper provider configuration retrieval + - Successful database query for load balancing configs + - Correct return format and data structure + """ + # Arrange: Create test data + fake = Faker() + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + provider, provider_model_setting = self._create_test_provider_and_setting( + db_session_with_containers, tenant.id, mock_external_service_dependencies + ) + + # Create load balancing config + from extensions.ext_database import db + + load_balancing_config = LoadBalancingModelConfig( + tenant_id=tenant.id, + provider_name="openai", + model_name="gpt-3.5-turbo", + model_type="text-generation", # Use the origin model type that matches the query + name="config1", + encrypted_config='{"api_key": "test_key"}', + enabled=True, + ) + db.session.add(load_balancing_config) + db.session.commit() + + # Verify the config was created + db.session.refresh(load_balancing_config) + assert load_balancing_config.id is not None + + # Setup mocks for get_load_balancing_configs method + mock_provider_config = mock_external_service_dependencies["provider_config"] + mock_provider_model_setting = mock_external_service_dependencies["provider_model_setting"] + mock_provider_model_setting.load_balancing_enabled = True + + # Mock credential schema methods + mock_credential_schema = mock_external_service_dependencies["credential_schema"] + mock_credential_schema.credential_form_schemas = [] + + # Mock encrypter + mock_encrypter = mock_external_service_dependencies["encrypter"] + mock_encrypter.get_decrypt_decoding.return_value = ("key", "cipher") + + # Mock _get_credential_schema method + mock_provider_config._get_credential_schema.return_value = mock_credential_schema + + # Mock extract_secret_variables method + mock_provider_config.extract_secret_variables.return_value = [] + + # Mock obfuscated_credentials method + mock_provider_config.obfuscated_credentials.return_value = {} + + # Mock LBModelManager.get_config_in_cooldown_and_ttl + mock_lb_model_manager = mock_external_service_dependencies["lb_model_manager"] + mock_lb_model_manager.get_config_in_cooldown_and_ttl.return_value = (False, 0) + + # Act: Execute the method under test + service = ModelLoadBalancingService() + is_enabled, configs = service.get_load_balancing_configs( + tenant_id=tenant.id, provider="openai", model="gpt-3.5-turbo", model_type="llm" + ) + + # Assert: Verify the expected outcomes + assert is_enabled is True + assert len(configs) == 1 + assert configs[0]["id"] == load_balancing_config.id + assert configs[0]["name"] == "config1" + assert configs[0]["enabled"] is True + assert configs[0]["in_cooldown"] is False + assert configs[0]["ttl"] == 0 + + # Verify database state + db.session.refresh(load_balancing_config) + assert load_balancing_config.id is not None + + def test_get_load_balancing_configs_provider_not_found( + self, db_session_with_containers, mock_external_service_dependencies + ): + """ + Test error handling when provider does not exist in get_load_balancing_configs. + + This test verifies: + - Proper error handling for non-existent provider + - Correct exception type and message + - No database state changes + """ + # Arrange: Create test data + fake = Faker() + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + + # Setup mocks to return empty provider configurations + mock_provider_manager = mock_external_service_dependencies["provider_manager"] + mock_provider_manager_instance = mock_provider_manager.return_value + mock_provider_manager_instance.get_configurations.return_value = {} + + # Act & Assert: Verify proper error handling + service = ModelLoadBalancingService() + with pytest.raises(ValueError) as exc_info: + service.get_load_balancing_configs( + tenant_id=tenant.id, provider="nonexistent_provider", model="gpt-3.5-turbo", model_type="llm" + ) + + # Verify correct error message + assert "Provider nonexistent_provider does not exist." in str(exc_info.value) + + # Verify no database state changes occurred + from extensions.ext_database import db + + db.session.rollback() + + def test_get_load_balancing_configs_with_inherit_config( + self, db_session_with_containers, mock_external_service_dependencies + ): + """ + Test load balancing configs retrieval with inherit configuration. + + This test verifies: + - Proper handling of inherit configuration + - Correct ordering of configurations + - Inherit config initialization when needed + """ + # Arrange: Create test data + fake = Faker() + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + provider, provider_model_setting = self._create_test_provider_and_setting( + db_session_with_containers, tenant.id, mock_external_service_dependencies + ) + + # Create load balancing config + from extensions.ext_database import db + + load_balancing_config = LoadBalancingModelConfig( + tenant_id=tenant.id, + provider_name="openai", + model_name="gpt-3.5-turbo", + model_type="text-generation", # Use the origin model type that matches the query + name="config1", + encrypted_config='{"api_key": "test_key"}', + enabled=True, + ) + db.session.add(load_balancing_config) + db.session.commit() + + # Setup mocks for inherit config scenario + mock_provider_config = mock_external_service_dependencies["provider_config"] + mock_provider_config.custom_configuration.provider = MagicMock() # Enable custom config + + mock_provider_model_setting = mock_external_service_dependencies["provider_model_setting"] + mock_provider_model_setting.load_balancing_enabled = True + + # Mock credential schema methods + mock_credential_schema = mock_external_service_dependencies["credential_schema"] + mock_credential_schema.credential_form_schemas = [] + + # Mock encrypter + mock_encrypter = mock_external_service_dependencies["encrypter"] + mock_encrypter.get_decrypt_decoding.return_value = ("key", "cipher") + + # Act: Execute the method under test + service = ModelLoadBalancingService() + is_enabled, configs = service.get_load_balancing_configs( + tenant_id=tenant.id, provider="openai", model="gpt-3.5-turbo", model_type="llm" + ) + + # Assert: Verify the expected outcomes + assert is_enabled is True + assert len(configs) == 2 # inherit config + existing config + + # First config should be inherit config + assert configs[0]["name"] == "__inherit__" + assert configs[0]["enabled"] is True + + # Second config should be the existing config + assert configs[1]["id"] == load_balancing_config.id + assert configs[1]["name"] == "config1" + + # Verify database state + db.session.refresh(load_balancing_config) + assert load_balancing_config.id is not None + + # Verify inherit config was created in database + inherit_configs = ( + db.session.query(LoadBalancingModelConfig).filter(LoadBalancingModelConfig.name == "__inherit__").all() + ) + assert len(inherit_configs) == 1 From f8fc9f8c794f43dc0dea1eaae2a27c2fac9ce59d Mon Sep 17 00:00:00 2001 From: Zhehao Peng <32246435+Zhehao-P@users.noreply.github.com> Date: Sun, 17 Aug 2025 19:01:29 -0700 Subject: [PATCH 11/78] feat: add select input support to the conversation opener (#24043) --- .../conversation-opener/modal.tsx | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx b/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx index 51e33c43d2..53db991e71 100644 --- a/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx +++ b/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx @@ -8,6 +8,7 @@ import Modal from '@/app/components/base/modal' import Button from '@/app/components/base/button' import Divider from '@/app/components/base/divider' import ConfirmAddVar from '@/app/components/app/configuration/config-prompt/confirm-add-var' +import PromptEditor from '@/app/components/base/prompt-editor' import type { OpeningStatement } from '@/app/components/base/features/types' import { getInputKeys } from '@/app/components/base/block-input' import type { PromptVariable } from '@/models/debug' @@ -101,7 +102,7 @@ const OpeningSettingModal = ({
·
{tempSuggestedQuestions.length}/{MAX_QUESTION_NUM}
- +
{t('appDebug.feature.conversationOpener.title')}
-
+
-