diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index a5a5071fae..9c3daddbfc 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -99,3 +99,6 @@ jobs: - name: Run Tool run: uv run --project api bash dev/pytest/pytest_tools.sh + + - name: Run TestContainers + run: uv run --project api bash dev/pytest/pytest_testcontainers.sh diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 5e290c5d02..152ff3b648 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -9,6 +9,7 @@ permissions: jobs: autofix: + if: github.repository == 'langgenius/dify' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index 16a1268cb1..775f6f351f 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,10 @@ Quickly deploy Dify to Alibaba cloud with [Alibaba Cloud Computing Nest](https:/ One-Click deploy Dify to Alibaba Cloud with [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) +#### Deploy to AKS with Azure Devops Pipeline + +One-Click deploy Dify to AKS with [Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS) + ## Contributing diff --git a/README_AR.md b/README_AR.md index d2cb0098a3..e7a4dbdb27 100644 --- a/README_AR.md +++ b/README_AR.md @@ -217,6 +217,10 @@ docker compose up -d انشر ​​Dify على علي بابا كلاود بنقرة واحدة باستخدام [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) +#### استخدام Azure Devops Pipeline للنشر على AKS + +انشر Dify على AKS بنقرة واحدة باستخدام [Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS) + ## المساهمة diff --git a/README_BN.md b/README_BN.md index f57413ec8b..e4da437eff 100644 --- a/README_BN.md +++ b/README_BN.md @@ -235,6 +235,10 @@ GitHub-এ ডিফাইকে স্টার দিয়ে রাখুন [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) + #### AKS-এ ডিপ্লয় করার জন্য Azure Devops Pipeline ব্যবহার + +[Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS) ব্যবহার করে Dify কে AKS-এ এক ক্লিকে ডিপ্লয় করুন + ## Contributing diff --git a/README_CN.md b/README_CN.md index e9c73eb48b..82149519d3 100644 --- a/README_CN.md +++ b/README_CN.md @@ -233,6 +233,9 @@ docker compose up -d 使用 [阿里云数据管理DMS](https://help.aliyun.com/zh/dms/dify-in-invitational-preview) 将 Dify 一键部署到 阿里云 +#### 使用 Azure Devops Pipeline 部署到AKS + +使用[Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS) 将 Dify 一键部署到 AKS ## Star History diff --git a/README_DE.md b/README_DE.md index d31a56542d..2420ac0392 100644 --- a/README_DE.md +++ b/README_DE.md @@ -230,6 +230,10 @@ Bereitstellung von Dify auf AWS mit [CDK](https://aws.amazon.com/cdk/) Ein-Klick-Bereitstellung von Dify in der Alibaba Cloud mit [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) +#### Verwendung von Azure Devops Pipeline für AKS-Bereitstellung + +Stellen Sie Dify mit einem Klick in AKS bereit, indem Sie [Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS) verwenden + ## Contributing diff --git a/README_ES.md b/README_ES.md index 918bfe2286..4fa59dc18f 100644 --- a/README_ES.md +++ b/README_ES.md @@ -230,6 +230,10 @@ Despliegue Dify en AWS usando [CDK](https://aws.amazon.com/cdk/) Despliega Dify en Alibaba Cloud con un solo clic con [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) +#### Uso de Azure Devops Pipeline para implementar en AKS + +Implementa Dify en AKS con un clic usando [Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS) + ## Contribuir diff --git a/README_FR.md b/README_FR.md index 56ca878aae..dcbc869620 100644 --- a/README_FR.md +++ b/README_FR.md @@ -228,6 +228,10 @@ Déployez Dify sur AWS en utilisant [CDK](https://aws.amazon.com/cdk/) Déployez Dify en un clic sur Alibaba Cloud avec [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) +#### Utilisation d'Azure Devops Pipeline pour déployer sur AKS + +Déployez Dify sur AKS en un clic en utilisant [Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS) + ## Contribuer diff --git a/README_JA.md b/README_JA.md index 6d277a36ed..d840fd6419 100644 --- a/README_JA.md +++ b/README_JA.md @@ -227,6 +227,10 @@ docker compose up -d #### Alibaba Cloud Data Management [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) を利用して、DifyをAlibaba Cloudへワンクリックでデプロイできます +#### AKSへのデプロイにAzure Devops Pipelineを使用 + +[Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS)を使用してDifyをAKSにワンクリックでデプロイ + ## 貢献 diff --git a/README_KL.md b/README_KL.md index dac67eeb29..41c7969e1c 100644 --- a/README_KL.md +++ b/README_KL.md @@ -228,6 +228,10 @@ wa'logh nIqHom neH ghun deployment toy'wI' [CDK](https://aws.amazon.com/cdk/) lo [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) +#### AKS 'e' Deploy je Azure Devops Pipeline lo'laH + +[Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS) lo'laH Dify AKS 'e' wa'DIch click 'e' Deploy + ## Contributing diff --git a/README_KR.md b/README_KR.md index 072481da02..d4b31a8928 100644 --- a/README_KR.md +++ b/README_KR.md @@ -222,6 +222,10 @@ Dify를 Kubernetes에 배포하고 프리미엄 스케일링 설정을 구성했 [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/)를 통해 원클릭으로 Dify를 Alibaba Cloud에 배포할 수 있습니다 +#### AKS에 배포하기 위해 Azure Devops Pipeline 사용 + +[Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS)을 사용하여 Dify를 AKS에 원클릭으로 배포 + ## 기여 diff --git a/README_PT.md b/README_PT.md index 1260f8e6fd..94452cb233 100644 --- a/README_PT.md +++ b/README_PT.md @@ -227,6 +227,10 @@ Implante o Dify na AWS usando [CDK](https://aws.amazon.com/cdk/) Implante o Dify na Alibaba Cloud com um clique usando o [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) +#### Usando Azure Devops Pipeline para Implantar no AKS + +Implante o Dify no AKS com um clique usando [Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS) + ## Contribuindo diff --git a/README_SI.md b/README_SI.md index 7ded001d86..d840e9155f 100644 --- a/README_SI.md +++ b/README_SI.md @@ -228,6 +228,10 @@ Uvedite Dify v AWS z uporabo [CDK](https://aws.amazon.com/cdk/) Z enim klikom namestite Dify na Alibaba Cloud z [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) +#### Uporaba Azure Devops Pipeline za uvajanje v AKS + +Z enim klikom namestite Dify v AKS z uporabo [Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS) + ## Prispevam diff --git a/README_TR.md b/README_TR.md index 37953f0de1..470a7570e0 100644 --- a/README_TR.md +++ b/README_TR.md @@ -221,6 +221,10 @@ Dify'ı bulut platformuna tek tıklamayla dağıtın [terraform](https://www.ter [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) kullanarak Dify'ı tek tıkla Alibaba Cloud'a dağıtın +#### AKS'ye Dağıtım için Azure Devops Pipeline Kullanımı + +[Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS) kullanarak Dify'ı tek tıkla AKS'ye dağıtın + ## Katkıda Bulunma diff --git a/README_TW.md b/README_TW.md index f70d6a25f6..18f1d2754a 100644 --- a/README_TW.md +++ b/README_TW.md @@ -233,6 +233,10 @@ Dify 的所有功能都提供相應的 API,因此您可以輕鬆地將 Dify 透過 [阿里雲數據管理DMS](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/),一鍵將 Dify 部署至阿里雲 +#### 使用 Azure Devops Pipeline 部署到AKS + +使用[Azure Devops Pipeline Helm Chart by @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS) 將 Dify 一鍵部署到 AKS + ## 貢獻 diff --git a/README_VI.md b/README_VI.md index ddd9aa95f6..2ab6da80fc 100644 --- a/README_VI.md +++ b/README_VI.md @@ -224,6 +224,10 @@ Triển khai Dify trên AWS bằng [CDK](https://aws.amazon.com/cdk/) Triển khai Dify lên Alibaba Cloud chỉ với một cú nhấp chuột bằng [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) +#### Sử dụng Azure Devops Pipeline để Triển khai lên AKS + +Triển khai Dify lên AKS chỉ với một cú nhấp chuột bằng [Azure Devops Pipeline Helm Chart bởi @LeoZhang](https://github.com/Ruiruiz30/Dify-helm-chart-AKS) + ## Đóng góp diff --git a/api/.env.example b/api/.env.example index 18f2dbf647..4beabfecea 100644 --- a/api/.env.example +++ b/api/.env.example @@ -232,6 +232,7 @@ TABLESTORE_ENDPOINT=https://instance-name.cn-hangzhou.ots.aliyuncs.com TABLESTORE_INSTANCE_NAME=instance-name TABLESTORE_ACCESS_KEY_ID=xxx TABLESTORE_ACCESS_KEY_SECRET=xxx +TABLESTORE_NORMALIZE_FULLTEXT_BM25_SCORE=false # Tidb Vector configuration TIDB_VECTOR_HOST=xxx.eu-central-1.xxx.aws.tidbcloud.com diff --git a/api/commands.py b/api/commands.py index 1eacdb542f..27f558c339 100644 --- a/api/commands.py +++ b/api/commands.py @@ -5,6 +5,7 @@ import secrets from typing import Any, Optional import click +import sqlalchemy as sa from flask import current_app from pydantic import TypeAdapter from sqlalchemy import select @@ -462,7 +463,7 @@ def convert_to_agent_apps(): """ with db.engine.begin() as conn: - rs = conn.execute(db.text(sql_query)) + rs = conn.execute(sa.text(sql_query)) apps = [] for i in rs: @@ -707,7 +708,7 @@ def fix_app_site_missing(): sql = """select apps.id as id from apps left join sites on sites.app_id=apps.id where sites.id is null limit 1000""" with db.engine.begin() as conn: - rs = conn.execute(db.text(sql)) + rs = conn.execute(sa.text(sql)) processed_count = 0 for i in rs: @@ -921,7 +922,7 @@ def clear_orphaned_file_records(force: bool): ) orphaned_message_files = [] with db.engine.begin() as conn: - rs = conn.execute(db.text(query)) + rs = conn.execute(sa.text(query)) for i in rs: orphaned_message_files.append({"id": str(i[0]), "message_id": str(i[1])}) @@ -942,7 +943,7 @@ def clear_orphaned_file_records(force: bool): click.echo(click.style("- Deleting orphaned message_files records", fg="white")) query = "DELETE FROM message_files WHERE id IN :ids" with db.engine.begin() as conn: - conn.execute(db.text(query), {"ids": tuple([record["id"] for record in orphaned_message_files])}) + conn.execute(sa.text(query), {"ids": tuple([record["id"] for record in orphaned_message_files])}) click.echo( click.style(f"Removed {len(orphaned_message_files)} orphaned message_files records.", fg="green") ) @@ -959,7 +960,7 @@ def clear_orphaned_file_records(force: bool): click.echo(click.style(f"- Listing file records in table {files_table['table']}", fg="white")) query = f"SELECT {files_table['id_column']}, {files_table['key_column']} FROM {files_table['table']}" with db.engine.begin() as conn: - rs = conn.execute(db.text(query)) + rs = conn.execute(sa.text(query)) for i in rs: all_files_in_tables.append({"table": files_table["table"], "id": str(i[0]), "key": i[1]}) click.echo(click.style(f"Found {len(all_files_in_tables)} files in tables.", fg="white")) @@ -979,7 +980,7 @@ def clear_orphaned_file_records(force: bool): f"SELECT {ids_table['column']} FROM {ids_table['table']} WHERE {ids_table['column']} IS NOT NULL" ) with db.engine.begin() as conn: - rs = conn.execute(db.text(query)) + rs = conn.execute(sa.text(query)) for i in rs: all_ids_in_tables.append({"table": ids_table["table"], "id": str(i[0])}) elif ids_table["type"] == "text": @@ -994,7 +995,7 @@ def clear_orphaned_file_records(force: bool): f"FROM {ids_table['table']}" ) with db.engine.begin() as conn: - rs = conn.execute(db.text(query)) + rs = conn.execute(sa.text(query)) for i in rs: for j in i[0]: all_ids_in_tables.append({"table": ids_table["table"], "id": j}) @@ -1013,7 +1014,7 @@ def clear_orphaned_file_records(force: bool): f"FROM {ids_table['table']}" ) with db.engine.begin() as conn: - rs = conn.execute(db.text(query)) + rs = conn.execute(sa.text(query)) for i in rs: for j in i[0]: all_ids_in_tables.append({"table": ids_table["table"], "id": j}) @@ -1042,7 +1043,7 @@ def clear_orphaned_file_records(force: bool): click.echo(click.style(f"- Deleting orphaned file records in table {files_table['table']}", fg="white")) query = f"DELETE FROM {files_table['table']} WHERE {files_table['id_column']} IN :ids" with db.engine.begin() as conn: - conn.execute(db.text(query), {"ids": tuple(orphaned_files)}) + conn.execute(sa.text(query), {"ids": tuple(orphaned_files)}) except Exception as e: click.echo(click.style(f"Error deleting orphaned file records: {str(e)}", fg="red")) return @@ -1112,7 +1113,7 @@ def remove_orphaned_files_on_storage(force: bool): click.echo(click.style(f"- Listing files from table {files_table['table']}", fg="white")) query = f"SELECT {files_table['key_column']} FROM {files_table['table']}" with db.engine.begin() as conn: - rs = conn.execute(db.text(query)) + rs = conn.execute(sa.text(query)) for i in rs: all_files_in_tables.append(str(i[0])) click.echo(click.style(f"Found {len(all_files_in_tables)} files in tables.", fg="white")) diff --git a/api/configs/middleware/__init__.py b/api/configs/middleware/__init__.py index 68b16e48db..ff290ff99d 100644 --- a/api/configs/middleware/__init__.py +++ b/api/configs/middleware/__init__.py @@ -215,7 +215,7 @@ class DatabaseConfig(BaseSettings): class CeleryConfig(DatabaseConfig): CELERY_BACKEND: str = Field( - description="Backend for Celery task results. Options: 'database', 'redis'.", + description="Backend for Celery task results. Options: 'database', 'redis', 'rabbitmq'.", default="redis", ) @@ -245,7 +245,12 @@ class CeleryConfig(DatabaseConfig): @computed_field def CELERY_RESULT_BACKEND(self) -> str | None: - return f"db+{self.SQLALCHEMY_DATABASE_URI}" if self.CELERY_BACKEND == "database" else self.CELERY_BROKER_URL + if self.CELERY_BACKEND in ("database", "rabbitmq"): + return f"db+{self.SQLALCHEMY_DATABASE_URI}" + elif self.CELERY_BACKEND == "redis": + return self.CELERY_BROKER_URL + else: + return None @property def BROKER_USE_SSL(self) -> bool: diff --git a/api/configs/middleware/vdb/tablestore_config.py b/api/configs/middleware/vdb/tablestore_config.py index c4dcc0d465..1aab01c6e1 100644 --- a/api/configs/middleware/vdb/tablestore_config.py +++ b/api/configs/middleware/vdb/tablestore_config.py @@ -28,3 +28,8 @@ class TableStoreConfig(BaseSettings): description="AccessKey secret for the instance name", default=None, ) + + TABLESTORE_NORMALIZE_FULLTEXT_BM25_SCORE: bool = Field( + description="Whether to normalize full-text search scores to [0, 1]", + default=False, + ) diff --git a/api/constants/__init__.py b/api/constants/__init__.py index 9e052320ac..c98f4d55c8 100644 --- a/api/constants/__init__.py +++ b/api/constants/__init__.py @@ -9,10 +9,10 @@ DEFAULT_FILE_NUMBER_LIMITS = 3 IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "webp", "gif", "svg"] IMAGE_EXTENSIONS.extend([ext.upper() for ext in IMAGE_EXTENSIONS]) -VIDEO_EXTENSIONS = ["mp4", "mov", "mpeg", "mpga"] +VIDEO_EXTENSIONS = ["mp4", "mov", "mpeg", "webm"] VIDEO_EXTENSIONS.extend([ext.upper() for ext in VIDEO_EXTENSIONS]) -AUDIO_EXTENSIONS = ["mp3", "m4a", "wav", "webm", "amr"] +AUDIO_EXTENSIONS = ["mp3", "m4a", "wav", "amr", "mpga"] AUDIO_EXTENSIONS.extend([ext.upper() for ext in AUDIO_EXTENSIONS]) diff --git a/api/controllers/console/__init__.py b/api/controllers/console/__init__.py index b94a9f4ee4..d373d7c72f 100644 --- a/api/controllers/console/__init__.py +++ b/api/controllers/console/__init__.py @@ -84,6 +84,7 @@ from .datasets import ( external, hit_testing, metadata, + upload_file, website, ) from .datasets.rag_pipeline import ( diff --git a/api/controllers/console/app/statistic.py b/api/controllers/console/app/statistic.py index 32b64d10c5..343b7acd7b 100644 --- a/api/controllers/console/app/statistic.py +++ b/api/controllers/console/app/statistic.py @@ -67,7 +67,7 @@ WHERE response_data = [] with db.engine.begin() as conn: - rs = conn.execute(db.text(sql_query), arg_dict) + rs = conn.execute(sa.text(sql_query), arg_dict) for i in rs: response_data.append({"date": str(i.date), "message_count": i.message_count}) @@ -176,7 +176,7 @@ WHERE response_data = [] with db.engine.begin() as conn: - rs = conn.execute(db.text(sql_query), arg_dict) + rs = conn.execute(sa.text(sql_query), arg_dict) for i in rs: response_data.append({"date": str(i.date), "terminal_count": i.terminal_count}) @@ -234,7 +234,7 @@ WHERE response_data = [] with db.engine.begin() as conn: - rs = conn.execute(db.text(sql_query), arg_dict) + rs = conn.execute(sa.text(sql_query), arg_dict) for i in rs: response_data.append( {"date": str(i.date), "token_count": i.token_count, "total_price": i.total_price, "currency": "USD"} @@ -310,7 +310,7 @@ ORDER BY response_data = [] with db.engine.begin() as conn: - rs = conn.execute(db.text(sql_query), arg_dict) + rs = conn.execute(sa.text(sql_query), arg_dict) for i in rs: response_data.append( {"date": str(i.date), "interactions": float(i.interactions.quantize(Decimal("0.01")))} @@ -373,7 +373,7 @@ WHERE response_data = [] with db.engine.begin() as conn: - rs = conn.execute(db.text(sql_query), arg_dict) + rs = conn.execute(sa.text(sql_query), arg_dict) for i in rs: response_data.append( { @@ -435,7 +435,7 @@ WHERE response_data = [] with db.engine.begin() as conn: - rs = conn.execute(db.text(sql_query), arg_dict) + rs = conn.execute(sa.text(sql_query), arg_dict) for i in rs: response_data.append({"date": str(i.date), "latency": round(i.latency * 1000, 4)}) @@ -495,7 +495,7 @@ WHERE response_data = [] with db.engine.begin() as conn: - rs = conn.execute(db.text(sql_query), arg_dict) + rs = conn.execute(sa.text(sql_query), arg_dict) for i in rs: response_data.append({"date": str(i.date), "tps": round(i.tokens_per_second, 4)}) diff --git a/api/controllers/console/app/workflow_statistic.py b/api/controllers/console/app/workflow_statistic.py index 6c7c73707b..7f80afd83b 100644 --- a/api/controllers/console/app/workflow_statistic.py +++ b/api/controllers/console/app/workflow_statistic.py @@ -2,6 +2,7 @@ from datetime import datetime from decimal import Decimal import pytz +import sqlalchemy as sa from flask import jsonify from flask_login import current_user from flask_restful import Resource, reqparse @@ -71,7 +72,7 @@ WHERE response_data = [] with db.engine.begin() as conn: - rs = conn.execute(db.text(sql_query), arg_dict) + rs = conn.execute(sa.text(sql_query), arg_dict) for i in rs: response_data.append({"date": str(i.date), "runs": i.runs}) @@ -133,7 +134,7 @@ WHERE response_data = [] with db.engine.begin() as conn: - rs = conn.execute(db.text(sql_query), arg_dict) + rs = conn.execute(sa.text(sql_query), arg_dict) for i in rs: response_data.append({"date": str(i.date), "terminal_count": i.terminal_count}) @@ -195,7 +196,7 @@ WHERE response_data = [] with db.engine.begin() as conn: - rs = conn.execute(db.text(sql_query), arg_dict) + rs = conn.execute(sa.text(sql_query), arg_dict) for i in rs: response_data.append( { @@ -277,7 +278,7 @@ GROUP BY response_data = [] with db.engine.begin() as conn: - rs = conn.execute(db.text(sql_query), arg_dict) + rs = conn.execute(sa.text(sql_query), arg_dict) for i in rs: response_data.append( {"date": str(i.date), "interactions": float(i.interactions.quantize(Decimal("0.01")))} diff --git a/api/controllers/console/datasets/datasets_document.py b/api/controllers/console/datasets/datasets_document.py index 1ef490705b..36a5444825 100644 --- a/api/controllers/console/datasets/datasets_document.py +++ b/api/controllers/console/datasets/datasets_document.py @@ -645,7 +645,7 @@ class DocumentIndexingStatusApi(DocumentResource): return marshal(document_dict, document_status_fields) -class DocumentDetailApi(DocumentResource): +class DocumentApi(DocumentResource): METADATA_CHOICES = {"all", "only", "without"} @setup_required @@ -733,6 +733,28 @@ class DocumentDetailApi(DocumentResource): return response, 200 + @setup_required + @login_required + @account_initialization_required + @cloud_edition_billing_rate_limit_check("knowledge") + def delete(self, dataset_id, document_id): + dataset_id = str(dataset_id) + document_id = str(document_id) + dataset = DatasetService.get_dataset(dataset_id) + if dataset is None: + raise NotFound("Dataset not found.") + # check user's model setting + DatasetService.check_dataset_model_setting(dataset) + + document = self.get_document(dataset_id, document_id) + + try: + DocumentService.delete_document(document) + except services.errors.document.DocumentIndexingError: + raise DocumentIndexingError("Cannot delete document during indexing.") + + return {"result": "success"}, 204 + class DocumentProcessingApi(DocumentResource): @setup_required @@ -771,30 +793,6 @@ class DocumentProcessingApi(DocumentResource): return {"result": "success"}, 200 -class DocumentDeleteApi(DocumentResource): - @setup_required - @login_required - @account_initialization_required - @cloud_edition_billing_rate_limit_check("knowledge") - def delete(self, dataset_id, document_id): - dataset_id = str(dataset_id) - document_id = str(document_id) - dataset = DatasetService.get_dataset(dataset_id) - if dataset is None: - raise NotFound("Dataset not found.") - # check user's model setting - DatasetService.check_dataset_model_setting(dataset) - - document = self.get_document(dataset_id, document_id) - - try: - DocumentService.delete_document(document) - except services.errors.document.DocumentIndexingError: - raise DocumentIndexingError("Cannot delete document during indexing.") - - return {"result": "success"}, 204 - - class DocumentMetadataApi(DocumentResource): @setup_required @login_required @@ -1075,11 +1073,10 @@ api.add_resource( api.add_resource(DocumentBatchIndexingEstimateApi, "/datasets//batch//indexing-estimate") api.add_resource(DocumentBatchIndexingStatusApi, "/datasets//batch//indexing-status") api.add_resource(DocumentIndexingStatusApi, "/datasets//documents//indexing-status") -api.add_resource(DocumentDetailApi, "/datasets//documents/") +api.add_resource(DocumentApi, "/datasets//documents/") api.add_resource( DocumentProcessingApi, "/datasets//documents//processing/" ) -api.add_resource(DocumentDeleteApi, "/datasets//documents/") api.add_resource(DocumentMetadataApi, "/datasets//documents//metadata") api.add_resource(DocumentStatusApi, "/datasets//documents/status//batch") api.add_resource(DocumentPauseApi, "/datasets//documents//processing/pause") diff --git a/api/controllers/console/datasets/upload_file.py b/api/controllers/console/datasets/upload_file.py new file mode 100644 index 0000000000..9b456c771d --- /dev/null +++ b/api/controllers/console/datasets/upload_file.py @@ -0,0 +1,62 @@ +from flask_login import current_user +from flask_restful import Resource +from werkzeug.exceptions import NotFound + +from controllers.console import api +from controllers.console.wraps import ( + account_initialization_required, + setup_required, +) +from core.file import helpers as file_helpers +from extensions.ext_database import db +from models.dataset import Dataset +from models.model import UploadFile +from services.dataset_service import DocumentService + + +class UploadFileApi(Resource): + @setup_required + @account_initialization_required + def get(self, dataset_id, document_id): + """Get upload file.""" + # check dataset + dataset_id = str(dataset_id) + dataset = ( + db.session.query(Dataset) + .filter(Dataset.tenant_id == current_user.current_tenant_id, Dataset.id == dataset_id) + .first() + ) + if not dataset: + raise NotFound("Dataset not found.") + # check document + document_id = str(document_id) + document = DocumentService.get_document(dataset.id, document_id) + if not document: + raise NotFound("Document not found.") + # check upload file + if document.data_source_type != "upload_file": + raise ValueError(f"Document data source type ({document.data_source_type}) is not upload_file.") + data_source_info = document.data_source_info_dict + if data_source_info and "upload_file_id" in data_source_info: + file_id = data_source_info["upload_file_id"] + upload_file = db.session.query(UploadFile).filter(UploadFile.id == file_id).first() + if not upload_file: + raise NotFound("UploadFile not found.") + else: + raise ValueError("Upload file id not found in document data source info.") + + url = file_helpers.get_signed_file_url(upload_file_id=upload_file.id) + return { + "id": upload_file.id, + "name": upload_file.name, + "size": upload_file.size, + "extension": upload_file.extension, + "url": url, + "download_url": f"{url}&as_attachment=true", + "mime_type": upload_file.mime_type, + "created_by": upload_file.created_by, + "created_at": upload_file.created_at.timestamp(), + }, 200 + + +api.add_resource(UploadFileApi, "/datasets//documents//upload-file") diff --git a/api/controllers/console/error.py b/api/controllers/console/error.py index 6944c56bf8..0a4dfe1c10 100644 --- a/api/controllers/console/error.py +++ b/api/controllers/console/error.py @@ -127,7 +127,7 @@ class EducationActivateLimitError(BaseHTTPException): code = 429 -class CompilanceRateLimitError(BaseHTTPException): - error_code = "compilance_rate_limit" +class ComplianceRateLimitError(BaseHTTPException): + error_code = "compliance_rate_limit" description = "Rate limit exceeded for downloading compliance report." code = 429 diff --git a/api/controllers/service_api/app/completion.py b/api/controllers/service_api/app/completion.py index edc66cc5e9..ea57f04850 100644 --- a/api/controllers/service_api/app/completion.py +++ b/api/controllers/service_api/app/completion.py @@ -2,7 +2,7 @@ import logging from flask import request from flask_restful import Resource, reqparse -from werkzeug.exceptions import InternalServerError, NotFound +from werkzeug.exceptions import BadRequest, InternalServerError, NotFound import services from controllers.service_api import api @@ -30,6 +30,7 @@ from libs import helper from libs.helper import uuid_value from models.model import App, AppMode, EndUser from services.app_generate_service import AppGenerateService +from services.errors.app import IsDraftWorkflowError, WorkflowIdFormatError, WorkflowNotFoundError from services.errors.llm import InvokeRateLimitError @@ -113,7 +114,7 @@ class ChatApi(Resource): parser.add_argument("conversation_id", type=uuid_value, location="json") parser.add_argument("retriever_from", type=str, required=False, default="dev", location="json") parser.add_argument("auto_generate_name", type=bool, required=False, default=True, location="json") - + parser.add_argument("workflow_id", type=str, required=False, location="json") args = parser.parse_args() external_trace_id = get_external_trace_id(request) @@ -128,6 +129,12 @@ class ChatApi(Resource): ) return helper.compact_generate_response(response) + except WorkflowNotFoundError as ex: + raise NotFound(str(ex)) + except IsDraftWorkflowError as ex: + raise BadRequest(str(ex)) + except WorkflowIdFormatError as ex: + raise BadRequest(str(ex)) except services.errors.conversation.ConversationNotExistsError: raise NotFound("Conversation Not Exists.") except services.errors.conversation.ConversationCompletedError: diff --git a/api/controllers/service_api/app/conversation.py b/api/controllers/service_api/app/conversation.py index 36a7905572..79c860e6b8 100644 --- a/api/controllers/service_api/app/conversation.py +++ b/api/controllers/service_api/app/conversation.py @@ -1,7 +1,9 @@ +import json + from flask_restful import Resource, marshal_with, reqparse from flask_restful.inputs import int_range from sqlalchemy.orm import Session -from werkzeug.exceptions import NotFound +from werkzeug.exceptions import BadRequest, NotFound import services from controllers.service_api import api @@ -15,6 +17,7 @@ from fields.conversation_fields import ( simple_conversation_fields, ) from fields.conversation_variable_fields import ( + conversation_variable_fields, conversation_variable_infinite_scroll_pagination_fields, ) from libs.helper import uuid_value @@ -120,7 +123,41 @@ class ConversationVariablesApi(Resource): raise NotFound("Conversation Not Exists.") +class ConversationVariableDetailApi(Resource): + @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON)) + @marshal_with(conversation_variable_fields) + def put(self, app_model: App, end_user: EndUser, c_id, variable_id): + """Update a conversation variable's value""" + app_mode = AppMode.value_of(app_model.mode) + if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: + raise NotChatAppError() + + conversation_id = str(c_id) + variable_id = str(variable_id) + + parser = reqparse.RequestParser() + parser.add_argument("value", required=True, location="json") + args = parser.parse_args() + + try: + return ConversationService.update_conversation_variable( + app_model, conversation_id, variable_id, end_user, json.loads(args["value"]) + ) + except services.errors.conversation.ConversationNotExistsError: + raise NotFound("Conversation Not Exists.") + except services.errors.conversation.ConversationVariableNotExistsError: + raise NotFound("Conversation Variable Not Exists.") + except services.errors.conversation.ConversationVariableTypeMismatchError as e: + raise BadRequest(str(e)) + + api.add_resource(ConversationRenameApi, "/conversations//name", endpoint="conversation_name") api.add_resource(ConversationApi, "/conversations") api.add_resource(ConversationDetailApi, "/conversations/", endpoint="conversation_detail") api.add_resource(ConversationVariablesApi, "/conversations//variables", endpoint="conversation_variables") +api.add_resource( + ConversationVariableDetailApi, + "/conversations//variables/", + endpoint="conversation_variable_detail", + methods=["PUT"], +) diff --git a/api/controllers/service_api/app/workflow.py b/api/controllers/service_api/app/workflow.py index 370ff911b4..cd8a5f03ac 100644 --- a/api/controllers/service_api/app/workflow.py +++ b/api/controllers/service_api/app/workflow.py @@ -5,7 +5,7 @@ from flask import request from flask_restful import Resource, fields, marshal_with, reqparse from flask_restful.inputs import int_range from sqlalchemy.orm import Session, sessionmaker -from werkzeug.exceptions import InternalServerError +from werkzeug.exceptions import BadRequest, InternalServerError, NotFound from controllers.service_api import api from controllers.service_api.app.error import ( @@ -34,6 +34,7 @@ from libs.helper import TimestampField from models.model import App, AppMode, EndUser from repositories.factory import DifyAPIRepositoryFactory from services.app_generate_service import AppGenerateService +from services.errors.app import IsDraftWorkflowError, WorkflowIdFormatError, WorkflowNotFoundError from services.errors.llm import InvokeRateLimitError from services.workflow_app_service import WorkflowAppService @@ -120,6 +121,59 @@ class WorkflowRunApi(Resource): raise InternalServerError() +class WorkflowRunByIdApi(Resource): + @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True)) + def post(self, app_model: App, end_user: EndUser, workflow_id: str): + """ + Run specific workflow by ID + """ + app_mode = AppMode.value_of(app_model.mode) + if app_mode != AppMode.WORKFLOW: + raise NotWorkflowAppError() + + parser = reqparse.RequestParser() + parser.add_argument("inputs", type=dict, required=True, nullable=False, location="json") + parser.add_argument("files", type=list, required=False, location="json") + parser.add_argument("response_mode", type=str, choices=["blocking", "streaming"], location="json") + args = parser.parse_args() + + # Add workflow_id to args for AppGenerateService + args["workflow_id"] = workflow_id + + external_trace_id = get_external_trace_id(request) + if external_trace_id: + args["external_trace_id"] = external_trace_id + streaming = args.get("response_mode") == "streaming" + + try: + response = AppGenerateService.generate( + app_model=app_model, user=end_user, args=args, invoke_from=InvokeFrom.SERVICE_API, streaming=streaming + ) + + return helper.compact_generate_response(response) + except WorkflowNotFoundError as ex: + raise NotFound(str(ex)) + except IsDraftWorkflowError as ex: + raise BadRequest(str(ex)) + except WorkflowIdFormatError as ex: + raise BadRequest(str(ex)) + except ProviderTokenNotInitError as ex: + raise ProviderNotInitializeError(ex.description) + except QuotaExceededError: + raise ProviderQuotaExceededError() + except ModelCurrentlyNotSupportError: + raise ProviderModelCurrentlyNotSupportError() + except InvokeRateLimitError as ex: + raise InvokeRateLimitHttpError(ex.description) + except InvokeError as e: + raise CompletionRequestError(e.description) + except ValueError as e: + raise e + except Exception: + logging.exception("internal server error.") + raise InternalServerError() + + class WorkflowTaskStopApi(Resource): @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True)) def post(self, app_model: App, end_user: EndUser, task_id: str): @@ -193,5 +247,6 @@ class WorkflowAppLogApi(Resource): api.add_resource(WorkflowRunApi, "/workflows/run") api.add_resource(WorkflowRunDetailApi, "/workflows/run/") +api.add_resource(WorkflowRunByIdApi, "/workflows//run") api.add_resource(WorkflowTaskStopApi, "/workflows/tasks//stop") api.add_resource(WorkflowAppLogApi, "/workflows/logs") diff --git a/api/controllers/service_api/dataset/document.py b/api/controllers/service_api/dataset/document.py index ac85c0b38d..77600aa18c 100644 --- a/api/controllers/service_api/dataset/document.py +++ b/api/controllers/service_api/dataset/document.py @@ -358,39 +358,6 @@ class DocumentUpdateByFileApi(DatasetApiResource): return documents_and_batch_fields, 200 -class DocumentDeleteApi(DatasetApiResource): - @cloud_edition_billing_rate_limit_check("knowledge", "dataset") - def delete(self, tenant_id, dataset_id, document_id): - """Delete document.""" - document_id = str(document_id) - dataset_id = str(dataset_id) - tenant_id = str(tenant_id) - - # get dataset info - dataset = db.session.query(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first() - - if not dataset: - raise ValueError("Dataset does not exist.") - - document = DocumentService.get_document(dataset.id, document_id) - - # 404 if document not found - if document is None: - raise NotFound("Document Not Exists.") - - # 403 if document is archived - if DocumentService.check_archived(document): - raise ArchivedDocumentImmutableError() - - try: - # delete document - DocumentService.delete_document(document) - except services.errors.document.DocumentIndexingError: - raise DocumentIndexingError("Cannot delete document during indexing.") - - return 204 - - class DocumentListApi(DatasetApiResource): def get(self, tenant_id, dataset_id): dataset_id = str(dataset_id) @@ -473,7 +440,7 @@ class DocumentIndexingStatusApi(DatasetApiResource): return data -class DocumentDetailApi(DatasetApiResource): +class DocumentApi(DatasetApiResource): METADATA_CHOICES = {"all", "only", "without"} def get(self, tenant_id, dataset_id, document_id): @@ -567,6 +534,37 @@ class DocumentDetailApi(DatasetApiResource): return response + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") + def delete(self, tenant_id, dataset_id, document_id): + """Delete document.""" + document_id = str(document_id) + dataset_id = str(dataset_id) + tenant_id = str(tenant_id) + + # get dataset info + dataset = db.session.query(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first() + + if not dataset: + raise ValueError("Dataset does not exist.") + + document = DocumentService.get_document(dataset.id, document_id) + + # 404 if document not found + if document is None: + raise NotFound("Document Not Exists.") + + # 403 if document is archived + if DocumentService.check_archived(document): + raise ArchivedDocumentImmutableError() + + try: + # delete document + DocumentService.delete_document(document) + except services.errors.document.DocumentIndexingError: + raise DocumentIndexingError("Cannot delete document during indexing.") + + return 204 + api.add_resource( DocumentAddByTextApi, @@ -588,7 +586,6 @@ api.add_resource( "/datasets//documents//update_by_file", "/datasets//documents//update-by-file", ) -api.add_resource(DocumentDeleteApi, "/datasets//documents/") +api.add_resource(DocumentApi, "/datasets//documents/") api.add_resource(DocumentListApi, "/datasets//documents") api.add_resource(DocumentIndexingStatusApi, "/datasets//documents//indexing-status") -api.add_resource(DocumentDetailApi, "/datasets//documents/") diff --git a/api/core/entities/provider_entities.py b/api/core/entities/provider_entities.py index 2a0751a5ee..a5a6e62bd7 100644 --- a/api/core/entities/provider_entities.py +++ b/api/core/entities/provider_entities.py @@ -176,7 +176,7 @@ class ProviderConfig(BasicProviderConfig): scope: AppSelectorScope | ModelSelectorScope | ToolSelectorScope | None = None required: bool = False - default: Optional[Union[int, str]] = None + default: Optional[Union[int, str, float, bool]] = None options: Optional[list[Option]] = None label: Optional[I18nObject] = None help: Optional[I18nObject] = None diff --git a/api/core/file/file_manager.py b/api/core/file/file_manager.py index 80ff5f693c..e342aca4cd 100644 --- a/api/core/file/file_manager.py +++ b/api/core/file/file_manager.py @@ -32,7 +32,7 @@ def get_attr(*, file: File, attr: FileAttribute): case FileAttribute.TRANSFER_METHOD: return file.transfer_method.value case FileAttribute.URL: - return file.remote_url + return _to_url(file) case FileAttribute.EXTENSION: return file.extension case FileAttribute.RELATED_ID: diff --git a/api/core/ops/ops_trace_manager.py b/api/core/ops/ops_trace_manager.py index b769934a9b..7eb5da7e3a 100644 --- a/api/core/ops/ops_trace_manager.py +++ b/api/core/ops/ops_trace_manager.py @@ -322,7 +322,7 @@ class OpsTraceManager: :return: """ # auth check - if enabled == True: + if enabled: try: provider_config_map[tracing_provider] except KeyError: diff --git a/api/core/rag/datasource/vdb/elasticsearch/elasticsearch_vector.py b/api/core/rag/datasource/vdb/elasticsearch/elasticsearch_vector.py index 9dea050dc3..49c4b392fe 100644 --- a/api/core/rag/datasource/vdb/elasticsearch/elasticsearch_vector.py +++ b/api/core/rag/datasource/vdb/elasticsearch/elasticsearch_vector.py @@ -7,6 +7,7 @@ from urllib.parse import urlparse import requests from elasticsearch import Elasticsearch from flask import current_app +from packaging.version import parse as parse_version from pydantic import BaseModel, model_validator from core.rag.datasource.vdb.field import Field @@ -149,7 +150,7 @@ class ElasticSearchVector(BaseVector): return cast(str, info["version"]["number"]) def _check_version(self): - if self._version < "8.0.0": + if parse_version(self._version) < parse_version("8.0.0"): raise ValueError("Elasticsearch vector database version must be greater than 8.0.0") def get_type(self) -> str: diff --git a/api/core/rag/datasource/vdb/tablestore/tablestore_vector.py b/api/core/rag/datasource/vdb/tablestore/tablestore_vector.py index 784e27fc7f..91d667ff2c 100644 --- a/api/core/rag/datasource/vdb/tablestore/tablestore_vector.py +++ b/api/core/rag/datasource/vdb/tablestore/tablestore_vector.py @@ -1,5 +1,6 @@ import json import logging +import math from typing import Any, Optional import tablestore # type: ignore @@ -22,6 +23,7 @@ class TableStoreConfig(BaseModel): access_key_secret: Optional[str] = None instance_name: Optional[str] = None endpoint: Optional[str] = None + normalize_full_text_bm25_score: Optional[bool] = False @model_validator(mode="before") @classmethod @@ -47,6 +49,7 @@ class TableStoreVector(BaseVector): config.access_key_secret, config.instance_name, ) + self._normalize_full_text_bm25_score = config.normalize_full_text_bm25_score self._table_name = f"{collection_name}" self._index_name = f"{collection_name}_idx" self._tags_field = f"{Field.METADATA_KEY.value}_tags" @@ -131,8 +134,8 @@ class TableStoreVector(BaseVector): filtered_list = None if document_ids_filter: filtered_list = ["document_id=" + item for item in document_ids_filter] - - return self._search_by_full_text(query, filtered_list, top_k) + score_threshold = float(kwargs.get("score_threshold") or 0.0) + return self._search_by_full_text(query, filtered_list, top_k, score_threshold) def delete(self) -> None: self._delete_table_if_exist() @@ -318,7 +321,19 @@ class TableStoreVector(BaseVector): documents = sorted(documents, key=lambda x: x.metadata["score"] if x.metadata else 0, reverse=True) return documents - def _search_by_full_text(self, query: str, document_ids_filter: list[str] | None, top_k: int) -> list[Document]: + @staticmethod + def _normalize_score_exp_decay(score: float, k: float = 0.15) -> float: + """ + Args: + score: BM25 search score. + k: decay factor, the larger the k, the steeper the low score end + """ + normalized_score = 1 - math.exp(-k * score) + return max(0.0, min(1.0, normalized_score)) + + def _search_by_full_text( + self, query: str, document_ids_filter: list[str] | None, top_k: int, score_threshold: float + ) -> list[Document]: bool_query = tablestore.BoolQuery(must_queries=[], filter_queries=[], should_queries=[], must_not_queries=[]) bool_query.must_queries.append(tablestore.MatchQuery(text=query, field_name=Field.CONTENT_KEY.value)) @@ -339,15 +354,27 @@ class TableStoreVector(BaseVector): documents = [] for search_hit in search_response.search_hits: + score = None + if self._normalize_full_text_bm25_score: + score = self._normalize_score_exp_decay(search_hit.score) + + # skip when score is below threshold and use normalize score + if score and score <= score_threshold: + continue + ots_column_map = {} for col in search_hit.row[1]: ots_column_map[col[0]] = col[1] - vector_str = ots_column_map.get(Field.VECTOR.value) metadata_str = ots_column_map.get(Field.METADATA_KEY.value) - vector = json.loads(vector_str) if vector_str else None metadata = json.loads(metadata_str) if metadata_str else {} + vector_str = ots_column_map.get(Field.VECTOR.value) + vector = json.loads(vector_str) if vector_str else None + + if score: + metadata["score"] = score + documents.append( Document( page_content=ots_column_map.get(Field.CONTENT_KEY.value) or "", @@ -355,6 +382,8 @@ class TableStoreVector(BaseVector): metadata=metadata, ) ) + if self._normalize_full_text_bm25_score: + documents = sorted(documents, key=lambda x: x.metadata["score"] if x.metadata else 0, reverse=True) return documents @@ -375,5 +404,6 @@ class TableStoreVectorFactory(AbstractVectorFactory): instance_name=dify_config.TABLESTORE_INSTANCE_NAME, access_key_id=dify_config.TABLESTORE_ACCESS_KEY_ID, access_key_secret=dify_config.TABLESTORE_ACCESS_KEY_SECRET, + normalize_full_text_bm25_score=dify_config.TABLESTORE_NORMALIZE_FULLTEXT_BM25_SCORE, ), ) diff --git a/api/core/tools/builtin_tool/providers/time/tools/timezone_conversion.py b/api/core/tools/builtin_tool/providers/time/tools/timezone_conversion.py index f9b776b3b9..91316b859a 100644 --- a/api/core/tools/builtin_tool/providers/time/tools/timezone_conversion.py +++ b/api/core/tools/builtin_tool/providers/time/tools/timezone_conversion.py @@ -27,7 +27,7 @@ class TimezoneConversionTool(BuiltinTool): target_time = self.timezone_convert(current_time, current_timezone, target_timezone) # type: ignore if not target_time: yield self.create_text_message( - f"Invalid datatime and timezone: {current_time},{current_timezone},{target_timezone}" + f"Invalid datetime and timezone: {current_time},{current_timezone},{target_timezone}" ) return diff --git a/api/core/tools/tool_manager.py b/api/core/tools/tool_manager.py index 1bb4cfa4cd..2737bcfb16 100644 --- a/api/core/tools/tool_manager.py +++ b/api/core/tools/tool_manager.py @@ -7,6 +7,7 @@ from os import listdir, path from threading import Lock from typing import TYPE_CHECKING, Any, Literal, Optional, Union, cast +import sqlalchemy as sa from pydantic import TypeAdapter from yarl import URL @@ -616,7 +617,7 @@ class ToolManager: WHERE tenant_id = :tenant_id ORDER BY tenant_id, provider, is_default DESC, created_at DESC """ - ids = [row.id for row in db.session.execute(db.text(sql), {"tenant_id": tenant_id}).all()] + ids = [row.id for row in db.session.execute(sa.text(sql), {"tenant_id": tenant_id}).all()] return db.session.query(BuiltinToolProvider).where(BuiltinToolProvider.id.in_(ids)).all() @classmethod diff --git a/api/core/variables/types.py b/api/core/variables/types.py index e79b2410bf..d28fb11401 100644 --- a/api/core/variables/types.py +++ b/api/core/variables/types.py @@ -109,7 +109,7 @@ class SegmentType(StrEnum): elif array_validation == ArrayValidation.FIRST: return element_type.is_valid(value[0]) else: - return all([element_type.is_valid(i, array_validation=ArrayValidation.NONE)] for i in value) + return all(element_type.is_valid(i, array_validation=ArrayValidation.NONE) for i in value) def is_valid(self, value: Any, array_validation: ArrayValidation = ArrayValidation.FIRST) -> bool: """ @@ -152,7 +152,7 @@ class SegmentType(StrEnum): _ARRAY_ELEMENT_TYPES_MAPPING: Mapping[SegmentType, SegmentType] = { - # ARRAY_ANY does not have correpond element type. + # ARRAY_ANY does not have corresponding element type. SegmentType.ARRAY_STRING: SegmentType.STRING, SegmentType.ARRAY_NUMBER: SegmentType.NUMBER, SegmentType.ARRAY_OBJECT: SegmentType.OBJECT, diff --git a/api/core/workflow/nodes/document_extractor/node.py b/api/core/workflow/nodes/document_extractor/node.py index f3061f7d96..23512c8ce4 100644 --- a/api/core/workflow/nodes/document_extractor/node.py +++ b/api/core/workflow/nodes/document_extractor/node.py @@ -597,7 +597,7 @@ def _extract_text_from_vtt(vtt_bytes: bytes) -> str: for i in range(1, len(raw_results)): spk, txt = raw_results[i] - if spk == None: + if spk is None: merged_results.append((None, current_text)) continue diff --git a/api/core/workflow/nodes/http_request/executor.py b/api/core/workflow/nodes/http_request/executor.py index fe103c7117..2106369bd6 100644 --- a/api/core/workflow/nodes/http_request/executor.py +++ b/api/core/workflow/nodes/http_request/executor.py @@ -277,6 +277,22 @@ class Executor: elif self.auth.config.type == "custom": headers[authorization.config.header] = authorization.config.api_key or "" + # Handle Content-Type for multipart/form-data requests + # Fix for issue #22880: Missing boundary when using multipart/form-data + body = self.node_data.body + if body and body.type == "form-data": + # For multipart/form-data with files, let httpx handle the boundary automatically + # by not setting Content-Type header when files are present + if not self.files or all(f[0] == "__multipart_placeholder__" for f in self.files): + # Only set Content-Type when there are no actual files + # This ensures httpx generates the correct boundary + if "content-type" not in (k.lower() for k in headers): + headers["Content-Type"] = "multipart/form-data" + elif body and body.type in BODY_TYPE_TO_CONTENT_TYPE: + # Set Content-Type for other body types + if "content-type" not in (k.lower() for k in headers): + headers["Content-Type"] = BODY_TYPE_TO_CONTENT_TYPE[body.type] + return headers def _validate_and_parse_response(self, response: httpx.Response) -> Response: @@ -384,15 +400,24 @@ class Executor: # '__multipart_placeholder__' is inserted to force multipart encoding but is not a real file. # This prevents logging meaningless placeholder entries. if self.files and not all(f[0] == "__multipart_placeholder__" for f in self.files): - for key, (filename, content, mime_type) in self.files: + for file_entry in self.files: + # file_entry should be (key, (filename, content, mime_type)), but handle edge cases + if len(file_entry) != 2 or not isinstance(file_entry[1], tuple) or len(file_entry[1]) < 2: + continue # skip malformed entries + key = file_entry[0] + content = file_entry[1][1] body_string += f"--{boundary}\r\n" body_string += f'Content-Disposition: form-data; name="{key}"\r\n\r\n' - # decode content - try: - body_string += content.decode("utf-8") - except UnicodeDecodeError: - # fix: decode binary content - pass + # decode content safely + if isinstance(content, bytes): + try: + body_string += content.decode("utf-8") + except UnicodeDecodeError: + body_string += content.decode("utf-8", errors="replace") + elif isinstance(content, str): + body_string += content + else: + body_string += f"[Unsupported content type: {type(content).__name__}]" body_string += "\r\n" body_string += f"--{boundary}--\r\n" elif self.node_data.body: diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index 90a0397b67..dfc2a0000b 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -3,7 +3,7 @@ import io import json import logging from collections.abc import Generator, Mapping, Sequence -from typing import TYPE_CHECKING, Any, Optional, cast +from typing import TYPE_CHECKING, Any, Optional from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity from core.file import FileType, file_manager @@ -33,12 +33,10 @@ from core.model_runtime.entities.message_entities import ( UserPromptMessage, ) from core.model_runtime.entities.model_entities import ( - AIModelEntity, ModelFeature, ModelPropertyKey, ModelType, ) -from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from core.model_runtime.utils.encoders import jsonable_encoder from core.prompt.entities.advanced_prompt_entities import CompletionModelPromptTemplate, MemoryConfig from core.prompt.utils.prompt_message_util import PromptMessageUtil @@ -1006,21 +1004,6 @@ class LLMNode(BaseNode): ) return saved_file - def _fetch_model_schema(self, provider: str) -> AIModelEntity | None: - """ - Fetch model schema - """ - model_name = self._node_data.model.name - model_manager = ModelManager() - model_instance = model_manager.get_model_instance( - tenant_id=self.tenant_id, model_type=ModelType.LLM, provider=provider, model=model_name - ) - model_type_instance = model_instance.model_type_instance - model_type_instance = cast(LargeLanguageModel, model_type_instance) - model_credentials = model_instance.credentials - model_schema = model_type_instance.get_model_schema(model_name, model_credentials) - return model_schema - @staticmethod def fetch_structured_output_schema( *, diff --git a/api/core/workflow/nodes/tool/tool_node.py b/api/core/workflow/nodes/tool/tool_node.py index 4c8e13de70..df89b2476d 100644 --- a/api/core/workflow/nodes/tool/tool_node.py +++ b/api/core/workflow/nodes/tool/tool_node.py @@ -318,6 +318,33 @@ 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"]) diff --git a/api/factories/file_factory.py b/api/factories/file_factory.py index 9836113077..a4f65a9c9b 100644 --- a/api/factories/file_factory.py +++ b/api/factories/file_factory.py @@ -1,4 +1,6 @@ import mimetypes +import os +import urllib.parse import uuid from collections.abc import Callable, Mapping, Sequence from typing import Any, cast @@ -241,16 +243,21 @@ def _build_from_remote_url( def _get_remote_file_info(url: str): file_size = -1 - filename = url.split("/")[-1].split("?")[0] or "unknown_file" - mime_type = mimetypes.guess_type(filename)[0] or "" + parsed_url = urllib.parse.urlparse(url) + url_path = parsed_url.path + filename = os.path.basename(url_path) + + # Initialize mime_type from filename as fallback + mime_type, _ = mimetypes.guess_type(filename) resp = ssrf_proxy.head(url, follow_redirects=True) resp = cast(httpx.Response, resp) if resp.status_code == httpx.codes.OK: if content_disposition := resp.headers.get("Content-Disposition"): filename = str(content_disposition.split("filename=")[-1].strip('"')) + # Re-guess mime_type from updated filename + mime_type, _ = mimetypes.guess_type(filename) file_size = int(resp.headers.get("Content-Length", file_size)) - mime_type = mime_type or str(resp.headers.get("Content-Type", "")) return mime_type, filename, file_size diff --git a/api/fields/app_fields.py b/api/fields/app_fields.py index b6d85e0e24..1a5fcabf97 100644 --- a/api/fields/app_fields.py +++ b/api/fields/app_fields.py @@ -59,6 +59,8 @@ model_config_fields = { "updated_at": TimestampField, } +tag_fields = {"id": fields.String, "name": fields.String, "type": fields.String} + app_detail_fields = { "id": fields.String, "name": fields.String, @@ -77,6 +79,7 @@ app_detail_fields = { "updated_by": fields.String, "updated_at": TimestampField, "access_mode": fields.String, + "tags": fields.List(fields.Nested(tag_fields)), } prompt_config_fields = { @@ -92,8 +95,6 @@ model_config_partial_fields = { "updated_at": TimestampField, } -tag_fields = {"id": fields.String, "name": fields.String, "type": fields.String} - app_partial_fields = { "id": fields.String, "name": fields.String, @@ -185,7 +186,6 @@ app_detail_fields_with_site = { "enable_api": fields.Boolean, "model_config": fields.Nested(model_config_fields, attribute="app_model_config", allow_null=True), "workflow": fields.Nested(workflow_partial_fields, allow_null=True), - "site": fields.Nested(site_fields), "api_base_url": fields.String, "use_icon_as_answer_icon": fields.Boolean, "max_active_requests": fields.Integer, @@ -195,6 +195,8 @@ app_detail_fields_with_site = { "updated_at": TimestampField, "deleted_tools": fields.List(fields.Nested(deleted_tool_fields)), "access_mode": fields.String, + "tags": fields.List(fields.Nested(tag_fields)), + "site": fields.Nested(site_fields), } diff --git a/api/migrations/versions/2025_07_24_1450-532b3f888abf_manual_dataset_field_update.py b/api/migrations/versions/2025_07_24_1450-532b3f888abf_manual_dataset_field_update.py new file mode 100644 index 0000000000..1664fb99c4 --- /dev/null +++ b/api/migrations/versions/2025_07_24_1450-532b3f888abf_manual_dataset_field_update.py @@ -0,0 +1,25 @@ +"""manual dataset field update + +Revision ID: 532b3f888abf +Revises: 8bcc02c9bd07 +Create Date: 2025-07-24 14:50:48.779833 + +""" +from alembic import op +import models as models +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '532b3f888abf' +down_revision = '8bcc02c9bd07' +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute("ALTER TABLE tidb_auth_bindings ALTER COLUMN status SET DEFAULT 'CREATING'::character varying") + + +def downgrade(): + op.execute("ALTER TABLE tidb_auth_bindings ALTER COLUMN status SET DEFAULT 'CREATING'") diff --git a/api/models/account.py b/api/models/account.py index d63c5d7fb5..1a0752440d 100644 --- a/api/models/account.py +++ b/api/models/account.py @@ -3,8 +3,9 @@ import json from datetime import datetime from typing import Optional, cast +import sqlalchemy as sa from flask_login import UserMixin # type: ignore -from sqlalchemy import func, select +from sqlalchemy import DateTime, String, func, select from sqlalchemy.orm import Mapped, mapped_column, reconstructor from models.base import Base @@ -83,26 +84,24 @@ class AccountStatus(enum.StrEnum): class Account(UserMixin, Base): __tablename__ = "accounts" - __table_args__ = (db.PrimaryKeyConstraint("id", name="account_pkey"), db.Index("account_email_idx", "email")) + __table_args__ = (sa.PrimaryKeyConstraint("id", name="account_pkey"), sa.Index("account_email_idx", "email")) - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) - name: Mapped[str] = mapped_column(db.String(255)) - email: Mapped[str] = mapped_column(db.String(255)) - password: Mapped[Optional[str]] = mapped_column(db.String(255)) - password_salt: Mapped[Optional[str]] = mapped_column(db.String(255)) - avatar: Mapped[Optional[str]] = mapped_column(db.String(255), nullable=True) - interface_language: Mapped[Optional[str]] = mapped_column(db.String(255)) - interface_theme: Mapped[Optional[str]] = mapped_column(db.String(255), nullable=True) - timezone: Mapped[Optional[str]] = mapped_column(db.String(255)) - last_login_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime, nullable=True) - last_login_ip: Mapped[Optional[str]] = mapped_column(db.String(255), nullable=True) - last_active_at: Mapped[datetime] = mapped_column( - db.DateTime, server_default=func.current_timestamp(), nullable=False - ) - status: Mapped[str] = mapped_column(db.String(16), server_default=db.text("'active'::character varying")) - initialized_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime, nullable=True) - created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp(), nullable=False) - updated_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp(), nullable=False) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) + name: Mapped[str] = mapped_column(String(255)) + email: Mapped[str] = mapped_column(String(255)) + password: Mapped[Optional[str]] = mapped_column(String(255)) + password_salt: Mapped[Optional[str]] = mapped_column(String(255)) + avatar: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + interface_language: Mapped[Optional[str]] = mapped_column(String(255)) + interface_theme: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + timezone: Mapped[Optional[str]] = mapped_column(String(255)) + last_login_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + last_login_ip: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + last_active_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.current_timestamp(), nullable=False) + status: Mapped[str] = mapped_column(String(16), server_default=sa.text("'active'::character varying")) + initialized_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.current_timestamp(), nullable=False) + updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.current_timestamp(), nullable=False) @reconstructor def init_on_load(self): @@ -197,16 +196,16 @@ class TenantStatus(enum.StrEnum): class Tenant(Base): __tablename__ = "tenants" - __table_args__ = (db.PrimaryKeyConstraint("id", name="tenant_pkey"),) + __table_args__ = (sa.PrimaryKeyConstraint("id", name="tenant_pkey"),) - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) - name: Mapped[str] = mapped_column(db.String(255)) - encrypt_public_key = db.Column(db.Text) - plan: Mapped[str] = mapped_column(db.String(255), server_default=db.text("'basic'::character varying")) - status: Mapped[str] = mapped_column(db.String(255), server_default=db.text("'normal'::character varying")) - custom_config: Mapped[Optional[str]] = mapped_column(db.Text) - created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp(), nullable=False) - updated_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp()) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) + name: Mapped[str] = mapped_column(String(255)) + encrypt_public_key = db.Column(sa.Text) + plan: Mapped[str] = mapped_column(String(255), server_default=sa.text("'basic'::character varying")) + status: Mapped[str] = mapped_column(String(255), server_default=sa.text("'normal'::character varying")) + custom_config: Mapped[Optional[str]] = mapped_column(sa.Text) + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.current_timestamp(), nullable=False) + updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.current_timestamp()) def get_accounts(self) -> list[Account]: return ( @@ -227,56 +226,56 @@ class Tenant(Base): class TenantAccountJoin(Base): __tablename__ = "tenant_account_joins" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="tenant_account_join_pkey"), - db.Index("tenant_account_join_account_id_idx", "account_id"), - db.Index("tenant_account_join_tenant_id_idx", "tenant_id"), - db.UniqueConstraint("tenant_id", "account_id", name="unique_tenant_account_join"), + sa.PrimaryKeyConstraint("id", name="tenant_account_join_pkey"), + sa.Index("tenant_account_join_account_id_idx", "account_id"), + sa.Index("tenant_account_join_tenant_id_idx", "tenant_id"), + sa.UniqueConstraint("tenant_id", "account_id", name="unique_tenant_account_join"), ) - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id: Mapped[str] = mapped_column(StringUUID) account_id: Mapped[str] = mapped_column(StringUUID) - current: Mapped[bool] = mapped_column(db.Boolean, server_default=db.text("false")) - role: Mapped[str] = mapped_column(db.String(16), server_default="normal") + current: Mapped[bool] = mapped_column(sa.Boolean, server_default=sa.text("false")) + role: Mapped[str] = mapped_column(String(16), server_default="normal") invited_by: Mapped[Optional[str]] = mapped_column(StringUUID) - created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp()) - updated_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp()) + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.current_timestamp()) + updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.current_timestamp()) class AccountIntegrate(Base): __tablename__ = "account_integrates" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="account_integrate_pkey"), - db.UniqueConstraint("account_id", "provider", name="unique_account_provider"), - db.UniqueConstraint("provider", "open_id", name="unique_provider_open_id"), + sa.PrimaryKeyConstraint("id", name="account_integrate_pkey"), + sa.UniqueConstraint("account_id", "provider", name="unique_account_provider"), + sa.UniqueConstraint("provider", "open_id", name="unique_provider_open_id"), ) - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) account_id: Mapped[str] = mapped_column(StringUUID) - provider: Mapped[str] = mapped_column(db.String(16)) - open_id: Mapped[str] = mapped_column(db.String(255)) - encrypted_token: Mapped[str] = mapped_column(db.String(255)) - created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp()) - updated_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp()) + provider: Mapped[str] = mapped_column(String(16)) + open_id: Mapped[str] = mapped_column(String(255)) + encrypted_token: Mapped[str] = mapped_column(String(255)) + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.current_timestamp()) + updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.current_timestamp()) class InvitationCode(Base): __tablename__ = "invitation_codes" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="invitation_code_pkey"), - db.Index("invitation_codes_batch_idx", "batch"), - db.Index("invitation_codes_code_idx", "code", "status"), + sa.PrimaryKeyConstraint("id", name="invitation_code_pkey"), + sa.Index("invitation_codes_batch_idx", "batch"), + sa.Index("invitation_codes_code_idx", "code", "status"), ) - id: Mapped[int] = mapped_column(db.Integer) - batch: Mapped[str] = mapped_column(db.String(255)) - code: Mapped[str] = mapped_column(db.String(32)) - status: Mapped[str] = mapped_column(db.String(16), server_default=db.text("'unused'::character varying")) - used_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime) + id: Mapped[int] = mapped_column(sa.Integer) + batch: Mapped[str] = mapped_column(String(255)) + code: Mapped[str] = mapped_column(String(32)) + status: Mapped[str] = mapped_column(String(16), server_default=sa.text("'unused'::character varying")) + used_at: Mapped[Optional[datetime]] = mapped_column(DateTime) used_by_tenant_id: Mapped[Optional[str]] = mapped_column(StringUUID) used_by_account_id: Mapped[Optional[str]] = mapped_column(StringUUID) - deprecated_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime, nullable=True) - created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=db.text("CURRENT_TIMESTAMP(0)")) + deprecated_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=sa.text("CURRENT_TIMESTAMP(0)")) class TenantPluginPermission(Base): @@ -292,16 +291,14 @@ class TenantPluginPermission(Base): __tablename__ = "account_plugin_permissions" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="account_plugin_permission_pkey"), - db.UniqueConstraint("tenant_id", name="unique_tenant_plugin"), + sa.PrimaryKeyConstraint("id", name="account_plugin_permission_pkey"), + sa.UniqueConstraint("tenant_id", name="unique_tenant_plugin"), ) - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) - install_permission: Mapped[InstallPermission] = mapped_column( - db.String(16), nullable=False, server_default="everyone" - ) - debug_permission: Mapped[DebugPermission] = mapped_column(db.String(16), nullable=False, server_default="noone") + install_permission: Mapped[InstallPermission] = mapped_column(String(16), nullable=False, server_default="everyone") + debug_permission: Mapped[DebugPermission] = mapped_column(String(16), nullable=False, server_default="noone") class TenantPluginAutoUpgradeStrategy(Base): @@ -317,20 +314,16 @@ class TenantPluginAutoUpgradeStrategy(Base): __tablename__ = "tenant_plugin_auto_upgrade_strategies" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="tenant_plugin_auto_upgrade_strategy_pkey"), - db.UniqueConstraint("tenant_id", name="unique_tenant_plugin_auto_upgrade_strategy"), + sa.PrimaryKeyConstraint("id", name="tenant_plugin_auto_upgrade_strategy_pkey"), + sa.UniqueConstraint("tenant_id", name="unique_tenant_plugin_auto_upgrade_strategy"), ) - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) - strategy_setting: Mapped[StrategySetting] = mapped_column(db.String(16), nullable=False, server_default="fix_only") - upgrade_time_of_day: Mapped[int] = mapped_column(db.Integer, nullable=False, default=0) # seconds of the day - upgrade_mode: Mapped[UpgradeMode] = mapped_column(db.String(16), nullable=False, server_default="exclude") - exclude_plugins: Mapped[list[str]] = mapped_column( - db.ARRAY(db.String(255)), nullable=False - ) # plugin_id (author/name) - include_plugins: Mapped[list[str]] = mapped_column( - db.ARRAY(db.String(255)), nullable=False - ) # plugin_id (author/name) - created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + strategy_setting: Mapped[StrategySetting] = mapped_column(String(16), nullable=False, server_default="fix_only") + upgrade_time_of_day: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0) # seconds of the day + upgrade_mode: Mapped[UpgradeMode] = mapped_column(String(16), nullable=False, server_default="exclude") + exclude_plugins: Mapped[list[str]] = mapped_column(sa.ARRAY(String(255)), nullable=False) # plugin_id (author/name) + include_plugins: Mapped[list[str]] = mapped_column(sa.ARRAY(String(255)), nullable=False) # plugin_id (author/name) + created_at = db.Column(DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = db.Column(DateTime, nullable=False, server_default=func.current_timestamp()) diff --git a/api/models/api_based_extension.py b/api/models/api_based_extension.py index 3cef5a0fb2..60167d9069 100644 --- a/api/models/api_based_extension.py +++ b/api/models/api_based_extension.py @@ -1,10 +1,11 @@ import enum +from datetime import datetime -from sqlalchemy import func -from sqlalchemy.orm import mapped_column +import sqlalchemy as sa +from sqlalchemy import DateTime, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column from .base import Base -from .engine import db from .types import StringUUID @@ -18,13 +19,13 @@ class APIBasedExtensionPoint(enum.Enum): class APIBasedExtension(Base): __tablename__ = "api_based_extensions" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="api_based_extension_pkey"), - db.Index("api_based_extension_tenant_idx", "tenant_id"), + sa.PrimaryKeyConstraint("id", name="api_based_extension_pkey"), + sa.Index("api_based_extension_tenant_idx", "tenant_id"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id = mapped_column(StringUUID, nullable=False) - name = mapped_column(db.String(255), nullable=False) - api_endpoint = mapped_column(db.String(255), nullable=False) - api_key = mapped_column(db.Text, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + name: Mapped[str] = mapped_column(String(255), nullable=False) + api_endpoint: Mapped[str] = mapped_column(String(255), nullable=False) + api_key = mapped_column(Text, nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) diff --git a/api/models/dataset.py b/api/models/dataset.py index e6426dc19b..b559654032 100644 --- a/api/models/dataset.py +++ b/api/models/dataset.py @@ -12,7 +12,8 @@ from datetime import datetime from json import JSONDecodeError from typing import Any, Optional, cast -from sqlalchemy import func, select +import sqlalchemy as sa +from sqlalchemy import DateTime, String, func, select from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column @@ -38,25 +39,25 @@ class DatasetPermissionEnum(enum.StrEnum): class Dataset(Base): __tablename__ = "datasets" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="dataset_pkey"), - db.Index("dataset_tenant_idx", "tenant_id"), - db.Index("retrieval_model_idx", "retrieval_model", postgresql_using="gin"), + sa.PrimaryKeyConstraint("id", name="dataset_pkey"), + sa.Index("dataset_tenant_idx", "tenant_id"), + sa.Index("retrieval_model_idx", "retrieval_model", postgresql_using="gin"), ) INDEXING_TECHNIQUE_LIST = ["high_quality", "economy", None] PROVIDER_LIST = ["vendor", "external", None] - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id: Mapped[str] = mapped_column(StringUUID) - name: Mapped[str] = mapped_column(db.String(255)) - description = mapped_column(db.Text, nullable=True) - provider: Mapped[str] = mapped_column(db.String(255), server_default=db.text("'vendor'::character varying")) - permission: Mapped[str] = mapped_column(db.String(255), server_default=db.text("'only_me'::character varying")) - data_source_type = mapped_column(db.String(255)) - indexing_technique: Mapped[Optional[str]] = mapped_column(db.String(255)) - index_struct = mapped_column(db.Text, nullable=True) + name: Mapped[str] = mapped_column(String(255)) + description = mapped_column(sa.Text, nullable=True) + provider: Mapped[str] = mapped_column(String(255), server_default=sa.text("'vendor'::character varying")) + permission: Mapped[str] = mapped_column(String(255), server_default=sa.text("'only_me'::character varying")) + data_source_type = mapped_column(String(255)) + indexing_technique: Mapped[Optional[str]] = mapped_column(String(255)) + index_struct = mapped_column(sa.Text, nullable=True) created_by = mapped_column(StringUUID, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) updated_by = mapped_column(StringUUID, nullable=True) updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) embedding_model = db.Column(db.String(255), nullable=True) # TODO: mapped_column @@ -294,16 +295,16 @@ class Dataset(Base): class DatasetProcessRule(Base): __tablename__ = "dataset_process_rules" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="dataset_process_rule_pkey"), - db.Index("dataset_process_rule_dataset_id_idx", "dataset_id"), + sa.PrimaryKeyConstraint("id", name="dataset_process_rule_pkey"), + sa.Index("dataset_process_rule_dataset_id_idx", "dataset_id"), ) - id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, nullable=False, server_default=sa.text("uuid_generate_v4()")) dataset_id = mapped_column(StringUUID, nullable=False) - mode = mapped_column(db.String(255), nullable=False, server_default=db.text("'automatic'::character varying")) - rules = mapped_column(db.Text, nullable=True) + mode = mapped_column(String(255), nullable=False, server_default=sa.text("'automatic'::character varying")) + rules = mapped_column(sa.Text, nullable=True) created_by = mapped_column(StringUUID, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) MODES = ["automatic", "custom", "hierarchical"] PRE_PROCESSING_RULES = ["remove_stopwords", "remove_extra_spaces", "remove_urls_emails"] @@ -334,72 +335,70 @@ class DatasetProcessRule(Base): class Document(Base): __tablename__ = "documents" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="document_pkey"), - db.Index("document_dataset_id_idx", "dataset_id"), - db.Index("document_is_paused_idx", "is_paused"), - db.Index("document_tenant_idx", "tenant_id"), - db.Index("document_metadata_idx", "doc_metadata", postgresql_using="gin"), + sa.PrimaryKeyConstraint("id", name="document_pkey"), + sa.Index("document_dataset_id_idx", "dataset_id"), + sa.Index("document_is_paused_idx", "is_paused"), + sa.Index("document_tenant_idx", "tenant_id"), + sa.Index("document_metadata_idx", "doc_metadata", postgresql_using="gin"), ) # initial fields - id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, nullable=False, server_default=sa.text("uuid_generate_v4()")) tenant_id = mapped_column(StringUUID, nullable=False) dataset_id = mapped_column(StringUUID, nullable=False) - position = mapped_column(db.Integer, nullable=False) - data_source_type = mapped_column(db.String(255), nullable=False) - data_source_info = mapped_column(db.Text, nullable=True) + position: Mapped[int] = mapped_column(sa.Integer, nullable=False) + data_source_type: Mapped[str] = mapped_column(String(255), nullable=False) + data_source_info = mapped_column(sa.Text, nullable=True) dataset_process_rule_id = mapped_column(StringUUID, nullable=True) - batch = mapped_column(db.String(255), nullable=False) - name = mapped_column(db.String(255), nullable=False) - created_from = mapped_column(db.String(255), nullable=False) + batch: Mapped[str] = mapped_column(String(255), nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) + created_from: Mapped[str] = mapped_column(String(255), nullable=False) created_by = mapped_column(StringUUID, nullable=False) created_api_request_id = mapped_column(StringUUID, nullable=True) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) # start processing - processing_started_at = mapped_column(db.DateTime, nullable=True) + processing_started_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) # parsing - file_id = mapped_column(db.Text, nullable=True) - word_count = mapped_column(db.Integer, nullable=True) - parsing_completed_at = mapped_column(db.DateTime, nullable=True) + file_id = mapped_column(sa.Text, nullable=True) + word_count: Mapped[Optional[int]] = mapped_column(sa.Integer, nullable=True) # TODO: make this not nullable + parsing_completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) # cleaning - cleaning_completed_at = mapped_column(db.DateTime, nullable=True) + cleaning_completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) # split - splitting_completed_at = mapped_column(db.DateTime, nullable=True) + splitting_completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) # indexing - tokens = mapped_column(db.Integer, nullable=True) - indexing_latency = mapped_column(db.Float, nullable=True) - completed_at = mapped_column(db.DateTime, nullable=True) + tokens: Mapped[Optional[int]] = mapped_column(sa.Integer, nullable=True) + indexing_latency: Mapped[Optional[float]] = mapped_column(sa.Float, nullable=True) + completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) # pause - is_paused = mapped_column(db.Boolean, nullable=True, server_default=db.text("false")) + is_paused: Mapped[Optional[bool]] = mapped_column(sa.Boolean, nullable=True, server_default=sa.text("false")) paused_by = mapped_column(StringUUID, nullable=True) - paused_at = mapped_column(db.DateTime, nullable=True) + paused_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) # error - error = mapped_column(db.Text, nullable=True) - stopped_at = mapped_column(db.DateTime, nullable=True) + error = mapped_column(sa.Text, nullable=True) + stopped_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) # basic fields - indexing_status = mapped_column( - db.String(255), nullable=False, server_default=db.text("'waiting'::character varying") - ) - enabled = mapped_column(db.Boolean, nullable=False, server_default=db.text("true")) - disabled_at = mapped_column(db.DateTime, nullable=True) + indexing_status = mapped_column(String(255), nullable=False, server_default=sa.text("'waiting'::character varying")) + enabled: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("true")) + disabled_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) disabled_by = mapped_column(StringUUID, nullable=True) - archived = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) - archived_reason = mapped_column(db.String(255), nullable=True) + archived: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false")) + archived_reason = mapped_column(String(255), nullable=True) archived_by = mapped_column(StringUUID, nullable=True) - archived_at = mapped_column(db.DateTime, nullable=True) - updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - doc_type = mapped_column(db.String(40), nullable=True) + archived_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) + doc_type = mapped_column(String(40), nullable=True) doc_metadata = mapped_column(JSONB, nullable=True) - doc_form = mapped_column(db.String(255), nullable=False, server_default=db.text("'text_model'::character varying")) - doc_language = mapped_column(db.String(255), nullable=True) + doc_form = mapped_column(String(255), nullable=False, server_default=sa.text("'text_model'::character varying")) + doc_language = mapped_column(String(255), nullable=True) DATA_SOURCES = ["upload_file", "notion_import", "website_crawl"] @@ -556,7 +555,7 @@ class Document(Base): "id": "built-in", "name": BuiltInField.upload_date, "type": "time", - "value": self.created_at.timestamp(), + "value": str(self.created_at.timestamp()), } ) built_in_fields.append( @@ -564,7 +563,7 @@ class Document(Base): "id": "built-in", "name": BuiltInField.last_update_date, "type": "time", - "value": self.updated_at.timestamp(), + "value": str(self.updated_at.timestamp()), } ) built_in_fields.append( @@ -677,45 +676,45 @@ class Document(Base): class DocumentSegment(Base): __tablename__ = "document_segments" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="document_segment_pkey"), - db.Index("document_segment_dataset_id_idx", "dataset_id"), - db.Index("document_segment_document_id_idx", "document_id"), - db.Index("document_segment_tenant_dataset_idx", "dataset_id", "tenant_id"), - db.Index("document_segment_tenant_document_idx", "document_id", "tenant_id"), - db.Index("document_segment_node_dataset_idx", "index_node_id", "dataset_id"), - db.Index("document_segment_tenant_idx", "tenant_id"), + sa.PrimaryKeyConstraint("id", name="document_segment_pkey"), + sa.Index("document_segment_dataset_id_idx", "dataset_id"), + sa.Index("document_segment_document_id_idx", "document_id"), + sa.Index("document_segment_tenant_dataset_idx", "dataset_id", "tenant_id"), + sa.Index("document_segment_tenant_document_idx", "document_id", "tenant_id"), + sa.Index("document_segment_node_dataset_idx", "index_node_id", "dataset_id"), + sa.Index("document_segment_tenant_idx", "tenant_id"), ) # initial fields - id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, nullable=False, server_default=sa.text("uuid_generate_v4()")) tenant_id = mapped_column(StringUUID, nullable=False) dataset_id = mapped_column(StringUUID, nullable=False) document_id = mapped_column(StringUUID, nullable=False) position: Mapped[int] - content = mapped_column(db.Text, nullable=False) - answer = mapped_column(db.Text, nullable=True) + content = mapped_column(sa.Text, nullable=False) + answer = mapped_column(sa.Text, nullable=True) word_count: Mapped[int] tokens: Mapped[int] # indexing fields - keywords = mapped_column(db.JSON, nullable=True) - index_node_id = mapped_column(db.String(255), nullable=True) - index_node_hash = mapped_column(db.String(255), nullable=True) + keywords = mapped_column(sa.JSON, nullable=True) + index_node_id = mapped_column(String(255), nullable=True) + index_node_hash = mapped_column(String(255), nullable=True) # basic fields - hit_count = mapped_column(db.Integer, nullable=False, default=0) - enabled = mapped_column(db.Boolean, nullable=False, server_default=db.text("true")) - disabled_at = mapped_column(db.DateTime, nullable=True) + hit_count: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0) + enabled: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("true")) + disabled_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) disabled_by = mapped_column(StringUUID, nullable=True) - status: Mapped[str] = mapped_column(db.String(255), server_default=db.text("'waiting'::character varying")) + status: Mapped[str] = mapped_column(String(255), server_default=sa.text("'waiting'::character varying")) created_by = mapped_column(StringUUID, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) updated_by = mapped_column(StringUUID, nullable=True) - updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - indexing_at = mapped_column(db.DateTime, nullable=True) - completed_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime, nullable=True) - error = mapped_column(db.Text, nullable=True) - stopped_at = mapped_column(db.DateTime, nullable=True) + updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) + indexing_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + error = mapped_column(sa.Text, nullable=True) + stopped_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) @property def dataset(self): @@ -828,32 +827,36 @@ class DocumentSegment(Base): class ChildChunk(Base): __tablename__ = "child_chunks" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="child_chunk_pkey"), - db.Index("child_chunk_dataset_id_idx", "tenant_id", "dataset_id", "document_id", "segment_id", "index_node_id"), - db.Index("child_chunks_node_idx", "index_node_id", "dataset_id"), - db.Index("child_chunks_segment_idx", "segment_id"), + sa.PrimaryKeyConstraint("id", name="child_chunk_pkey"), + sa.Index("child_chunk_dataset_id_idx", "tenant_id", "dataset_id", "document_id", "segment_id", "index_node_id"), + sa.Index("child_chunks_node_idx", "index_node_id", "dataset_id"), + sa.Index("child_chunks_segment_idx", "segment_id"), ) # initial fields - id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, nullable=False, server_default=sa.text("uuid_generate_v4()")) tenant_id = mapped_column(StringUUID, nullable=False) dataset_id = mapped_column(StringUUID, nullable=False) document_id = mapped_column(StringUUID, nullable=False) segment_id = mapped_column(StringUUID, nullable=False) - position = mapped_column(db.Integer, nullable=False) - content = mapped_column(db.Text, nullable=False) - word_count = mapped_column(db.Integer, nullable=False) + position: Mapped[int] = mapped_column(sa.Integer, nullable=False) + content = mapped_column(sa.Text, nullable=False) + word_count: Mapped[int] = mapped_column(sa.Integer, nullable=False) # indexing fields - index_node_id = mapped_column(db.String(255), nullable=True) - index_node_hash = mapped_column(db.String(255), nullable=True) - type = mapped_column(db.String(255), nullable=False, server_default=db.text("'automatic'::character varying")) + index_node_id = mapped_column(String(255), nullable=True) + index_node_hash = mapped_column(String(255), nullable=True) + type = mapped_column(String(255), nullable=False, server_default=sa.text("'automatic'::character varying")) created_by = mapped_column(StringUUID, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, server_default=sa.text("CURRENT_TIMESTAMP(0)") + ) updated_by = mapped_column(StringUUID, nullable=True) - updated_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) - indexing_at = mapped_column(db.DateTime, nullable=True) - completed_at = mapped_column(db.DateTime, nullable=True) - error = mapped_column(db.Text, nullable=True) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, server_default=sa.text("CURRENT_TIMESTAMP(0)") + ) + indexing_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + error = mapped_column(sa.Text, nullable=True) @property def dataset(self): @@ -871,14 +874,14 @@ class ChildChunk(Base): class AppDatasetJoin(Base): __tablename__ = "app_dataset_joins" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="app_dataset_join_pkey"), - db.Index("app_dataset_join_app_dataset_idx", "dataset_id", "app_id"), + sa.PrimaryKeyConstraint("id", name="app_dataset_join_pkey"), + sa.Index("app_dataset_join_app_dataset_idx", "dataset_id", "app_id"), ) - id = mapped_column(StringUUID, primary_key=True, nullable=False, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, primary_key=True, nullable=False, server_default=sa.text("uuid_generate_v4()")) app_id = mapped_column(StringUUID, nullable=False) dataset_id = mapped_column(StringUUID, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=db.func.current_timestamp()) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=db.func.current_timestamp()) @property def app(self): @@ -888,32 +891,32 @@ class AppDatasetJoin(Base): class DatasetQuery(Base): __tablename__ = "dataset_queries" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="dataset_query_pkey"), - db.Index("dataset_query_dataset_id_idx", "dataset_id"), + sa.PrimaryKeyConstraint("id", name="dataset_query_pkey"), + sa.Index("dataset_query_dataset_id_idx", "dataset_id"), ) - id = mapped_column(StringUUID, primary_key=True, nullable=False, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, primary_key=True, nullable=False, server_default=sa.text("uuid_generate_v4()")) dataset_id = mapped_column(StringUUID, nullable=False) - content = mapped_column(db.Text, nullable=False) - source = mapped_column(db.String(255), nullable=False) + content = mapped_column(sa.Text, nullable=False) + source: Mapped[str] = mapped_column(String(255), nullable=False) source_app_id = mapped_column(StringUUID, nullable=True) - created_by_role = mapped_column(db.String, nullable=False) + created_by_role = mapped_column(String, nullable=False) created_by = mapped_column(StringUUID, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=db.func.current_timestamp()) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=db.func.current_timestamp()) class DatasetKeywordTable(Base): __tablename__ = "dataset_keyword_tables" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="dataset_keyword_table_pkey"), - db.Index("dataset_keyword_table_dataset_id_idx", "dataset_id"), + sa.PrimaryKeyConstraint("id", name="dataset_keyword_table_pkey"), + sa.Index("dataset_keyword_table_dataset_id_idx", "dataset_id"), ) - id = mapped_column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, primary_key=True, server_default=sa.text("uuid_generate_v4()")) dataset_id = mapped_column(StringUUID, nullable=False, unique=True) - keyword_table = mapped_column(db.Text, nullable=False) + keyword_table = mapped_column(sa.Text, nullable=False) data_source_type = mapped_column( - db.String(255), nullable=False, server_default=db.text("'database'::character varying") + String(255), nullable=False, server_default=sa.text("'database'::character varying") ) @property @@ -950,19 +953,19 @@ class DatasetKeywordTable(Base): class Embedding(Base): __tablename__ = "embeddings" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="embedding_pkey"), - db.UniqueConstraint("model_name", "hash", "provider_name", name="embedding_hash_idx"), - db.Index("created_at_idx", "created_at"), + sa.PrimaryKeyConstraint("id", name="embedding_pkey"), + sa.UniqueConstraint("model_name", "hash", "provider_name", name="embedding_hash_idx"), + sa.Index("created_at_idx", "created_at"), ) - id = mapped_column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, primary_key=True, server_default=sa.text("uuid_generate_v4()")) model_name = mapped_column( - db.String(255), nullable=False, server_default=db.text("'text-embedding-ada-002'::character varying") + String(255), nullable=False, server_default=sa.text("'text-embedding-ada-002'::character varying") ) - hash = mapped_column(db.String(64), nullable=False) - embedding = mapped_column(db.LargeBinary, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - provider_name = mapped_column(db.String(255), nullable=False, server_default=db.text("''::character varying")) + hash = mapped_column(String(64), nullable=False) + embedding = mapped_column(sa.LargeBinary, nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) + provider_name = mapped_column(String(255), nullable=False, server_default=sa.text("''::character varying")) def set_embedding(self, embedding_data: list[float]): self.embedding = pickle.dumps(embedding_data, protocol=pickle.HIGHEST_PROTOCOL) @@ -974,84 +977,84 @@ class Embedding(Base): class DatasetCollectionBinding(Base): __tablename__ = "dataset_collection_bindings" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="dataset_collection_bindings_pkey"), - db.Index("provider_model_name_idx", "provider_name", "model_name"), + sa.PrimaryKeyConstraint("id", name="dataset_collection_bindings_pkey"), + sa.Index("provider_model_name_idx", "provider_name", "model_name"), ) - id = mapped_column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()")) - provider_name = mapped_column(db.String(255), nullable=False) - model_name = mapped_column(db.String(255), nullable=False) - type = mapped_column(db.String(40), server_default=db.text("'dataset'::character varying"), nullable=False) - collection_name = mapped_column(db.String(64), nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + id = mapped_column(StringUUID, primary_key=True, server_default=sa.text("uuid_generate_v4()")) + provider_name: Mapped[str] = mapped_column(String(255), nullable=False) + model_name: Mapped[str] = mapped_column(String(255), nullable=False) + type = mapped_column(String(40), server_default=sa.text("'dataset'::character varying"), nullable=False) + collection_name = mapped_column(String(64), nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) class TidbAuthBinding(Base): __tablename__ = "tidb_auth_bindings" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="tidb_auth_bindings_pkey"), - db.Index("tidb_auth_bindings_tenant_idx", "tenant_id"), - db.Index("tidb_auth_bindings_active_idx", "active"), - db.Index("tidb_auth_bindings_created_at_idx", "created_at"), - db.Index("tidb_auth_bindings_status_idx", "status"), + sa.PrimaryKeyConstraint("id", name="tidb_auth_bindings_pkey"), + sa.Index("tidb_auth_bindings_tenant_idx", "tenant_id"), + sa.Index("tidb_auth_bindings_active_idx", "active"), + sa.Index("tidb_auth_bindings_created_at_idx", "created_at"), + sa.Index("tidb_auth_bindings_status_idx", "status"), ) - id = mapped_column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, primary_key=True, server_default=sa.text("uuid_generate_v4()")) tenant_id = mapped_column(StringUUID, nullable=True) - cluster_id = mapped_column(db.String(255), nullable=False) - cluster_name = mapped_column(db.String(255), nullable=False) - active = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) - status = mapped_column(db.String(255), nullable=False, server_default=db.text("CREATING")) - account = mapped_column(db.String(255), nullable=False) - password = mapped_column(db.String(255), nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + cluster_id: Mapped[str] = mapped_column(String(255), nullable=False) + cluster_name: Mapped[str] = mapped_column(String(255), nullable=False) + active: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=db.text("false")) + status = mapped_column(String(255), nullable=False, server_default=db.text("'CREATING'::character varying")) + account: Mapped[str] = mapped_column(String(255), nullable=False) + password: Mapped[str] = mapped_column(String(255), nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) class Whitelist(Base): __tablename__ = "whitelists" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="whitelists_pkey"), - db.Index("whitelists_tenant_idx", "tenant_id"), + sa.PrimaryKeyConstraint("id", name="whitelists_pkey"), + sa.Index("whitelists_tenant_idx", "tenant_id"), ) - id = mapped_column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, primary_key=True, server_default=sa.text("uuid_generate_v4()")) tenant_id = mapped_column(StringUUID, nullable=True) - category = mapped_column(db.String(255), nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + category: Mapped[str] = mapped_column(String(255), nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) class DatasetPermission(Base): __tablename__ = "dataset_permissions" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="dataset_permission_pkey"), - db.Index("idx_dataset_permissions_dataset_id", "dataset_id"), - db.Index("idx_dataset_permissions_account_id", "account_id"), - db.Index("idx_dataset_permissions_tenant_id", "tenant_id"), + sa.PrimaryKeyConstraint("id", name="dataset_permission_pkey"), + sa.Index("idx_dataset_permissions_dataset_id", "dataset_id"), + sa.Index("idx_dataset_permissions_account_id", "account_id"), + sa.Index("idx_dataset_permissions_tenant_id", "tenant_id"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"), primary_key=True) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"), primary_key=True) dataset_id = mapped_column(StringUUID, nullable=False) account_id = mapped_column(StringUUID, nullable=False) tenant_id = mapped_column(StringUUID, nullable=False) - has_permission = mapped_column(db.Boolean, nullable=False, server_default=db.text("true")) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + has_permission: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("true")) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) class ExternalKnowledgeApis(Base): __tablename__ = "external_knowledge_apis" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="external_knowledge_apis_pkey"), - db.Index("external_knowledge_apis_tenant_idx", "tenant_id"), - db.Index("external_knowledge_apis_name_idx", "name"), + sa.PrimaryKeyConstraint("id", name="external_knowledge_apis_pkey"), + sa.Index("external_knowledge_apis_tenant_idx", "tenant_id"), + sa.Index("external_knowledge_apis_name_idx", "name"), ) - id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) - name = mapped_column(db.String(255), nullable=False) - description = mapped_column(db.String(255), nullable=False) + id = mapped_column(StringUUID, nullable=False, server_default=sa.text("uuid_generate_v4()")) + name: Mapped[str] = mapped_column(String(255), nullable=False) + description: Mapped[str] = mapped_column(String(255), nullable=False) tenant_id = mapped_column(StringUUID, nullable=False) - settings = mapped_column(db.Text, nullable=True) + settings = mapped_column(sa.Text, nullable=True) created_by = mapped_column(StringUUID, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) updated_by = mapped_column(StringUUID, nullable=True) - updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) def to_dict(self): return { @@ -1091,71 +1094,79 @@ class ExternalKnowledgeApis(Base): class ExternalKnowledgeBindings(Base): __tablename__ = "external_knowledge_bindings" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="external_knowledge_bindings_pkey"), - db.Index("external_knowledge_bindings_tenant_idx", "tenant_id"), - db.Index("external_knowledge_bindings_dataset_idx", "dataset_id"), - db.Index("external_knowledge_bindings_external_knowledge_idx", "external_knowledge_id"), - db.Index("external_knowledge_bindings_external_knowledge_api_idx", "external_knowledge_api_id"), + sa.PrimaryKeyConstraint("id", name="external_knowledge_bindings_pkey"), + sa.Index("external_knowledge_bindings_tenant_idx", "tenant_id"), + sa.Index("external_knowledge_bindings_dataset_idx", "dataset_id"), + sa.Index("external_knowledge_bindings_external_knowledge_idx", "external_knowledge_id"), + sa.Index("external_knowledge_bindings_external_knowledge_api_idx", "external_knowledge_api_id"), ) - id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, nullable=False, server_default=sa.text("uuid_generate_v4()")) tenant_id = mapped_column(StringUUID, nullable=False) external_knowledge_api_id = mapped_column(StringUUID, nullable=False) dataset_id = mapped_column(StringUUID, nullable=False) - external_knowledge_id = mapped_column(db.Text, nullable=False) + external_knowledge_id = mapped_column(sa.Text, nullable=False) created_by = mapped_column(StringUUID, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) updated_by = mapped_column(StringUUID, nullable=True) - updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) class DatasetAutoDisableLog(Base): __tablename__ = "dataset_auto_disable_logs" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="dataset_auto_disable_log_pkey"), - db.Index("dataset_auto_disable_log_tenant_idx", "tenant_id"), - db.Index("dataset_auto_disable_log_dataset_idx", "dataset_id"), - db.Index("dataset_auto_disable_log_created_atx", "created_at"), + sa.PrimaryKeyConstraint("id", name="dataset_auto_disable_log_pkey"), + sa.Index("dataset_auto_disable_log_tenant_idx", "tenant_id"), + sa.Index("dataset_auto_disable_log_dataset_idx", "dataset_id"), + sa.Index("dataset_auto_disable_log_created_atx", "created_at"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id = mapped_column(StringUUID, nullable=False) dataset_id = mapped_column(StringUUID, nullable=False) document_id = mapped_column(StringUUID, nullable=False) - notified = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) - created_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) + notified: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false")) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, server_default=sa.text("CURRENT_TIMESTAMP(0)") + ) class RateLimitLog(Base): __tablename__ = "rate_limit_logs" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="rate_limit_log_pkey"), - db.Index("rate_limit_log_tenant_idx", "tenant_id"), - db.Index("rate_limit_log_operation_idx", "operation"), + sa.PrimaryKeyConstraint("id", name="rate_limit_log_pkey"), + sa.Index("rate_limit_log_tenant_idx", "tenant_id"), + sa.Index("rate_limit_log_operation_idx", "operation"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id = mapped_column(StringUUID, nullable=False) - subscription_plan = mapped_column(db.String(255), nullable=False) - operation = mapped_column(db.String(255), nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) + subscription_plan: Mapped[str] = mapped_column(String(255), nullable=False) + operation: Mapped[str] = mapped_column(String(255), nullable=False) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, server_default=sa.text("CURRENT_TIMESTAMP(0)") + ) class DatasetMetadata(Base): __tablename__ = "dataset_metadatas" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="dataset_metadata_pkey"), - db.Index("dataset_metadata_tenant_idx", "tenant_id"), - db.Index("dataset_metadata_dataset_idx", "dataset_id"), + sa.PrimaryKeyConstraint("id", name="dataset_metadata_pkey"), + sa.Index("dataset_metadata_tenant_idx", "tenant_id"), + sa.Index("dataset_metadata_dataset_idx", "dataset_id"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id = mapped_column(StringUUID, nullable=False) dataset_id = mapped_column(StringUUID, nullable=False) - type = mapped_column(db.String(255), nullable=False) - name = mapped_column(db.String(255), nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) - updated_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) + type: Mapped[str] = mapped_column(String(255), nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, server_default=sa.text("CURRENT_TIMESTAMP(0)") + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, server_default=sa.text("CURRENT_TIMESTAMP(0)") + ) created_by = mapped_column(StringUUID, nullable=False) updated_by = mapped_column(StringUUID, nullable=True) @@ -1163,19 +1174,19 @@ class DatasetMetadata(Base): class DatasetMetadataBinding(Base): __tablename__ = "dataset_metadata_bindings" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="dataset_metadata_binding_pkey"), - db.Index("dataset_metadata_binding_tenant_idx", "tenant_id"), - db.Index("dataset_metadata_binding_dataset_idx", "dataset_id"), - db.Index("dataset_metadata_binding_metadata_idx", "metadata_id"), - db.Index("dataset_metadata_binding_document_idx", "document_id"), + sa.PrimaryKeyConstraint("id", name="dataset_metadata_binding_pkey"), + sa.Index("dataset_metadata_binding_tenant_idx", "tenant_id"), + sa.Index("dataset_metadata_binding_dataset_idx", "dataset_id"), + sa.Index("dataset_metadata_binding_metadata_idx", "metadata_id"), + sa.Index("dataset_metadata_binding_document_idx", "document_id"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id = mapped_column(StringUUID, nullable=False) dataset_id = mapped_column(StringUUID, nullable=False) metadata_id = mapped_column(StringUUID, nullable=False) document_id = mapped_column(StringUUID, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) created_by = mapped_column(StringUUID, nullable=False) diff --git a/api/models/model.py b/api/models/model.py index ce1156e33e..b8240e1931 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -17,7 +17,7 @@ if TYPE_CHECKING: import sqlalchemy as sa from flask import request from flask_login import UserMixin -from sqlalchemy import Float, Index, PrimaryKeyConstraint, func, text +from sqlalchemy import Float, Index, PrimaryKeyConstraint, String, func, text from sqlalchemy.orm import Mapped, Session, mapped_column from configs import dify_config @@ -35,10 +35,10 @@ from .types import StringUUID class DifySetup(Base): __tablename__ = "dify_setups" - __table_args__ = (db.PrimaryKeyConstraint("version", name="dify_setup_pkey"),) + __table_args__ = (sa.PrimaryKeyConstraint("version", name="dify_setup_pkey"),) - version = mapped_column(db.String(255), nullable=False) - setup_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + version: Mapped[str] = mapped_column(String(255), nullable=False) + setup_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) class AppMode(StrEnum): @@ -71,33 +71,33 @@ class IconType(Enum): class App(Base): __tablename__ = "apps" - __table_args__ = (db.PrimaryKeyConstraint("id", name="app_pkey"), db.Index("app_tenant_id_idx", "tenant_id")) + __table_args__ = (sa.PrimaryKeyConstraint("id", name="app_pkey"), sa.Index("app_tenant_id_idx", "tenant_id")) - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id: Mapped[str] = mapped_column(StringUUID) - name: Mapped[str] = mapped_column(db.String(255)) - description: Mapped[str] = mapped_column(db.Text, server_default=db.text("''::character varying")) - mode: Mapped[str] = mapped_column(db.String(255)) - icon_type: Mapped[Optional[str]] = mapped_column(db.String(255)) # image, emoji - icon = db.Column(db.String(255)) - icon_background: Mapped[Optional[str]] = mapped_column(db.String(255)) + name: Mapped[str] = mapped_column(String(255)) + description: Mapped[str] = mapped_column(sa.Text, server_default=sa.text("''::character varying")) + mode: Mapped[str] = mapped_column(String(255)) + icon_type: Mapped[Optional[str]] = mapped_column(String(255)) # image, emoji + icon = db.Column(String(255)) + icon_background: Mapped[Optional[str]] = mapped_column(String(255)) app_model_config_id = mapped_column(StringUUID, nullable=True) workflow_id = mapped_column(StringUUID, nullable=True) - status: Mapped[str] = mapped_column(db.String(255), server_default=db.text("'normal'::character varying")) - enable_site: Mapped[bool] = mapped_column(db.Boolean) - enable_api: Mapped[bool] = mapped_column(db.Boolean) - api_rpm: Mapped[int] = mapped_column(db.Integer, server_default=db.text("0")) - api_rph: Mapped[int] = mapped_column(db.Integer, server_default=db.text("0")) - is_demo: Mapped[bool] = mapped_column(db.Boolean, server_default=db.text("false")) - is_public: Mapped[bool] = mapped_column(db.Boolean, server_default=db.text("false")) - is_universal: Mapped[bool] = mapped_column(db.Boolean, server_default=db.text("false")) - tracing = mapped_column(db.Text, nullable=True) + status: Mapped[str] = mapped_column(String(255), server_default=sa.text("'normal'::character varying")) + enable_site: Mapped[bool] = mapped_column(sa.Boolean) + enable_api: Mapped[bool] = mapped_column(sa.Boolean) + api_rpm: Mapped[int] = mapped_column(sa.Integer, server_default=sa.text("0")) + api_rph: Mapped[int] = mapped_column(sa.Integer, server_default=sa.text("0")) + is_demo: Mapped[bool] = mapped_column(sa.Boolean, server_default=sa.text("false")) + is_public: Mapped[bool] = mapped_column(sa.Boolean, server_default=sa.text("false")) + is_universal: Mapped[bool] = mapped_column(sa.Boolean, server_default=sa.text("false")) + tracing = mapped_column(sa.Text, nullable=True) max_active_requests: Mapped[Optional[int]] created_by = mapped_column(StringUUID, nullable=True) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) updated_by = mapped_column(StringUUID, nullable=True) - updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - use_icon_as_answer_icon: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) + updated_at: Mapped[datetime] = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + use_icon_as_answer_icon: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false")) @property def desc_or_prompt(self): @@ -304,36 +304,36 @@ class App(Base): class AppModelConfig(Base): __tablename__ = "app_model_configs" - __table_args__ = (db.PrimaryKeyConstraint("id", name="app_model_config_pkey"), db.Index("app_app_id_idx", "app_id")) + __table_args__ = (sa.PrimaryKeyConstraint("id", name="app_model_config_pkey"), sa.Index("app_app_id_idx", "app_id")) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) app_id = mapped_column(StringUUID, nullable=False) - provider = mapped_column(db.String(255), nullable=True) - model_id = mapped_column(db.String(255), nullable=True) - configs = mapped_column(db.JSON, nullable=True) + provider = mapped_column(String(255), nullable=True) + model_id = mapped_column(String(255), nullable=True) + configs = mapped_column(sa.JSON, nullable=True) created_by = mapped_column(StringUUID, nullable=True) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) updated_by = mapped_column(StringUUID, nullable=True) - updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - opening_statement = mapped_column(db.Text) - suggested_questions = mapped_column(db.Text) - suggested_questions_after_answer = mapped_column(db.Text) - speech_to_text = mapped_column(db.Text) - text_to_speech = mapped_column(db.Text) - more_like_this = mapped_column(db.Text) - model = mapped_column(db.Text) - user_input_form = mapped_column(db.Text) - dataset_query_variable = mapped_column(db.String(255)) - pre_prompt = mapped_column(db.Text) - agent_mode = mapped_column(db.Text) - sensitive_word_avoidance = mapped_column(db.Text) - retriever_resource = mapped_column(db.Text) - prompt_type = mapped_column(db.String(255), nullable=False, server_default=db.text("'simple'::character varying")) - chat_prompt_config = mapped_column(db.Text) - completion_prompt_config = mapped_column(db.Text) - dataset_configs = mapped_column(db.Text) - external_data_tools = mapped_column(db.Text) - file_upload = mapped_column(db.Text) + updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + opening_statement = mapped_column(sa.Text) + suggested_questions = mapped_column(sa.Text) + suggested_questions_after_answer = mapped_column(sa.Text) + speech_to_text = mapped_column(sa.Text) + text_to_speech = mapped_column(sa.Text) + more_like_this = mapped_column(sa.Text) + model = mapped_column(sa.Text) + user_input_form = mapped_column(sa.Text) + dataset_query_variable = mapped_column(String(255)) + pre_prompt = mapped_column(sa.Text) + agent_mode = mapped_column(sa.Text) + sensitive_word_avoidance = mapped_column(sa.Text) + retriever_resource = mapped_column(sa.Text) + prompt_type = mapped_column(String(255), nullable=False, server_default=sa.text("'simple'::character varying")) + chat_prompt_config = mapped_column(sa.Text) + completion_prompt_config = mapped_column(sa.Text) + dataset_configs = mapped_column(sa.Text) + external_data_tools = mapped_column(sa.Text) + file_upload = mapped_column(sa.Text) @property def app(self): @@ -555,24 +555,24 @@ class AppModelConfig(Base): class RecommendedApp(Base): __tablename__ = "recommended_apps" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="recommended_app_pkey"), - db.Index("recommended_app_app_id_idx", "app_id"), - db.Index("recommended_app_is_listed_idx", "is_listed", "language"), + sa.PrimaryKeyConstraint("id", name="recommended_app_pkey"), + sa.Index("recommended_app_app_id_idx", "app_id"), + sa.Index("recommended_app_is_listed_idx", "is_listed", "language"), ) - id = mapped_column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, primary_key=True, server_default=sa.text("uuid_generate_v4()")) app_id = mapped_column(StringUUID, nullable=False) - description = mapped_column(db.JSON, nullable=False) - copyright = mapped_column(db.String(255), nullable=False) - privacy_policy = mapped_column(db.String(255), nullable=False) + description = mapped_column(sa.JSON, nullable=False) + copyright: Mapped[str] = mapped_column(String(255), nullable=False) + privacy_policy: Mapped[str] = mapped_column(String(255), nullable=False) custom_disclaimer: Mapped[str] = mapped_column(sa.TEXT, default="") - category = mapped_column(db.String(255), nullable=False) - position = mapped_column(db.Integer, nullable=False, default=0) - is_listed = mapped_column(db.Boolean, nullable=False, default=True) - install_count = mapped_column(db.Integer, nullable=False, default=0) - language = mapped_column(db.String(255), nullable=False, server_default=db.text("'en-US'::character varying")) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + category: Mapped[str] = mapped_column(String(255), nullable=False) + position: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0) + is_listed: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=True) + install_count: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0) + language = mapped_column(String(255), nullable=False, server_default=sa.text("'en-US'::character varying")) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) @property def app(self): @@ -583,20 +583,20 @@ class RecommendedApp(Base): class InstalledApp(Base): __tablename__ = "installed_apps" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="installed_app_pkey"), - db.Index("installed_app_tenant_id_idx", "tenant_id"), - db.Index("installed_app_app_id_idx", "app_id"), - db.UniqueConstraint("tenant_id", "app_id", name="unique_tenant_app"), + sa.PrimaryKeyConstraint("id", name="installed_app_pkey"), + sa.Index("installed_app_tenant_id_idx", "tenant_id"), + sa.Index("installed_app_app_id_idx", "app_id"), + sa.UniqueConstraint("tenant_id", "app_id", name="unique_tenant_app"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id = mapped_column(StringUUID, nullable=False) app_id = mapped_column(StringUUID, nullable=False) app_owner_tenant_id = mapped_column(StringUUID, nullable=False) - position = mapped_column(db.Integer, nullable=False, default=0) - is_pinned = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) - last_used_at = mapped_column(db.DateTime, nullable=True) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + position: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0) + is_pinned: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false")) + last_used_at = mapped_column(sa.DateTime, nullable=True) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) @property def app(self): @@ -612,47 +612,47 @@ class InstalledApp(Base): class Conversation(Base): __tablename__ = "conversations" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="conversation_pkey"), - db.Index("conversation_app_from_user_idx", "app_id", "from_source", "from_end_user_id"), + sa.PrimaryKeyConstraint("id", name="conversation_pkey"), + sa.Index("conversation_app_from_user_idx", "app_id", "from_source", "from_end_user_id"), ) - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) app_id = mapped_column(StringUUID, nullable=False) app_model_config_id = mapped_column(StringUUID, nullable=True) - model_provider = mapped_column(db.String(255), nullable=True) - override_model_configs = mapped_column(db.Text) - model_id = mapped_column(db.String(255), nullable=True) - mode: Mapped[str] = mapped_column(db.String(255)) - name = mapped_column(db.String(255), nullable=False) - summary = mapped_column(db.Text) - _inputs: Mapped[dict] = mapped_column("inputs", db.JSON) - introduction = mapped_column(db.Text) - system_instruction = mapped_column(db.Text) - system_instruction_tokens = mapped_column(db.Integer, nullable=False, server_default=db.text("0")) - status = mapped_column(db.String(255), nullable=False) + model_provider = mapped_column(String(255), nullable=True) + override_model_configs = mapped_column(sa.Text) + model_id = mapped_column(String(255), nullable=True) + mode: Mapped[str] = mapped_column(String(255)) + name: Mapped[str] = mapped_column(String(255), nullable=False) + summary = mapped_column(sa.Text) + _inputs: Mapped[dict] = mapped_column("inputs", sa.JSON) + introduction = mapped_column(sa.Text) + system_instruction = mapped_column(sa.Text) + system_instruction_tokens: Mapped[int] = mapped_column(sa.Integer, nullable=False, server_default=sa.text("0")) + status: Mapped[str] = mapped_column(String(255), nullable=False) # The `invoke_from` records how the conversation is created. # # Its value corresponds to the members of `InvokeFrom`. # (api/core/app/entities/app_invoke_entities.py) - invoke_from = mapped_column(db.String(255), nullable=True) + invoke_from = mapped_column(String(255), nullable=True) # ref: ConversationSource. - from_source = mapped_column(db.String(255), nullable=False) + from_source: Mapped[str] = mapped_column(String(255), nullable=False) from_end_user_id = mapped_column(StringUUID) from_account_id = mapped_column(StringUUID) - read_at = mapped_column(db.DateTime) + read_at = mapped_column(sa.DateTime) read_account_id = mapped_column(StringUUID) dialogue_count: Mapped[int] = mapped_column(default=0) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) messages = db.relationship("Message", backref="conversation", lazy="select", passive_deletes="all") message_annotations = db.relationship( "MessageAnnotation", backref="conversation", lazy="select", passive_deletes="all" ) - is_deleted = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) + is_deleted: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false")) @property def inputs(self): @@ -894,36 +894,36 @@ class Message(Base): Index("message_created_at_idx", "created_at"), ) - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) app_id = mapped_column(StringUUID, nullable=False) - model_provider = mapped_column(db.String(255), nullable=True) - model_id = mapped_column(db.String(255), nullable=True) - override_model_configs = mapped_column(db.Text) - conversation_id = mapped_column(StringUUID, db.ForeignKey("conversations.id"), nullable=False) - _inputs: Mapped[dict] = mapped_column("inputs", db.JSON) - query: Mapped[str] = mapped_column(db.Text, nullable=False) - message = mapped_column(db.JSON, nullable=False) - message_tokens: Mapped[int] = mapped_column(db.Integer, nullable=False, server_default=db.text("0")) - message_unit_price = mapped_column(db.Numeric(10, 4), nullable=False) - message_price_unit = mapped_column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001")) - answer: Mapped[str] = db.Column(db.Text, nullable=False) # TODO make it mapped_column - answer_tokens: Mapped[int] = mapped_column(db.Integer, nullable=False, server_default=db.text("0")) - answer_unit_price = mapped_column(db.Numeric(10, 4), nullable=False) - answer_price_unit = mapped_column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001")) + model_provider = mapped_column(String(255), nullable=True) + model_id = mapped_column(String(255), nullable=True) + override_model_configs = mapped_column(sa.Text) + conversation_id = mapped_column(StringUUID, sa.ForeignKey("conversations.id"), nullable=False) + _inputs: Mapped[dict] = mapped_column("inputs", sa.JSON) + query: Mapped[str] = mapped_column(sa.Text, nullable=False) + message = mapped_column(sa.JSON, nullable=False) + message_tokens: Mapped[int] = mapped_column(sa.Integer, nullable=False, server_default=sa.text("0")) + message_unit_price = mapped_column(sa.Numeric(10, 4), nullable=False) + message_price_unit = mapped_column(sa.Numeric(10, 7), nullable=False, server_default=sa.text("0.001")) + answer: Mapped[str] = db.Column(sa.Text, nullable=False) # TODO make it mapped_column + answer_tokens: Mapped[int] = mapped_column(sa.Integer, nullable=False, server_default=sa.text("0")) + answer_unit_price = mapped_column(sa.Numeric(10, 4), nullable=False) + answer_price_unit = mapped_column(sa.Numeric(10, 7), nullable=False, server_default=sa.text("0.001")) parent_message_id = mapped_column(StringUUID, nullable=True) - provider_response_latency = mapped_column(db.Float, nullable=False, server_default=db.text("0")) - total_price = mapped_column(db.Numeric(10, 7)) - currency = mapped_column(db.String(255), nullable=False) - status = mapped_column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying")) - error = mapped_column(db.Text) - message_metadata = mapped_column(db.Text) - invoke_from: Mapped[Optional[str]] = mapped_column(db.String(255), nullable=True) - from_source = mapped_column(db.String(255), nullable=False) + provider_response_latency = mapped_column(sa.Float, nullable=False, server_default=sa.text("0")) + total_price = mapped_column(sa.Numeric(10, 7)) + currency: Mapped[str] = mapped_column(String(255), nullable=False) + status = mapped_column(String(255), nullable=False, server_default=sa.text("'normal'::character varying")) + error = mapped_column(sa.Text) + message_metadata = mapped_column(sa.Text) + invoke_from: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + from_source: Mapped[str] = mapped_column(String(255), nullable=False) from_end_user_id: Mapped[Optional[str]] = mapped_column(StringUUID) from_account_id: Mapped[Optional[str]] = mapped_column(StringUUID) - created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp()) - updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - agent_based = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) + created_at: Mapped[datetime] = mapped_column(sa.DateTime, server_default=func.current_timestamp()) + updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + agent_based: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false")) workflow_run_id: Mapped[Optional[str]] = mapped_column(StringUUID) @property @@ -1230,23 +1230,23 @@ class Message(Base): class MessageFeedback(Base): __tablename__ = "message_feedbacks" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="message_feedback_pkey"), - db.Index("message_feedback_app_idx", "app_id"), - db.Index("message_feedback_message_idx", "message_id", "from_source"), - db.Index("message_feedback_conversation_idx", "conversation_id", "from_source", "rating"), + sa.PrimaryKeyConstraint("id", name="message_feedback_pkey"), + sa.Index("message_feedback_app_idx", "app_id"), + sa.Index("message_feedback_message_idx", "message_id", "from_source"), + sa.Index("message_feedback_conversation_idx", "conversation_id", "from_source", "rating"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) app_id = mapped_column(StringUUID, nullable=False) conversation_id = mapped_column(StringUUID, nullable=False) message_id = mapped_column(StringUUID, nullable=False) - rating = mapped_column(db.String(255), nullable=False) - content = mapped_column(db.Text) - from_source = mapped_column(db.String(255), nullable=False) + rating: Mapped[str] = mapped_column(String(255), nullable=False) + content = mapped_column(sa.Text) + from_source: Mapped[str] = mapped_column(String(255), nullable=False) from_end_user_id = mapped_column(StringUUID) from_account_id = mapped_column(StringUUID) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) @property def from_account(self): @@ -1272,9 +1272,9 @@ class MessageFeedback(Base): class MessageFile(Base): __tablename__ = "message_files" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="message_file_pkey"), - db.Index("message_file_message_idx", "message_id"), - db.Index("message_file_created_by_idx", "created_by"), + sa.PrimaryKeyConstraint("id", name="message_file_pkey"), + sa.Index("message_file_message_idx", "message_id"), + sa.Index("message_file_created_by_idx", "created_by"), ) def __init__( @@ -1298,37 +1298,37 @@ class MessageFile(Base): self.created_by_role = created_by_role.value self.created_by = created_by - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) message_id: Mapped[str] = mapped_column(StringUUID, nullable=False) - type: Mapped[str] = mapped_column(db.String(255), nullable=False) - transfer_method: Mapped[str] = mapped_column(db.String(255), nullable=False) - url: Mapped[Optional[str]] = mapped_column(db.Text, nullable=True) - belongs_to: Mapped[Optional[str]] = mapped_column(db.String(255), nullable=True) + type: Mapped[str] = mapped_column(String(255), nullable=False) + transfer_method: Mapped[str] = mapped_column(String(255), nullable=False) + url: Mapped[Optional[str]] = mapped_column(sa.Text, nullable=True) + belongs_to: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) upload_file_id: Mapped[Optional[str]] = mapped_column(StringUUID, nullable=True) - created_by_role: Mapped[str] = mapped_column(db.String(255), nullable=False) + created_by_role: Mapped[str] = mapped_column(String(255), nullable=False) created_by: Mapped[str] = mapped_column(StringUUID, nullable=False) - created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at: Mapped[datetime] = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) class MessageAnnotation(Base): __tablename__ = "message_annotations" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="message_annotation_pkey"), - db.Index("message_annotation_app_idx", "app_id"), - db.Index("message_annotation_conversation_idx", "conversation_id"), - db.Index("message_annotation_message_idx", "message_id"), + sa.PrimaryKeyConstraint("id", name="message_annotation_pkey"), + sa.Index("message_annotation_app_idx", "app_id"), + sa.Index("message_annotation_conversation_idx", "conversation_id"), + sa.Index("message_annotation_message_idx", "message_id"), ) - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) app_id: Mapped[str] = mapped_column(StringUUID) - conversation_id: Mapped[Optional[str]] = mapped_column(StringUUID, db.ForeignKey("conversations.id")) + conversation_id: Mapped[Optional[str]] = mapped_column(StringUUID, sa.ForeignKey("conversations.id")) message_id: Mapped[Optional[str]] = mapped_column(StringUUID) - question = db.Column(db.Text, nullable=True) - content = mapped_column(db.Text, nullable=False) - hit_count = mapped_column(db.Integer, nullable=False, server_default=db.text("0")) + question = db.Column(sa.Text, nullable=True) + content = mapped_column(sa.Text, nullable=False) + hit_count: Mapped[int] = mapped_column(sa.Integer, nullable=False, server_default=sa.text("0")) account_id = mapped_column(StringUUID, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) @property def account(self): @@ -1344,24 +1344,24 @@ class MessageAnnotation(Base): class AppAnnotationHitHistory(Base): __tablename__ = "app_annotation_hit_histories" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="app_annotation_hit_histories_pkey"), - db.Index("app_annotation_hit_histories_app_idx", "app_id"), - db.Index("app_annotation_hit_histories_account_idx", "account_id"), - db.Index("app_annotation_hit_histories_annotation_idx", "annotation_id"), - db.Index("app_annotation_hit_histories_message_idx", "message_id"), + sa.PrimaryKeyConstraint("id", name="app_annotation_hit_histories_pkey"), + sa.Index("app_annotation_hit_histories_app_idx", "app_id"), + sa.Index("app_annotation_hit_histories_account_idx", "account_id"), + sa.Index("app_annotation_hit_histories_annotation_idx", "annotation_id"), + sa.Index("app_annotation_hit_histories_message_idx", "message_id"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) app_id = mapped_column(StringUUID, nullable=False) annotation_id: Mapped[str] = mapped_column(StringUUID, nullable=False) - source = mapped_column(db.Text, nullable=False) - question = mapped_column(db.Text, nullable=False) + source = mapped_column(sa.Text, nullable=False) + question = mapped_column(sa.Text, nullable=False) account_id = mapped_column(StringUUID, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - score = mapped_column(Float, nullable=False, server_default=db.text("0")) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + score = mapped_column(Float, nullable=False, server_default=sa.text("0")) message_id = mapped_column(StringUUID, nullable=False) - annotation_question = mapped_column(db.Text, nullable=False) - annotation_content = mapped_column(db.Text, nullable=False) + annotation_question = mapped_column(sa.Text, nullable=False) + annotation_content = mapped_column(sa.Text, nullable=False) @property def account(self): @@ -1382,18 +1382,18 @@ class AppAnnotationHitHistory(Base): class AppAnnotationSetting(Base): __tablename__ = "app_annotation_settings" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="app_annotation_settings_pkey"), - db.Index("app_annotation_settings_app_idx", "app_id"), + sa.PrimaryKeyConstraint("id", name="app_annotation_settings_pkey"), + sa.Index("app_annotation_settings_app_idx", "app_id"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) app_id = mapped_column(StringUUID, nullable=False) - score_threshold = mapped_column(Float, nullable=False, server_default=db.text("0")) + score_threshold = mapped_column(Float, nullable=False, server_default=sa.text("0")) collection_binding_id = mapped_column(StringUUID, nullable=False) created_user_id = mapped_column(StringUUID, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) updated_user_id = mapped_column(StringUUID, nullable=False) - updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) @property def collection_binding_detail(self): @@ -1410,58 +1410,58 @@ class AppAnnotationSetting(Base): class OperationLog(Base): __tablename__ = "operation_logs" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="operation_log_pkey"), - db.Index("operation_log_account_action_idx", "tenant_id", "account_id", "action"), + sa.PrimaryKeyConstraint("id", name="operation_log_pkey"), + sa.Index("operation_log_account_action_idx", "tenant_id", "account_id", "action"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id = mapped_column(StringUUID, nullable=False) account_id = mapped_column(StringUUID, nullable=False) - action = mapped_column(db.String(255), nullable=False) - content = mapped_column(db.JSON) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - created_ip = mapped_column(db.String(255), nullable=False) - updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + action: Mapped[str] = mapped_column(String(255), nullable=False) + content = mapped_column(sa.JSON) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + created_ip: Mapped[str] = mapped_column(String(255), nullable=False) + updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) class EndUser(Base, UserMixin): __tablename__ = "end_users" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="end_user_pkey"), - db.Index("end_user_session_id_idx", "session_id", "type"), - db.Index("end_user_tenant_session_id_idx", "tenant_id", "session_id", "type"), + sa.PrimaryKeyConstraint("id", name="end_user_pkey"), + sa.Index("end_user_session_id_idx", "session_id", "type"), + sa.Index("end_user_tenant_session_id_idx", "tenant_id", "session_id", "type"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) app_id = mapped_column(StringUUID, nullable=True) - type = mapped_column(db.String(255), nullable=False) - external_user_id = mapped_column(db.String(255), nullable=True) - name = mapped_column(db.String(255)) - is_anonymous = mapped_column(db.Boolean, nullable=False, server_default=db.text("true")) + type: Mapped[str] = mapped_column(String(255), nullable=False) + external_user_id = mapped_column(String(255), nullable=True) + name = mapped_column(String(255)) + is_anonymous: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("true")) session_id: Mapped[str] = mapped_column() - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) class AppMCPServer(Base): __tablename__ = "app_mcp_servers" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="app_mcp_server_pkey"), - db.UniqueConstraint("tenant_id", "app_id", name="unique_app_mcp_server_tenant_app_id"), - db.UniqueConstraint("server_code", name="unique_app_mcp_server_server_code"), + sa.PrimaryKeyConstraint("id", name="app_mcp_server_pkey"), + sa.UniqueConstraint("tenant_id", "app_id", name="unique_app_mcp_server_tenant_app_id"), + sa.UniqueConstraint("server_code", name="unique_app_mcp_server_server_code"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id = mapped_column(StringUUID, nullable=False) app_id = mapped_column(StringUUID, nullable=False) - name = mapped_column(db.String(255), nullable=False) - description = mapped_column(db.String(255), nullable=False) - server_code = mapped_column(db.String(255), nullable=False) - status = mapped_column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying")) - parameters = mapped_column(db.Text, nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) + description: Mapped[str] = mapped_column(String(255), nullable=False) + server_code: Mapped[str] = mapped_column(String(255), nullable=False) + status = mapped_column(String(255), nullable=False, server_default=sa.text("'normal'::character varying")) + parameters = mapped_column(sa.Text, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) @staticmethod def generate_server_code(n): @@ -1480,35 +1480,35 @@ class AppMCPServer(Base): class Site(Base): __tablename__ = "sites" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="site_pkey"), - db.Index("site_app_id_idx", "app_id"), - db.Index("site_code_idx", "code", "status"), + sa.PrimaryKeyConstraint("id", name="site_pkey"), + sa.Index("site_app_id_idx", "app_id"), + sa.Index("site_code_idx", "code", "status"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) app_id = mapped_column(StringUUID, nullable=False) - title = mapped_column(db.String(255), nullable=False) - icon_type = mapped_column(db.String(255), nullable=True) - icon = mapped_column(db.String(255)) - icon_background = mapped_column(db.String(255)) - description = mapped_column(db.Text) - default_language = mapped_column(db.String(255), nullable=False) - chat_color_theme = mapped_column(db.String(255)) - chat_color_theme_inverted = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) - copyright = mapped_column(db.String(255)) - privacy_policy = mapped_column(db.String(255)) - show_workflow_steps = mapped_column(db.Boolean, nullable=False, server_default=db.text("true")) - use_icon_as_answer_icon = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) + title: Mapped[str] = mapped_column(String(255), nullable=False) + icon_type = mapped_column(String(255), nullable=True) + icon = mapped_column(String(255)) + icon_background = mapped_column(String(255)) + description = mapped_column(sa.Text) + default_language: Mapped[str] = mapped_column(String(255), nullable=False) + chat_color_theme = mapped_column(String(255)) + chat_color_theme_inverted: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false")) + copyright = mapped_column(String(255)) + privacy_policy = mapped_column(String(255)) + show_workflow_steps: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("true")) + use_icon_as_answer_icon: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false")) _custom_disclaimer: Mapped[str] = mapped_column("custom_disclaimer", sa.TEXT, default="") - customize_domain = mapped_column(db.String(255)) - customize_token_strategy = mapped_column(db.String(255), nullable=False) - prompt_public = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) - status = mapped_column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying")) + customize_domain = mapped_column(String(255)) + customize_token_strategy: Mapped[str] = mapped_column(String(255), nullable=False) + prompt_public: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false")) + status = mapped_column(String(255), nullable=False, server_default=sa.text("'normal'::character varying")) created_by = mapped_column(StringUUID, nullable=True) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) updated_by = mapped_column(StringUUID, nullable=True) - updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - code = mapped_column(db.String(255)) + updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + code = mapped_column(String(255)) @property def custom_disclaimer(self): @@ -1537,19 +1537,19 @@ class Site(Base): class ApiToken(Base): __tablename__ = "api_tokens" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="api_token_pkey"), - db.Index("api_token_app_id_type_idx", "app_id", "type"), - db.Index("api_token_token_idx", "token", "type"), - db.Index("api_token_tenant_idx", "tenant_id", "type"), + sa.PrimaryKeyConstraint("id", name="api_token_pkey"), + sa.Index("api_token_app_id_type_idx", "app_id", "type"), + sa.Index("api_token_token_idx", "token", "type"), + sa.Index("api_token_tenant_idx", "tenant_id", "type"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) app_id = mapped_column(StringUUID, nullable=True) tenant_id = mapped_column(StringUUID, nullable=True) - type = mapped_column(db.String(16), nullable=False) - token = mapped_column(db.String(255), nullable=False) - last_used_at = mapped_column(db.DateTime, nullable=True) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + type = mapped_column(String(16), nullable=False) + token: Mapped[str] = mapped_column(String(255), nullable=False) + last_used_at = mapped_column(sa.DateTime, nullable=True) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) @staticmethod def generate_api_key(prefix, n): @@ -1563,27 +1563,27 @@ class ApiToken(Base): class UploadFile(Base): __tablename__ = "upload_files" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="upload_file_pkey"), - db.Index("upload_file_tenant_idx", "tenant_id"), + sa.PrimaryKeyConstraint("id", name="upload_file_pkey"), + sa.Index("upload_file_tenant_idx", "tenant_id"), ) - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) - storage_type: Mapped[str] = mapped_column(db.String(255), nullable=False) - key: Mapped[str] = mapped_column(db.String(255), nullable=False) - name: Mapped[str] = mapped_column(db.String(255), nullable=False) - size: Mapped[int] = mapped_column(db.Integer, nullable=False) - extension: Mapped[str] = mapped_column(db.String(255), nullable=False) - mime_type: Mapped[str] = mapped_column(db.String(255), nullable=True) + storage_type: Mapped[str] = mapped_column(String(255), nullable=False) + key: Mapped[str] = mapped_column(String(255), nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) + size: Mapped[int] = mapped_column(sa.Integer, nullable=False) + extension: Mapped[str] = mapped_column(String(255), nullable=False) + mime_type: Mapped[str] = mapped_column(String(255), nullable=True) created_by_role: Mapped[str] = mapped_column( - db.String(255), nullable=False, server_default=db.text("'account'::character varying") + String(255), nullable=False, server_default=sa.text("'account'::character varying") ) created_by: Mapped[str] = mapped_column(StringUUID, nullable=False) - created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - used: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) + created_at: Mapped[datetime] = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + used: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false")) used_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True) - used_at: Mapped[datetime | None] = mapped_column(db.DateTime, nullable=True) - hash: Mapped[str | None] = mapped_column(db.String(255), nullable=True) + used_at: Mapped[datetime | None] = mapped_column(sa.DateTime, nullable=True) + hash: Mapped[str | None] = mapped_column(String(255), nullable=True) source_url: Mapped[str] = mapped_column(sa.TEXT, default="") def __init__( @@ -1625,71 +1625,71 @@ class UploadFile(Base): class ApiRequest(Base): __tablename__ = "api_requests" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="api_request_pkey"), - db.Index("api_request_token_idx", "tenant_id", "api_token_id"), + sa.PrimaryKeyConstraint("id", name="api_request_pkey"), + sa.Index("api_request_token_idx", "tenant_id", "api_token_id"), ) - id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, nullable=False, server_default=sa.text("uuid_generate_v4()")) tenant_id = mapped_column(StringUUID, nullable=False) api_token_id = mapped_column(StringUUID, nullable=False) - path = mapped_column(db.String(255), nullable=False) - request = mapped_column(db.Text, nullable=True) - response = mapped_column(db.Text, nullable=True) - ip = mapped_column(db.String(255), nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + path: Mapped[str] = mapped_column(String(255), nullable=False) + request = mapped_column(sa.Text, nullable=True) + response = mapped_column(sa.Text, nullable=True) + ip: Mapped[str] = mapped_column(String(255), nullable=False) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) class MessageChain(Base): __tablename__ = "message_chains" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="message_chain_pkey"), - db.Index("message_chain_message_id_idx", "message_id"), + sa.PrimaryKeyConstraint("id", name="message_chain_pkey"), + sa.Index("message_chain_message_id_idx", "message_id"), ) - id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, nullable=False, server_default=sa.text("uuid_generate_v4()")) message_id = mapped_column(StringUUID, nullable=False) - type = mapped_column(db.String(255), nullable=False) - input = mapped_column(db.Text, nullable=True) - output = mapped_column(db.Text, nullable=True) - created_at = mapped_column(db.DateTime, nullable=False, server_default=db.func.current_timestamp()) + type: Mapped[str] = mapped_column(String(255), nullable=False) + input = mapped_column(sa.Text, nullable=True) + output = mapped_column(sa.Text, nullable=True) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=db.func.current_timestamp()) class MessageAgentThought(Base): __tablename__ = "message_agent_thoughts" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="message_agent_thought_pkey"), - db.Index("message_agent_thought_message_id_idx", "message_id"), - db.Index("message_agent_thought_message_chain_id_idx", "message_chain_id"), + sa.PrimaryKeyConstraint("id", name="message_agent_thought_pkey"), + sa.Index("message_agent_thought_message_id_idx", "message_id"), + sa.Index("message_agent_thought_message_chain_id_idx", "message_chain_id"), ) - id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, nullable=False, server_default=sa.text("uuid_generate_v4()")) message_id = mapped_column(StringUUID, nullable=False) message_chain_id = mapped_column(StringUUID, nullable=True) - position = mapped_column(db.Integer, nullable=False) - thought = mapped_column(db.Text, nullable=True) - tool = mapped_column(db.Text, nullable=True) - tool_labels_str = mapped_column(db.Text, nullable=False, server_default=db.text("'{}'::text")) - tool_meta_str = mapped_column(db.Text, nullable=False, server_default=db.text("'{}'::text")) - tool_input = mapped_column(db.Text, nullable=True) - observation = mapped_column(db.Text, nullable=True) + position: Mapped[int] = mapped_column(sa.Integer, nullable=False) + thought = mapped_column(sa.Text, nullable=True) + tool = mapped_column(sa.Text, nullable=True) + tool_labels_str = mapped_column(sa.Text, nullable=False, server_default=sa.text("'{}'::text")) + tool_meta_str = mapped_column(sa.Text, nullable=False, server_default=sa.text("'{}'::text")) + tool_input = mapped_column(sa.Text, nullable=True) + observation = mapped_column(sa.Text, nullable=True) # plugin_id = mapped_column(StringUUID, nullable=True) ## for future design - tool_process_data = mapped_column(db.Text, nullable=True) - message = mapped_column(db.Text, nullable=True) - message_token = mapped_column(db.Integer, nullable=True) - message_unit_price = mapped_column(db.Numeric, nullable=True) - message_price_unit = mapped_column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001")) - message_files = mapped_column(db.Text, nullable=True) - answer = db.Column(db.Text, nullable=True) - answer_token = mapped_column(db.Integer, nullable=True) - answer_unit_price = mapped_column(db.Numeric, nullable=True) - answer_price_unit = mapped_column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001")) - tokens = mapped_column(db.Integer, nullable=True) - total_price = mapped_column(db.Numeric, nullable=True) - currency = mapped_column(db.String, nullable=True) - latency = mapped_column(db.Float, nullable=True) - created_by_role = mapped_column(db.String, nullable=False) + tool_process_data = mapped_column(sa.Text, nullable=True) + message = mapped_column(sa.Text, nullable=True) + message_token: Mapped[Optional[int]] = mapped_column(sa.Integer, nullable=True) + message_unit_price = mapped_column(sa.Numeric, nullable=True) + message_price_unit = mapped_column(sa.Numeric(10, 7), nullable=False, server_default=sa.text("0.001")) + message_files = mapped_column(sa.Text, nullable=True) + answer = db.Column(sa.Text, nullable=True) + answer_token: Mapped[Optional[int]] = mapped_column(sa.Integer, nullable=True) + answer_unit_price = mapped_column(sa.Numeric, nullable=True) + answer_price_unit = mapped_column(sa.Numeric(10, 7), nullable=False, server_default=sa.text("0.001")) + tokens: Mapped[Optional[int]] = mapped_column(sa.Integer, nullable=True) + total_price = mapped_column(sa.Numeric, nullable=True) + currency = mapped_column(String, nullable=True) + latency: Mapped[Optional[float]] = mapped_column(sa.Float, nullable=True) + created_by_role = mapped_column(String, nullable=False) created_by = mapped_column(StringUUID, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=db.func.current_timestamp()) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=db.func.current_timestamp()) @property def files(self) -> list: @@ -1771,80 +1771,80 @@ class MessageAgentThought(Base): class DatasetRetrieverResource(Base): __tablename__ = "dataset_retriever_resources" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="dataset_retriever_resource_pkey"), - db.Index("dataset_retriever_resource_message_id_idx", "message_id"), + sa.PrimaryKeyConstraint("id", name="dataset_retriever_resource_pkey"), + sa.Index("dataset_retriever_resource_message_id_idx", "message_id"), ) - id = mapped_column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, nullable=False, server_default=sa.text("uuid_generate_v4()")) message_id = mapped_column(StringUUID, nullable=False) - position = mapped_column(db.Integer, nullable=False) + position: Mapped[int] = mapped_column(sa.Integer, nullable=False) dataset_id = mapped_column(StringUUID, nullable=False) - dataset_name = mapped_column(db.Text, nullable=False) + dataset_name = mapped_column(sa.Text, nullable=False) document_id = mapped_column(StringUUID, nullable=True) - document_name = mapped_column(db.Text, nullable=False) - data_source_type = mapped_column(db.Text, nullable=True) + document_name = mapped_column(sa.Text, nullable=False) + data_source_type = mapped_column(sa.Text, nullable=True) segment_id = mapped_column(StringUUID, nullable=True) - score = mapped_column(db.Float, nullable=True) - content = mapped_column(db.Text, nullable=False) - hit_count = mapped_column(db.Integer, nullable=True) - word_count = mapped_column(db.Integer, nullable=True) - segment_position = mapped_column(db.Integer, nullable=True) - index_node_hash = mapped_column(db.Text, nullable=True) - retriever_from = mapped_column(db.Text, nullable=False) + score: Mapped[Optional[float]] = mapped_column(sa.Float, nullable=True) + content = mapped_column(sa.Text, nullable=False) + hit_count: Mapped[Optional[int]] = mapped_column(sa.Integer, nullable=True) + word_count: Mapped[Optional[int]] = mapped_column(sa.Integer, nullable=True) + segment_position: Mapped[Optional[int]] = mapped_column(sa.Integer, nullable=True) + index_node_hash = mapped_column(sa.Text, nullable=True) + retriever_from = mapped_column(sa.Text, nullable=False) created_by = mapped_column(StringUUID, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=db.func.current_timestamp()) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=db.func.current_timestamp()) class Tag(Base): __tablename__ = "tags" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="tag_pkey"), - db.Index("tag_type_idx", "type"), - db.Index("tag_name_idx", "name"), + sa.PrimaryKeyConstraint("id", name="tag_pkey"), + sa.Index("tag_type_idx", "type"), + sa.Index("tag_name_idx", "name"), ) TAG_TYPE_LIST = ["knowledge", "app"] - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id = mapped_column(StringUUID, nullable=True) - type = mapped_column(db.String(16), nullable=False) - name = mapped_column(db.String(255), nullable=False) + type = mapped_column(String(16), nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) created_by = mapped_column(StringUUID, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) class TagBinding(Base): __tablename__ = "tag_bindings" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="tag_binding_pkey"), - db.Index("tag_bind_target_id_idx", "target_id"), - db.Index("tag_bind_tag_id_idx", "tag_id"), + sa.PrimaryKeyConstraint("id", name="tag_binding_pkey"), + sa.Index("tag_bind_target_id_idx", "target_id"), + sa.Index("tag_bind_tag_id_idx", "tag_id"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id = mapped_column(StringUUID, nullable=True) tag_id = mapped_column(StringUUID, nullable=True) target_id = mapped_column(StringUUID, nullable=True) created_by = mapped_column(StringUUID, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) class TraceAppConfig(Base): __tablename__ = "trace_app_config" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="tracing_app_config_pkey"), - db.Index("trace_app_config_app_id_idx", "app_id"), + sa.PrimaryKeyConstraint("id", name="tracing_app_config_pkey"), + sa.Index("trace_app_config_app_id_idx", "app_id"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) app_id = mapped_column(StringUUID, nullable=False) - tracing_provider = mapped_column(db.String(255), nullable=True) - tracing_config = mapped_column(db.JSON, nullable=True) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + tracing_provider = mapped_column(String(255), nullable=True) + tracing_config = mapped_column(sa.JSON, nullable=True) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) updated_at = mapped_column( - db.DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp() + sa.DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp() ) - is_active = mapped_column(db.Boolean, nullable=False, server_default=db.text("true")) + is_active: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("true")) @property def tracing_config_dict(self): diff --git a/api/models/provider.py b/api/models/provider.py index 1e25f0c90f..4ea2c59fdb 100644 --- a/api/models/provider.py +++ b/api/models/provider.py @@ -2,11 +2,11 @@ from datetime import datetime from enum import Enum from typing import Optional -from sqlalchemy import func, text +import sqlalchemy as sa +from sqlalchemy import DateTime, String, func, text from sqlalchemy.orm import Mapped, mapped_column from .base import Base -from .engine import db from .types import StringUUID @@ -47,31 +47,31 @@ class Provider(Base): __tablename__ = "providers" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="provider_pkey"), - db.Index("provider_tenant_id_provider_idx", "tenant_id", "provider_name"), - db.UniqueConstraint( + sa.PrimaryKeyConstraint("id", name="provider_pkey"), + sa.Index("provider_tenant_id_provider_idx", "tenant_id", "provider_name"), + sa.UniqueConstraint( "tenant_id", "provider_name", "provider_type", "quota_type", name="unique_provider_name_type_quota" ), ) id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()")) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) - provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False) + provider_name: Mapped[str] = mapped_column(String(255), nullable=False) provider_type: Mapped[str] = mapped_column( - db.String(40), nullable=False, server_default=text("'custom'::character varying") + String(40), nullable=False, server_default=text("'custom'::character varying") ) - encrypted_config: Mapped[Optional[str]] = mapped_column(db.Text, nullable=True) - is_valid: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=text("false")) - last_used: Mapped[Optional[datetime]] = mapped_column(db.DateTime, nullable=True) + encrypted_config: Mapped[Optional[str]] = mapped_column(sa.Text, nullable=True) + is_valid: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=text("false")) + last_used: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) quota_type: Mapped[Optional[str]] = mapped_column( - db.String(40), nullable=True, server_default=text("''::character varying") + String(40), nullable=True, server_default=text("''::character varying") ) - quota_limit: Mapped[Optional[int]] = mapped_column(db.BigInteger, nullable=True) - quota_used: Mapped[Optional[int]] = mapped_column(db.BigInteger, default=0) + quota_limit: Mapped[Optional[int]] = mapped_column(sa.BigInteger, nullable=True) + quota_used: Mapped[Optional[int]] = mapped_column(sa.BigInteger, default=0) - created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) def __repr__(self): return ( @@ -104,80 +104,80 @@ class ProviderModel(Base): __tablename__ = "provider_models" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="provider_model_pkey"), - db.Index("provider_model_tenant_id_provider_idx", "tenant_id", "provider_name"), - db.UniqueConstraint( + sa.PrimaryKeyConstraint("id", name="provider_model_pkey"), + sa.Index("provider_model_tenant_id_provider_idx", "tenant_id", "provider_name"), + sa.UniqueConstraint( "tenant_id", "provider_name", "model_name", "model_type", name="unique_provider_model_name" ), ) id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()")) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) - provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False) - model_name: Mapped[str] = mapped_column(db.String(255), nullable=False) - model_type: Mapped[str] = mapped_column(db.String(40), nullable=False) - encrypted_config: Mapped[Optional[str]] = mapped_column(db.Text, nullable=True) - is_valid: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=text("false")) - created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + provider_name: Mapped[str] = mapped_column(String(255), nullable=False) + model_name: Mapped[str] = mapped_column(String(255), nullable=False) + model_type: Mapped[str] = mapped_column(String(40), nullable=False) + encrypted_config: Mapped[Optional[str]] = mapped_column(sa.Text, nullable=True) + is_valid: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=text("false")) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) class TenantDefaultModel(Base): __tablename__ = "tenant_default_models" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="tenant_default_model_pkey"), - db.Index("tenant_default_model_tenant_id_provider_type_idx", "tenant_id", "provider_name", "model_type"), + sa.PrimaryKeyConstraint("id", name="tenant_default_model_pkey"), + sa.Index("tenant_default_model_tenant_id_provider_type_idx", "tenant_id", "provider_name", "model_type"), ) id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()")) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) - provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False) - model_name: Mapped[str] = mapped_column(db.String(255), nullable=False) - model_type: Mapped[str] = mapped_column(db.String(40), nullable=False) - created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + provider_name: Mapped[str] = mapped_column(String(255), nullable=False) + model_name: Mapped[str] = mapped_column(String(255), nullable=False) + model_type: Mapped[str] = mapped_column(String(40), nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) class TenantPreferredModelProvider(Base): __tablename__ = "tenant_preferred_model_providers" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="tenant_preferred_model_provider_pkey"), - db.Index("tenant_preferred_model_provider_tenant_provider_idx", "tenant_id", "provider_name"), + sa.PrimaryKeyConstraint("id", name="tenant_preferred_model_provider_pkey"), + sa.Index("tenant_preferred_model_provider_tenant_provider_idx", "tenant_id", "provider_name"), ) id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()")) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) - provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False) - preferred_provider_type: Mapped[str] = mapped_column(db.String(40), nullable=False) - created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + provider_name: Mapped[str] = mapped_column(String(255), nullable=False) + preferred_provider_type: Mapped[str] = mapped_column(String(40), nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) class ProviderOrder(Base): __tablename__ = "provider_orders" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="provider_order_pkey"), - db.Index("provider_order_tenant_provider_idx", "tenant_id", "provider_name"), + sa.PrimaryKeyConstraint("id", name="provider_order_pkey"), + sa.Index("provider_order_tenant_provider_idx", "tenant_id", "provider_name"), ) id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()")) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) - provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False) + provider_name: Mapped[str] = mapped_column(String(255), nullable=False) account_id: Mapped[str] = mapped_column(StringUUID, nullable=False) - payment_product_id: Mapped[str] = mapped_column(db.String(191), nullable=False) - payment_id: Mapped[Optional[str]] = mapped_column(db.String(191)) - transaction_id: Mapped[Optional[str]] = mapped_column(db.String(191)) - quantity: Mapped[int] = mapped_column(db.Integer, nullable=False, server_default=text("1")) - currency: Mapped[Optional[str]] = mapped_column(db.String(40)) - total_amount: Mapped[Optional[int]] = mapped_column(db.Integer) + payment_product_id: Mapped[str] = mapped_column(String(191), nullable=False) + payment_id: Mapped[Optional[str]] = mapped_column(String(191)) + transaction_id: Mapped[Optional[str]] = mapped_column(String(191)) + quantity: Mapped[int] = mapped_column(sa.Integer, nullable=False, server_default=text("1")) + currency: Mapped[Optional[str]] = mapped_column(String(40)) + total_amount: Mapped[Optional[int]] = mapped_column(sa.Integer) payment_status: Mapped[str] = mapped_column( - db.String(40), nullable=False, server_default=text("'wait_pay'::character varying") + String(40), nullable=False, server_default=text("'wait_pay'::character varying") ) - paid_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime) - pay_failed_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime) - refunded_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime) - created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + paid_at: Mapped[Optional[datetime]] = mapped_column(DateTime) + pay_failed_at: Mapped[Optional[datetime]] = mapped_column(DateTime) + refunded_at: Mapped[Optional[datetime]] = mapped_column(DateTime) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) class ProviderModelSetting(Base): @@ -187,19 +187,19 @@ class ProviderModelSetting(Base): __tablename__ = "provider_model_settings" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="provider_model_setting_pkey"), - db.Index("provider_model_setting_tenant_provider_model_idx", "tenant_id", "provider_name", "model_type"), + sa.PrimaryKeyConstraint("id", name="provider_model_setting_pkey"), + sa.Index("provider_model_setting_tenant_provider_model_idx", "tenant_id", "provider_name", "model_type"), ) id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()")) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) - provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False) - model_name: Mapped[str] = mapped_column(db.String(255), nullable=False) - model_type: Mapped[str] = mapped_column(db.String(40), nullable=False) - enabled: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=text("true")) - load_balancing_enabled: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=text("false")) - created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + provider_name: Mapped[str] = mapped_column(String(255), nullable=False) + model_name: Mapped[str] = mapped_column(String(255), nullable=False) + model_type: Mapped[str] = mapped_column(String(40), nullable=False) + enabled: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=text("true")) + load_balancing_enabled: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=text("false")) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) class LoadBalancingModelConfig(Base): @@ -209,17 +209,17 @@ class LoadBalancingModelConfig(Base): __tablename__ = "load_balancing_model_configs" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="load_balancing_model_config_pkey"), - db.Index("load_balancing_model_config_tenant_provider_model_idx", "tenant_id", "provider_name", "model_type"), + sa.PrimaryKeyConstraint("id", name="load_balancing_model_config_pkey"), + sa.Index("load_balancing_model_config_tenant_provider_model_idx", "tenant_id", "provider_name", "model_type"), ) id: Mapped[str] = mapped_column(StringUUID, server_default=text("uuid_generate_v4()")) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) - provider_name: Mapped[str] = mapped_column(db.String(255), nullable=False) - model_name: Mapped[str] = mapped_column(db.String(255), nullable=False) - model_type: Mapped[str] = mapped_column(db.String(40), nullable=False) - name: Mapped[str] = mapped_column(db.String(255), nullable=False) - encrypted_config: Mapped[Optional[str]] = mapped_column(db.Text, nullable=True) - enabled: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=text("true")) - created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + provider_name: Mapped[str] = mapped_column(String(255), nullable=False) + model_name: Mapped[str] = mapped_column(String(255), nullable=False) + model_type: Mapped[str] = mapped_column(String(40), nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) + encrypted_config: Mapped[Optional[str]] = mapped_column(sa.Text, nullable=True) + enabled: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=text("true")) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) diff --git a/api/models/source.py b/api/models/source.py index 100e0d96ef..8456d65a87 100644 --- a/api/models/source.py +++ b/api/models/source.py @@ -1,49 +1,51 @@ import json +from datetime import datetime +from typing import Optional -from sqlalchemy import func +import sqlalchemy as sa +from sqlalchemy import DateTime, String, func from sqlalchemy.dialects.postgresql import JSONB -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import Mapped, mapped_column from models.base import Base -from .engine import db from .types import StringUUID class DataSourceOauthBinding(Base): __tablename__ = "data_source_oauth_bindings" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="source_binding_pkey"), - db.Index("source_binding_tenant_id_idx", "tenant_id"), - db.Index("source_info_idx", "source_info", postgresql_using="gin"), + sa.PrimaryKeyConstraint("id", name="source_binding_pkey"), + sa.Index("source_binding_tenant_id_idx", "tenant_id"), + sa.Index("source_info_idx", "source_info", postgresql_using="gin"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id = mapped_column(StringUUID, nullable=False) - access_token = mapped_column(db.String(255), nullable=False) - provider = mapped_column(db.String(255), nullable=False) + access_token: Mapped[str] = mapped_column(String(255), nullable=False) + provider: Mapped[str] = mapped_column(String(255), nullable=False) source_info = mapped_column(JSONB, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - disabled = mapped_column(db.Boolean, nullable=True, server_default=db.text("false")) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) + disabled: Mapped[Optional[bool]] = mapped_column(sa.Boolean, nullable=True, server_default=sa.text("false")) class DataSourceApiKeyAuthBinding(Base): __tablename__ = "data_source_api_key_auth_bindings" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="data_source_api_key_auth_binding_pkey"), - db.Index("data_source_api_key_auth_binding_tenant_id_idx", "tenant_id"), - db.Index("data_source_api_key_auth_binding_provider_idx", "provider"), + sa.PrimaryKeyConstraint("id", name="data_source_api_key_auth_binding_pkey"), + sa.Index("data_source_api_key_auth_binding_tenant_id_idx", "tenant_id"), + sa.Index("data_source_api_key_auth_binding_provider_idx", "provider"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id = mapped_column(StringUUID, nullable=False) - category = mapped_column(db.String(255), nullable=False) - provider = mapped_column(db.String(255), nullable=False) - credentials = mapped_column(db.Text, nullable=True) # JSON - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - disabled = mapped_column(db.Boolean, nullable=True, server_default=db.text("false")) + category: Mapped[str] = mapped_column(String(255), nullable=False) + provider: Mapped[str] = mapped_column(String(255), nullable=False) + credentials = mapped_column(sa.Text, nullable=True) # JSON + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) + disabled: Mapped[Optional[bool]] = mapped_column(sa.Boolean, nullable=True, server_default=sa.text("false")) def to_dict(self): return { diff --git a/api/models/task.py b/api/models/task.py index 3e5ebd2099..ab700c553c 100644 --- a/api/models/task.py +++ b/api/models/task.py @@ -1,7 +1,9 @@ from datetime import datetime from typing import Optional +import sqlalchemy as sa from celery import states # type: ignore +from sqlalchemy import DateTime, String from sqlalchemy.orm import Mapped, mapped_column from libs.datetime_utils import naive_utc_now @@ -15,23 +17,23 @@ class CeleryTask(Base): __tablename__ = "celery_taskmeta" - id = mapped_column(db.Integer, db.Sequence("task_id_sequence"), primary_key=True, autoincrement=True) - task_id = mapped_column(db.String(155), unique=True) - status = mapped_column(db.String(50), default=states.PENDING) + id = mapped_column(sa.Integer, sa.Sequence("task_id_sequence"), primary_key=True, autoincrement=True) + task_id = mapped_column(String(155), unique=True) + status = mapped_column(String(50), default=states.PENDING) result = mapped_column(db.PickleType, nullable=True) date_done = mapped_column( - db.DateTime, + DateTime, default=lambda: naive_utc_now(), onupdate=lambda: naive_utc_now(), nullable=True, ) - traceback = mapped_column(db.Text, nullable=True) - name = mapped_column(db.String(155), nullable=True) - args = mapped_column(db.LargeBinary, nullable=True) - kwargs = mapped_column(db.LargeBinary, nullable=True) - worker = mapped_column(db.String(155), nullable=True) - retries = mapped_column(db.Integer, nullable=True) - queue = mapped_column(db.String(155), nullable=True) + traceback = mapped_column(sa.Text, nullable=True) + name = mapped_column(String(155), nullable=True) + args = mapped_column(sa.LargeBinary, nullable=True) + kwargs = mapped_column(sa.LargeBinary, nullable=True) + worker = mapped_column(String(155), nullable=True) + retries: Mapped[Optional[int]] = mapped_column(sa.Integer, nullable=True) + queue = mapped_column(String(155), nullable=True) class CeleryTaskSet(Base): @@ -40,8 +42,8 @@ class CeleryTaskSet(Base): __tablename__ = "celery_tasksetmeta" id: Mapped[int] = mapped_column( - db.Integer, db.Sequence("taskset_id_sequence"), autoincrement=True, primary_key=True + sa.Integer, sa.Sequence("taskset_id_sequence"), autoincrement=True, primary_key=True ) - taskset_id = mapped_column(db.String(155), unique=True) + taskset_id = mapped_column(String(155), unique=True) result = mapped_column(db.PickleType, nullable=True) - date_done: Mapped[Optional[datetime]] = mapped_column(db.DateTime, default=lambda: naive_utc_now(), nullable=True) + date_done: Mapped[Optional[datetime]] = mapped_column(DateTime, default=lambda: naive_utc_now(), nullable=True) diff --git a/api/models/tools.py b/api/models/tools.py index 68f4211e59..408c1371c2 100644 --- a/api/models/tools.py +++ b/api/models/tools.py @@ -5,7 +5,7 @@ from urllib.parse import urlparse import sqlalchemy as sa from deprecated import deprecated -from sqlalchemy import ForeignKey, func +from sqlalchemy import ForeignKey, String, func from sqlalchemy.orm import Mapped, mapped_column from core.file import helpers as file_helpers @@ -25,33 +25,33 @@ from .types import StringUUID class ToolOAuthSystemClient(Base): __tablename__ = "tool_oauth_system_clients" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="tool_oauth_system_client_pkey"), - db.UniqueConstraint("plugin_id", "provider", name="tool_oauth_system_client_plugin_id_provider_idx"), + sa.PrimaryKeyConstraint("id", name="tool_oauth_system_client_pkey"), + sa.UniqueConstraint("plugin_id", "provider", name="tool_oauth_system_client_plugin_id_provider_idx"), ) - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) - plugin_id: Mapped[str] = mapped_column(db.String(512), nullable=False) - provider: Mapped[str] = mapped_column(db.String(255), nullable=False) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) + plugin_id = mapped_column(String(512), nullable=False) + provider: Mapped[str] = mapped_column(String(255), nullable=False) # oauth params of the tool provider - encrypted_oauth_params: Mapped[str] = mapped_column(db.Text, nullable=False) + encrypted_oauth_params: Mapped[str] = mapped_column(sa.Text, nullable=False) # tenant level tool oauth client params (client_id, client_secret, etc.) class ToolOAuthTenantClient(Base): __tablename__ = "tool_oauth_tenant_clients" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="tool_oauth_tenant_client_pkey"), - db.UniqueConstraint("tenant_id", "plugin_id", "provider", name="unique_tool_oauth_tenant_client"), + sa.PrimaryKeyConstraint("id", name="tool_oauth_tenant_client_pkey"), + sa.UniqueConstraint("tenant_id", "plugin_id", "provider", name="unique_tool_oauth_tenant_client"), ) - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) # tenant id tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) - plugin_id: Mapped[str] = mapped_column(db.String(512), nullable=False) - provider: Mapped[str] = mapped_column(db.String(255), nullable=False) - enabled: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("true")) + plugin_id: Mapped[str] = mapped_column(String(512), nullable=False) + provider: Mapped[str] = mapped_column(String(255), nullable=False) + enabled: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("true")) # oauth params of the tool provider - encrypted_oauth_params: Mapped[str] = mapped_column(db.Text, nullable=False) + encrypted_oauth_params: Mapped[str] = mapped_column(sa.Text, nullable=False) @property def oauth_params(self) -> dict: @@ -65,35 +65,35 @@ class BuiltinToolProvider(Base): __tablename__ = "tool_builtin_providers" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="tool_builtin_provider_pkey"), - db.UniqueConstraint("tenant_id", "provider", "name", name="unique_builtin_tool_provider"), + sa.PrimaryKeyConstraint("id", name="tool_builtin_provider_pkey"), + sa.UniqueConstraint("tenant_id", "provider", "name", name="unique_builtin_tool_provider"), ) # id of the tool provider - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) name: Mapped[str] = mapped_column( - db.String(256), nullable=False, server_default=db.text("'API KEY 1'::character varying") + String(256), nullable=False, server_default=sa.text("'API KEY 1'::character varying") ) # id of the tenant tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=True) # who created this tool provider user_id: Mapped[str] = mapped_column(StringUUID, nullable=False) # name of the tool provider - provider: Mapped[str] = mapped_column(db.String(256), nullable=False) + provider: Mapped[str] = mapped_column(String(256), nullable=False) # credential of the tool provider - encrypted_credentials: Mapped[str] = mapped_column(db.Text, nullable=True) + encrypted_credentials: Mapped[str] = mapped_column(sa.Text, nullable=True) created_at: Mapped[datetime] = mapped_column( - db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)") + sa.DateTime, nullable=False, server_default=sa.text("CURRENT_TIMESTAMP(0)") ) updated_at: Mapped[datetime] = mapped_column( - db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)") + sa.DateTime, nullable=False, server_default=sa.text("CURRENT_TIMESTAMP(0)") ) - is_default: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) + is_default: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false")) # credential type, e.g., "api-key", "oauth2" credential_type: Mapped[str] = mapped_column( - db.String(32), nullable=False, server_default=db.text("'api-key'::character varying") + String(32), nullable=False, server_default=sa.text("'api-key'::character varying") ) - expires_at: Mapped[int] = mapped_column(db.BigInteger, nullable=False, server_default=db.text("-1")) + expires_at: Mapped[int] = mapped_column(sa.BigInteger, nullable=False, server_default=sa.text("-1")) @property def credentials(self) -> dict: @@ -107,35 +107,35 @@ class ApiToolProvider(Base): __tablename__ = "tool_api_providers" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="tool_api_provider_pkey"), - db.UniqueConstraint("name", "tenant_id", name="unique_api_tool_provider"), + sa.PrimaryKeyConstraint("id", name="tool_api_provider_pkey"), + sa.UniqueConstraint("name", "tenant_id", name="unique_api_tool_provider"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) # name of the api provider - name = mapped_column(db.String(255), nullable=False, server_default=db.text("'API KEY 1'::character varying")) + name = mapped_column(String(255), nullable=False, server_default=sa.text("'API KEY 1'::character varying")) # icon - icon = mapped_column(db.String(255), nullable=False) + icon: Mapped[str] = mapped_column(String(255), nullable=False) # original schema - schema = mapped_column(db.Text, nullable=False) - schema_type_str: Mapped[str] = mapped_column(db.String(40), nullable=False) + schema = mapped_column(sa.Text, nullable=False) + schema_type_str: Mapped[str] = mapped_column(String(40), nullable=False) # who created this tool user_id = mapped_column(StringUUID, nullable=False) # tenant id tenant_id = mapped_column(StringUUID, nullable=False) # description of the provider - description = mapped_column(db.Text, nullable=False) + description = mapped_column(sa.Text, nullable=False) # json format tools - tools_str = mapped_column(db.Text, nullable=False) + tools_str = mapped_column(sa.Text, nullable=False) # json format credentials - credentials_str = mapped_column(db.Text, nullable=False) + credentials_str = mapped_column(sa.Text, nullable=False) # privacy policy - privacy_policy = mapped_column(db.String(255), nullable=True) + privacy_policy = mapped_column(String(255), nullable=True) # custom_disclaimer custom_disclaimer: Mapped[str] = mapped_column(sa.TEXT, default="") - created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at: Mapped[datetime] = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at: Mapped[datetime] = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) @property def schema_type(self) -> ApiProviderSchemaType: @@ -167,17 +167,17 @@ class ToolLabelBinding(Base): __tablename__ = "tool_label_bindings" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="tool_label_bind_pkey"), - db.UniqueConstraint("tool_id", "label_name", name="unique_tool_label_bind"), + sa.PrimaryKeyConstraint("id", name="tool_label_bind_pkey"), + sa.UniqueConstraint("tool_id", "label_name", name="unique_tool_label_bind"), ) - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) # tool id - tool_id: Mapped[str] = mapped_column(db.String(64), nullable=False) + tool_id: Mapped[str] = mapped_column(String(64), nullable=False) # tool type - tool_type: Mapped[str] = mapped_column(db.String(40), nullable=False) + tool_type: Mapped[str] = mapped_column(String(40), nullable=False) # label name - label_name: Mapped[str] = mapped_column(db.String(40), nullable=False) + label_name: Mapped[str] = mapped_column(String(40), nullable=False) class WorkflowToolProvider(Base): @@ -187,38 +187,38 @@ class WorkflowToolProvider(Base): __tablename__ = "tool_workflow_providers" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="tool_workflow_provider_pkey"), - db.UniqueConstraint("name", "tenant_id", name="unique_workflow_tool_provider"), - db.UniqueConstraint("tenant_id", "app_id", name="unique_workflow_tool_provider_app_id"), + sa.PrimaryKeyConstraint("id", name="tool_workflow_provider_pkey"), + sa.UniqueConstraint("name", "tenant_id", name="unique_workflow_tool_provider"), + sa.UniqueConstraint("tenant_id", "app_id", name="unique_workflow_tool_provider_app_id"), ) - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) # name of the workflow provider - name: Mapped[str] = mapped_column(db.String(255), nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) # label of the workflow provider - label: Mapped[str] = mapped_column(db.String(255), nullable=False, server_default="") + label: Mapped[str] = mapped_column(String(255), nullable=False, server_default="") # icon - icon: Mapped[str] = mapped_column(db.String(255), nullable=False) + icon: Mapped[str] = mapped_column(String(255), nullable=False) # app id of the workflow provider app_id: Mapped[str] = mapped_column(StringUUID, nullable=False) # version of the workflow provider - version: Mapped[str] = mapped_column(db.String(255), nullable=False, server_default="") + version: Mapped[str] = mapped_column(String(255), nullable=False, server_default="") # who created this tool user_id: Mapped[str] = mapped_column(StringUUID, nullable=False) # tenant id tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) # description of the provider - description: Mapped[str] = mapped_column(db.Text, nullable=False) + description: Mapped[str] = mapped_column(sa.Text, nullable=False) # parameter configuration - parameter_configuration: Mapped[str] = mapped_column(db.Text, nullable=False, server_default="[]") + parameter_configuration: Mapped[str] = mapped_column(sa.Text, nullable=False, server_default="[]") # privacy policy - privacy_policy: Mapped[str] = mapped_column(db.String(255), nullable=True, server_default="") + privacy_policy: Mapped[str] = mapped_column(String(255), nullable=True, server_default="") created_at: Mapped[datetime] = mapped_column( - db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)") + sa.DateTime, nullable=False, server_default=sa.text("CURRENT_TIMESTAMP(0)") ) updated_at: Mapped[datetime] = mapped_column( - db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)") + sa.DateTime, nullable=False, server_default=sa.text("CURRENT_TIMESTAMP(0)") ) @property @@ -245,38 +245,38 @@ class MCPToolProvider(Base): __tablename__ = "tool_mcp_providers" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="tool_mcp_provider_pkey"), - db.UniqueConstraint("tenant_id", "server_url_hash", name="unique_mcp_provider_server_url"), - db.UniqueConstraint("tenant_id", "name", name="unique_mcp_provider_name"), - db.UniqueConstraint("tenant_id", "server_identifier", name="unique_mcp_provider_server_identifier"), + sa.PrimaryKeyConstraint("id", name="tool_mcp_provider_pkey"), + sa.UniqueConstraint("tenant_id", "server_url_hash", name="unique_mcp_provider_server_url"), + sa.UniqueConstraint("tenant_id", "name", name="unique_mcp_provider_name"), + sa.UniqueConstraint("tenant_id", "server_identifier", name="unique_mcp_provider_server_identifier"), ) - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) # name of the mcp provider - name: Mapped[str] = mapped_column(db.String(40), nullable=False) + name: Mapped[str] = mapped_column(String(40), nullable=False) # server identifier of the mcp provider - server_identifier: Mapped[str] = mapped_column(db.String(64), nullable=False) + server_identifier: Mapped[str] = mapped_column(String(64), nullable=False) # encrypted url of the mcp provider - server_url: Mapped[str] = mapped_column(db.Text, nullable=False) + server_url: Mapped[str] = mapped_column(sa.Text, nullable=False) # hash of server_url for uniqueness check - server_url_hash: Mapped[str] = mapped_column(db.String(64), nullable=False) + server_url_hash: Mapped[str] = mapped_column(String(64), nullable=False) # icon of the mcp provider - icon: Mapped[str] = mapped_column(db.String(255), nullable=True) + icon: Mapped[str] = mapped_column(String(255), nullable=True) # tenant id tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) # who created this tool user_id: Mapped[str] = mapped_column(StringUUID, nullable=False) # encrypted credentials - encrypted_credentials: Mapped[str] = mapped_column(db.Text, nullable=True) + encrypted_credentials: Mapped[str] = mapped_column(sa.Text, nullable=True) # authed - authed: Mapped[bool] = mapped_column(db.Boolean, nullable=False, default=False) + authed: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=False) # tools - tools: Mapped[str] = mapped_column(db.Text, nullable=False, default="[]") + tools: Mapped[str] = mapped_column(sa.Text, nullable=False, default="[]") created_at: Mapped[datetime] = mapped_column( - db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)") + sa.DateTime, nullable=False, server_default=sa.text("CURRENT_TIMESTAMP(0)") ) updated_at: Mapped[datetime] = mapped_column( - db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)") + sa.DateTime, nullable=False, server_default=sa.text("CURRENT_TIMESTAMP(0)") ) def load_user(self) -> Account | None: @@ -347,35 +347,35 @@ class ToolModelInvoke(Base): """ __tablename__ = "tool_model_invokes" - __table_args__ = (db.PrimaryKeyConstraint("id", name="tool_model_invoke_pkey"),) + __table_args__ = (sa.PrimaryKeyConstraint("id", name="tool_model_invoke_pkey"),) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) # who invoke this tool user_id = mapped_column(StringUUID, nullable=False) # tenant id tenant_id = mapped_column(StringUUID, nullable=False) # provider - provider = mapped_column(db.String(255), nullable=False) + provider: Mapped[str] = mapped_column(String(255), nullable=False) # type - tool_type = mapped_column(db.String(40), nullable=False) + tool_type = mapped_column(String(40), nullable=False) # tool name - tool_name = mapped_column(db.String(128), nullable=False) + tool_name = mapped_column(String(128), nullable=False) # invoke parameters - model_parameters = mapped_column(db.Text, nullable=False) + model_parameters = mapped_column(sa.Text, nullable=False) # prompt messages - prompt_messages = mapped_column(db.Text, nullable=False) + prompt_messages = mapped_column(sa.Text, nullable=False) # invoke response - model_response = mapped_column(db.Text, nullable=False) + model_response = mapped_column(sa.Text, nullable=False) - prompt_tokens = mapped_column(db.Integer, nullable=False, server_default=db.text("0")) - answer_tokens = mapped_column(db.Integer, nullable=False, server_default=db.text("0")) - answer_unit_price = mapped_column(db.Numeric(10, 4), nullable=False) - answer_price_unit = mapped_column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001")) - provider_response_latency = mapped_column(db.Float, nullable=False, server_default=db.text("0")) - total_price = mapped_column(db.Numeric(10, 7)) - currency = mapped_column(db.String(255), nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + prompt_tokens: Mapped[int] = mapped_column(sa.Integer, nullable=False, server_default=sa.text("0")) + answer_tokens: Mapped[int] = mapped_column(sa.Integer, nullable=False, server_default=sa.text("0")) + answer_unit_price = mapped_column(sa.Numeric(10, 4), nullable=False) + answer_price_unit = mapped_column(sa.Numeric(10, 7), nullable=False, server_default=sa.text("0.001")) + provider_response_latency = mapped_column(sa.Float, nullable=False, server_default=sa.text("0")) + total_price = mapped_column(sa.Numeric(10, 7)) + currency: Mapped[str] = mapped_column(String(255), nullable=False) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) @deprecated @@ -386,13 +386,13 @@ class ToolConversationVariables(Base): __tablename__ = "tool_conversation_variables" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="tool_conversation_variables_pkey"), + sa.PrimaryKeyConstraint("id", name="tool_conversation_variables_pkey"), # add index for user_id and conversation_id - db.Index("user_id_idx", "user_id"), - db.Index("conversation_id_idx", "conversation_id"), + sa.Index("user_id_idx", "user_id"), + sa.Index("conversation_id_idx", "conversation_id"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) # conversation user id user_id = mapped_column(StringUUID, nullable=False) # tenant id @@ -400,10 +400,10 @@ class ToolConversationVariables(Base): # conversation id conversation_id = mapped_column(StringUUID, nullable=False) # variables pool - variables_str = mapped_column(db.Text, nullable=False) + variables_str = mapped_column(sa.Text, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - updated_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) @property def variables(self) -> Any: @@ -417,11 +417,11 @@ class ToolFile(Base): __tablename__ = "tool_files" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="tool_file_pkey"), - db.Index("tool_file_conversation_id_idx", "conversation_id"), + sa.PrimaryKeyConstraint("id", name="tool_file_pkey"), + sa.Index("tool_file_conversation_id_idx", "conversation_id"), ) - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) # conversation user id user_id: Mapped[str] = mapped_column(StringUUID) # tenant id @@ -429,11 +429,11 @@ class ToolFile(Base): # conversation id conversation_id: Mapped[str] = mapped_column(StringUUID, nullable=True) # file key - file_key: Mapped[str] = mapped_column(db.String(255), nullable=False) + file_key: Mapped[str] = mapped_column(String(255), nullable=False) # mime type - mimetype: Mapped[str] = mapped_column(db.String(255), nullable=False) + mimetype: Mapped[str] = mapped_column(String(255), nullable=False) # original url - original_url: Mapped[str] = mapped_column(db.String(2048), nullable=True) + original_url: Mapped[str] = mapped_column(String(2048), nullable=True) # name name: Mapped[str] = mapped_column(default="") # size @@ -448,30 +448,30 @@ class DeprecatedPublishedAppTool(Base): __tablename__ = "tool_published_apps" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="published_app_tool_pkey"), - db.UniqueConstraint("app_id", "user_id", name="unique_published_app_tool"), + sa.PrimaryKeyConstraint("id", name="published_app_tool_pkey"), + sa.UniqueConstraint("app_id", "user_id", name="unique_published_app_tool"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) # id of the app app_id = mapped_column(StringUUID, ForeignKey("apps.id"), nullable=False) user_id: Mapped[str] = mapped_column(StringUUID, nullable=False) # who published this tool - description = mapped_column(db.Text, nullable=False) + description = mapped_column(sa.Text, nullable=False) # llm_description of the tool, for LLM - llm_description = mapped_column(db.Text, nullable=False) + llm_description = mapped_column(sa.Text, nullable=False) # query description, query will be seem as a parameter of the tool, # to describe this parameter to llm, we need this field - query_description = mapped_column(db.Text, nullable=False) + query_description = mapped_column(sa.Text, nullable=False) # query name, the name of the query parameter - query_name = mapped_column(db.String(40), nullable=False) + query_name = mapped_column(String(40), nullable=False) # name of the tool provider - tool_name = mapped_column(db.String(40), nullable=False) + tool_name = mapped_column(String(40), nullable=False) # author - author = mapped_column(db.String(40), nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) - updated_at = mapped_column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")) + author = mapped_column(String(40), nullable=False) + created_at = mapped_column(sa.DateTime, nullable=False, server_default=sa.text("CURRENT_TIMESTAMP(0)")) + updated_at = mapped_column(sa.DateTime, nullable=False, server_default=sa.text("CURRENT_TIMESTAMP(0)")) @property def description_i18n(self) -> I18nObject: diff --git a/api/models/web.py b/api/models/web.py index ce00f4010f..74f99e187b 100644 --- a/api/models/web.py +++ b/api/models/web.py @@ -1,4 +1,7 @@ -from sqlalchemy import func +from datetime import datetime + +import sqlalchemy as sa +from sqlalchemy import DateTime, String, func from sqlalchemy.orm import Mapped, mapped_column from models.base import Base @@ -11,18 +14,18 @@ from .types import StringUUID class SavedMessage(Base): __tablename__ = "saved_messages" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="saved_message_pkey"), - db.Index("saved_message_message_idx", "app_id", "message_id", "created_by_role", "created_by"), + sa.PrimaryKeyConstraint("id", name="saved_message_pkey"), + sa.Index("saved_message_message_idx", "app_id", "message_id", "created_by_role", "created_by"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) app_id = mapped_column(StringUUID, nullable=False) message_id = mapped_column(StringUUID, nullable=False) created_by_role = mapped_column( - db.String(255), nullable=False, server_default=db.text("'end_user'::character varying") + String(255), nullable=False, server_default=sa.text("'end_user'::character varying") ) created_by = mapped_column(StringUUID, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) @property def message(self): @@ -32,15 +35,15 @@ class SavedMessage(Base): class PinnedConversation(Base): __tablename__ = "pinned_conversations" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="pinned_conversation_pkey"), - db.Index("pinned_conversation_conversation_idx", "app_id", "conversation_id", "created_by_role", "created_by"), + sa.PrimaryKeyConstraint("id", name="pinned_conversation_pkey"), + sa.Index("pinned_conversation_conversation_idx", "app_id", "conversation_id", "created_by_role", "created_by"), ) - id = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) app_id = mapped_column(StringUUID, nullable=False) conversation_id: Mapped[str] = mapped_column(StringUUID) created_by_role = mapped_column( - db.String(255), nullable=False, server_default=db.text("'end_user'::character varying") + String(255), nullable=False, server_default=sa.text("'end_user'::character varying") ) created_by = mapped_column(StringUUID, nullable=False) - created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) diff --git a/api/models/workflow.py b/api/models/workflow.py index e67d592ecf..a83d7d07c5 100644 --- a/api/models/workflow.py +++ b/api/models/workflow.py @@ -6,8 +6,9 @@ from enum import Enum, StrEnum from typing import TYPE_CHECKING, Any, Optional, Union from uuid import uuid4 +import sqlalchemy as sa from flask_login import current_user -from sqlalchemy import orm +from sqlalchemy import DateTime, orm from core.file.constants import maybe_file_object from core.file.models import File @@ -24,8 +25,7 @@ from ._workflow_exc import NodeNotFoundError, WorkflowDataError if TYPE_CHECKING: from models.model import AppMode -import sqlalchemy as sa -from sqlalchemy import Index, PrimaryKeyConstraint, UniqueConstraint, func +from sqlalchemy import Index, PrimaryKeyConstraint, String, UniqueConstraint, func from sqlalchemy.orm import Mapped, declared_attr, mapped_column from constants import DEFAULT_FILE_NUMBER_LIMITS, HIDDEN_VALUE @@ -118,33 +118,33 @@ class Workflow(Base): __tablename__ = "workflows" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="workflow_pkey"), - db.Index("workflow_version_idx", "tenant_id", "app_id", "version"), + sa.PrimaryKeyConstraint("id", name="workflow_pkey"), + sa.Index("workflow_version_idx", "tenant_id", "app_id", "version"), ) - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) app_id: Mapped[str] = mapped_column(StringUUID, nullable=False) - type: Mapped[str] = mapped_column(db.String(255), nullable=False) - version: Mapped[str] = mapped_column(db.String(255), nullable=False) + type: Mapped[str] = mapped_column(String(255), nullable=False) + version: Mapped[str] = mapped_column(String(255), nullable=False) marked_name: Mapped[str] = mapped_column(default="", server_default="") marked_comment: Mapped[str] = mapped_column(default="", server_default="") graph: Mapped[str] = mapped_column(sa.Text) _features: Mapped[str] = mapped_column("features", sa.TEXT) created_by: Mapped[str] = mapped_column(StringUUID, nullable=False) - created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) updated_by: Mapped[Optional[str]] = mapped_column(StringUUID) updated_at: Mapped[datetime] = mapped_column( - db.DateTime, + DateTime, nullable=False, default=naive_utc_now(), server_onupdate=func.current_timestamp(), ) _environment_variables: Mapped[str] = mapped_column( - "environment_variables", db.Text, nullable=False, server_default="{}" + "environment_variables", sa.Text, nullable=False, server_default="{}" ) _conversation_variables: Mapped[str] = mapped_column( - "conversation_variables", db.Text, nullable=False, server_default="{}" + "conversation_variables", sa.Text, nullable=False, server_default="{}" ) _rag_pipeline_variables: Mapped[str] = mapped_column( "rag_pipeline_variables", db.Text, nullable=False, server_default="{}" @@ -521,31 +521,31 @@ class WorkflowRun(Base): __tablename__ = "workflow_runs" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="workflow_run_pkey"), - db.Index("workflow_run_triggerd_from_idx", "tenant_id", "app_id", "triggered_from"), + sa.PrimaryKeyConstraint("id", name="workflow_run_pkey"), + sa.Index("workflow_run_triggerd_from_idx", "tenant_id", "app_id", "triggered_from"), ) - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id: Mapped[str] = mapped_column(StringUUID) app_id: Mapped[str] = mapped_column(StringUUID) workflow_id: Mapped[str] = mapped_column(StringUUID) - type: Mapped[str] = mapped_column(db.String(255)) - triggered_from: Mapped[str] = mapped_column(db.String(255)) - version: Mapped[str] = mapped_column(db.String(255)) - graph: Mapped[Optional[str]] = mapped_column(db.Text) - inputs: Mapped[Optional[str]] = mapped_column(db.Text) - status: Mapped[str] = mapped_column(db.String(255)) # running, succeeded, failed, stopped, partial-succeeded + type: Mapped[str] = mapped_column(String(255)) + triggered_from: Mapped[str] = mapped_column(String(255)) + version: Mapped[str] = mapped_column(String(255)) + graph: Mapped[Optional[str]] = mapped_column(sa.Text) + inputs: Mapped[Optional[str]] = mapped_column(sa.Text) + status: Mapped[str] = mapped_column(String(255)) # running, succeeded, failed, stopped, partial-succeeded outputs: Mapped[Optional[str]] = mapped_column(sa.Text, default="{}") - error: Mapped[Optional[str]] = mapped_column(db.Text) - elapsed_time: Mapped[float] = mapped_column(db.Float, nullable=False, server_default=sa.text("0")) + error: Mapped[Optional[str]] = mapped_column(sa.Text) + elapsed_time: Mapped[float] = mapped_column(sa.Float, nullable=False, server_default=sa.text("0")) total_tokens: Mapped[int] = mapped_column(sa.BigInteger, server_default=sa.text("0")) - total_steps: Mapped[int] = mapped_column(db.Integer, server_default=db.text("0"), nullable=True) - created_by_role: Mapped[str] = mapped_column(db.String(255)) # account, end_user + total_steps: Mapped[int] = mapped_column(sa.Integer, server_default=sa.text("0"), nullable=True) + created_by_role: Mapped[str] = mapped_column(String(255)) # account, end_user created_by: Mapped[str] = mapped_column(StringUUID, nullable=False) - created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) - finished_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime) - exceptions_count: Mapped[int] = mapped_column(db.Integer, server_default=db.text("0"), nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) + finished_at: Mapped[Optional[datetime]] = mapped_column(DateTime) + exceptions_count: Mapped[int] = mapped_column(sa.Integer, server_default=sa.text("0"), nullable=True) @property def created_by_account(self): @@ -735,29 +735,29 @@ class WorkflowNodeExecutionModel(Base): ), ) - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id: Mapped[str] = mapped_column(StringUUID) app_id: Mapped[str] = mapped_column(StringUUID) workflow_id: Mapped[str] = mapped_column(StringUUID) - triggered_from: Mapped[str] = mapped_column(db.String(255)) + triggered_from: Mapped[str] = mapped_column(String(255)) workflow_run_id: Mapped[Optional[str]] = mapped_column(StringUUID) - index: Mapped[int] = mapped_column(db.Integer) - predecessor_node_id: Mapped[Optional[str]] = mapped_column(db.String(255)) - node_execution_id: Mapped[Optional[str]] = mapped_column(db.String(255)) - node_id: Mapped[str] = mapped_column(db.String(255)) - node_type: Mapped[str] = mapped_column(db.String(255)) - title: Mapped[str] = mapped_column(db.String(255)) - inputs: Mapped[Optional[str]] = mapped_column(db.Text) - process_data: Mapped[Optional[str]] = mapped_column(db.Text) - outputs: Mapped[Optional[str]] = mapped_column(db.Text) - status: Mapped[str] = mapped_column(db.String(255)) - error: Mapped[Optional[str]] = mapped_column(db.Text) - elapsed_time: Mapped[float] = mapped_column(db.Float, server_default=db.text("0")) - execution_metadata: Mapped[Optional[str]] = mapped_column(db.Text) - created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp()) - created_by_role: Mapped[str] = mapped_column(db.String(255)) + index: Mapped[int] = mapped_column(sa.Integer) + predecessor_node_id: Mapped[Optional[str]] = mapped_column(String(255)) + node_execution_id: Mapped[Optional[str]] = mapped_column(String(255)) + node_id: Mapped[str] = mapped_column(String(255)) + node_type: Mapped[str] = mapped_column(String(255)) + title: Mapped[str] = mapped_column(String(255)) + inputs: Mapped[Optional[str]] = mapped_column(sa.Text) + process_data: Mapped[Optional[str]] = mapped_column(sa.Text) + outputs: Mapped[Optional[str]] = mapped_column(sa.Text) + status: Mapped[str] = mapped_column(String(255)) + error: Mapped[Optional[str]] = mapped_column(sa.Text) + elapsed_time: Mapped[float] = mapped_column(sa.Float, server_default=sa.text("0")) + execution_metadata: Mapped[Optional[str]] = mapped_column(sa.Text) + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.current_timestamp()) + created_by_role: Mapped[str] = mapped_column(String(255)) created_by: Mapped[str] = mapped_column(StringUUID) - finished_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime) + finished_at: Mapped[Optional[datetime]] = mapped_column(DateTime) @property def created_by_account(self): @@ -865,19 +865,19 @@ class WorkflowAppLog(Base): __tablename__ = "workflow_app_logs" __table_args__ = ( - db.PrimaryKeyConstraint("id", name="workflow_app_log_pkey"), - db.Index("workflow_app_log_app_idx", "tenant_id", "app_id"), + sa.PrimaryKeyConstraint("id", name="workflow_app_log_pkey"), + sa.Index("workflow_app_log_app_idx", "tenant_id", "app_id"), ) - id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()")) tenant_id: Mapped[str] = mapped_column(StringUUID) app_id: Mapped[str] = mapped_column(StringUUID) workflow_id: Mapped[str] = mapped_column(StringUUID, nullable=False) workflow_run_id: Mapped[str] = mapped_column(StringUUID) - created_from: Mapped[str] = mapped_column(db.String(255), nullable=False) - created_by_role: Mapped[str] = mapped_column(db.String(255), nullable=False) + created_from: Mapped[str] = mapped_column(String(255), nullable=False) + created_by_role: Mapped[str] = mapped_column(String(255), nullable=False) created_by: Mapped[str] = mapped_column(StringUUID, nullable=False) - created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) @property def workflow_run(self): @@ -902,12 +902,12 @@ class ConversationVariable(Base): id: Mapped[str] = mapped_column(StringUUID, primary_key=True) conversation_id: Mapped[str] = mapped_column(StringUUID, nullable=False, primary_key=True, index=True) app_id: Mapped[str] = mapped_column(StringUUID, nullable=False, index=True) - data: Mapped[str] = mapped_column(db.Text, nullable=False) + data: Mapped[str] = mapped_column(sa.Text, nullable=False) created_at: Mapped[datetime] = mapped_column( - db.DateTime, nullable=False, server_default=func.current_timestamp(), index=True + DateTime, nullable=False, server_default=func.current_timestamp(), index=True ) updated_at: Mapped[datetime] = mapped_column( - db.DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp() + DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp() ) def __init__(self, *, id: str, app_id: str, conversation_id: str, data: str) -> None: @@ -964,17 +964,17 @@ class WorkflowDraftVariable(Base): __allow_unmapped__ = True # id is the unique identifier of a draft variable. - id: Mapped[str] = mapped_column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()")) + id: Mapped[str] = mapped_column(StringUUID, primary_key=True, server_default=sa.text("uuid_generate_v4()")) created_at: Mapped[datetime] = mapped_column( - db.DateTime, + DateTime, nullable=False, default=_naive_utc_datetime, server_default=func.current_timestamp(), ) updated_at: Mapped[datetime] = mapped_column( - db.DateTime, + DateTime, nullable=False, default=_naive_utc_datetime, server_default=func.current_timestamp(), @@ -989,7 +989,7 @@ class WorkflowDraftVariable(Base): # # If it's not edited after creation, its value is `None`. last_edited_at: Mapped[datetime | None] = mapped_column( - db.DateTime, + DateTime, nullable=True, default=None, ) diff --git a/api/pyproject.toml b/api/pyproject.toml index be42b509ed..d8f663ef8d 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -114,6 +114,7 @@ dev = [ "pytest-cov~=4.1.0", "pytest-env~=1.1.3", "pytest-mock~=3.14.0", + "testcontainers~=4.10.0", "types-aiofiles~=24.1.0", "types-beautifulsoup4~=4.12.0", "types-cachetools~=5.5.0", diff --git a/api/schedule/queue_monitor_task.py b/api/schedule/queue_monitor_task.py index 4d517e5498..f0d3bed057 100644 --- a/api/schedule/queue_monitor_task.py +++ b/api/schedule/queue_monitor_task.py @@ -12,10 +12,10 @@ from libs.email_i18n import EmailType, get_email_i18n_service redis_config = parse_url(dify_config.CELERY_BROKER_URL) celery_redis = Redis( - host=redis_config["hostname"], - port=redis_config["port"], - password=redis_config["password"], - db=int(redis_config["virtual_host"]) if redis_config["virtual_host"] else 1, + host=redis_config.get("hostname") or "localhost", + port=redis_config.get("port") or 6379, + password=redis_config.get("password") or None, + db=int(redis_config.get("virtual_host")) if redis_config.get("virtual_host") else 1, ) diff --git a/api/services/app_dsl_service.py b/api/services/app_dsl_service.py index fe0efd061d..2aa9f6cabd 100644 --- a/api/services/app_dsl_service.py +++ b/api/services/app_dsl_service.py @@ -12,6 +12,7 @@ import yaml # type: ignore from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad from packaging import version +from packaging.version import parse as parse_version from pydantic import BaseModel, Field from sqlalchemy import select from sqlalchemy.orm import Session @@ -269,7 +270,7 @@ class AppDslService: check_dependencies_pending_data = None if dependencies: check_dependencies_pending_data = [PluginDependency.model_validate(d) for d in dependencies] - elif imported_version <= "0.1.5": + elif parse_version(imported_version) <= parse_version("0.1.5"): if "workflow" in data: graph = data.get("workflow", {}).get("graph", {}) dependencies_list = self._extract_dependencies_from_workflow_graph(graph) diff --git a/api/services/app_generate_service.py b/api/services/app_generate_service.py index 6f7e705b52..6792324ec8 100644 --- a/api/services/app_generate_service.py +++ b/api/services/app_generate_service.py @@ -1,5 +1,6 @@ +import uuid from collections.abc import Generator, Mapping -from typing import Any, Union +from typing import Any, Optional, Union from openai._exceptions import RateLimitError @@ -15,6 +16,7 @@ from libs.helper import RateLimiter from models.model import Account, App, AppMode, EndUser from models.workflow import Workflow from services.billing_service import BillingService +from services.errors.app import WorkflowIdFormatError, WorkflowNotFoundError from services.errors.llm import InvokeRateLimitError from services.workflow_service import WorkflowService @@ -86,7 +88,8 @@ class AppGenerateService: request_id=request_id, ) elif app_model.mode == AppMode.ADVANCED_CHAT.value: - workflow = cls._get_workflow(app_model, invoke_from) + workflow_id = args.get("workflow_id") + workflow = cls._get_workflow(app_model, invoke_from, workflow_id) return rate_limit.generate( AdvancedChatAppGenerator.convert_to_event_stream( AdvancedChatAppGenerator().generate( @@ -101,7 +104,8 @@ class AppGenerateService: request_id=request_id, ) elif app_model.mode == AppMode.WORKFLOW.value: - workflow = cls._get_workflow(app_model, invoke_from) + workflow_id = args.get("workflow_id") + workflow = cls._get_workflow(app_model, invoke_from, workflow_id) return rate_limit.generate( WorkflowAppGenerator.convert_to_event_stream( WorkflowAppGenerator().generate( @@ -210,14 +214,27 @@ class AppGenerateService: ) @classmethod - def _get_workflow(cls, app_model: App, invoke_from: InvokeFrom) -> Workflow: + def _get_workflow(cls, app_model: App, invoke_from: InvokeFrom, workflow_id: Optional[str] = None) -> Workflow: """ Get workflow :param app_model: app model :param invoke_from: invoke from + :param workflow_id: optional workflow id to specify a specific version :return: """ workflow_service = WorkflowService() + + # If workflow_id is specified, get the specific workflow version + if workflow_id: + try: + workflow_uuid = uuid.UUID(workflow_id) + except ValueError: + raise WorkflowIdFormatError(f"Invalid workflow_id format: '{workflow_id}'. ") + workflow = workflow_service.get_published_workflow_by_id(app_model=app_model, workflow_id=workflow_id) + if not workflow: + raise WorkflowNotFoundError(f"Workflow not found with id: {workflow_id}") + return workflow + if invoke_from == InvokeFrom.DEBUGGER: # fetch draft workflow by app_model workflow = workflow_service.get_draft_workflow(app_model=app_model) diff --git a/api/services/billing_service.py b/api/services/billing_service.py index 5a12aa2e54..476fce0057 100644 --- a/api/services/billing_service.py +++ b/api/services/billing_service.py @@ -159,9 +159,9 @@ class BillingService: ): limiter_key = f"{account_id}:{tenant_id}" if cls.compliance_download_rate_limiter.is_rate_limited(limiter_key): - from controllers.console.error import CompilanceRateLimitError + from controllers.console.error import ComplianceRateLimitError - raise CompilanceRateLimitError() + raise ComplianceRateLimitError() json = { "doc_name": doc_name, diff --git a/api/services/conversation_service.py b/api/services/conversation_service.py index 206c832a20..692a3639cd 100644 --- a/api/services/conversation_service.py +++ b/api/services/conversation_service.py @@ -1,12 +1,15 @@ from collections.abc import Callable, Sequence -from typing import Optional, Union +from typing import Any, Optional, Union from sqlalchemy import asc, desc, func, or_, select from sqlalchemy.orm import Session from core.app.entities.app_invoke_entities import InvokeFrom from core.llm_generator.llm_generator import LLMGenerator +from core.variables.types import SegmentType +from core.workflow.nodes.variable_assigner.common.impl import conversation_variable_updater_factory from extensions.ext_database import db +from factories import variable_factory from libs.datetime_utils import naive_utc_now from libs.infinite_scroll_pagination import InfiniteScrollPagination from models import ConversationVariable @@ -15,6 +18,7 @@ from models.model import App, Conversation, EndUser, Message from services.errors.conversation import ( ConversationNotExistsError, ConversationVariableNotExistsError, + ConversationVariableTypeMismatchError, LastConversationNotExistsError, ) from services.errors.message import MessageNotExistsError @@ -220,3 +224,82 @@ class ConversationService: ] return InfiniteScrollPagination(variables, limit, has_more) + + @classmethod + def update_conversation_variable( + cls, + app_model: App, + conversation_id: str, + variable_id: str, + user: Optional[Union[Account, EndUser]], + new_value: Any, + ) -> dict: + """ + Update a conversation variable's value. + + Args: + app_model: The app model + conversation_id: The conversation ID + variable_id: The variable ID to update + user: The user (Account or EndUser) + new_value: The new value for the variable + + Returns: + Dictionary containing the updated variable information + + Raises: + ConversationNotExistsError: If the conversation doesn't exist + ConversationVariableNotExistsError: If the variable doesn't exist + ConversationVariableTypeMismatchError: If the new value type doesn't match the variable's expected type + """ + # Verify conversation exists and user has access + conversation = cls.get_conversation(app_model, conversation_id, user) + + # Get the existing conversation variable + stmt = ( + select(ConversationVariable) + .where(ConversationVariable.app_id == app_model.id) + .where(ConversationVariable.conversation_id == conversation.id) + .where(ConversationVariable.id == variable_id) + ) + + with Session(db.engine) as session: + existing_variable = session.scalar(stmt) + if not existing_variable: + raise ConversationVariableNotExistsError() + + # Convert existing variable to Variable object + current_variable = existing_variable.to_variable() + + # Validate that the new value type matches the expected variable type + expected_type = SegmentType(current_variable.value_type) + if not expected_type.is_valid(new_value): + inferred_type = SegmentType.infer_segment_type(new_value) + raise ConversationVariableTypeMismatchError( + f"Type mismatch: variable '{current_variable.name}' expects {expected_type.value}, " + f"but got {inferred_type.value if inferred_type else 'unknown'} type" + ) + + # Create updated variable with new value only, preserving everything else + updated_variable_dict = { + "id": current_variable.id, + "name": current_variable.name, + "description": current_variable.description, + "value_type": current_variable.value_type, + "value": new_value, + "selector": current_variable.selector, + } + + updated_variable = variable_factory.build_conversation_variable_from_mapping(updated_variable_dict) + + # Use the conversation variable updater to persist the changes + updater = conversation_variable_updater_factory() + updater.update(conversation_id, updated_variable) + updater.flush() + + # Return the updated variable data + return { + "created_at": existing_variable.created_at, + "updated_at": naive_utc_now(), # Update timestamp + **updated_variable.model_dump(), + } diff --git a/api/services/dataset_service.py b/api/services/dataset_service.py index 76726d3287..ad7f9bb30f 100644 --- a/api/services/dataset_service.py +++ b/api/services/dataset_service.py @@ -311,7 +311,7 @@ class DatasetService: "No Embedding Model available. Please configure a valid provider in the Settings -> Model Provider." ) except ProviderTokenNotInitError as ex: - raise ValueError(f"The dataset in unavailable, due to: {ex.description}") + raise ValueError(f"The dataset is unavailable, due to: {ex.description}") @staticmethod def check_embedding_model_setting(tenant_id: str, embedding_model_provider: str, embedding_model: str): @@ -426,7 +426,7 @@ class DatasetService: raise ValueError("External knowledge api id is required.") # Update metadata fields dataset.updated_by = user.id if user else None - dataset.updated_at = datetime.datetime.utcnow() + dataset.updated_at = naive_utc_now() db.session.add(dataset) # Update external knowledge binding @@ -2498,6 +2498,7 @@ class SegmentService: db.session.add(segment_document) # update document word count + assert document.word_count is not None document.word_count += segment_document.word_count db.session.add(document) db.session.commit() @@ -2582,6 +2583,7 @@ class SegmentService: else: keywords_list.append(None) # update document word count + assert document.word_count is not None document.word_count += increment_word_count db.session.add(document) try: @@ -2643,6 +2645,7 @@ class SegmentService: db.session.commit() # update document word count if word_count_change != 0: + assert document.word_count is not None document.word_count = max(0, document.word_count + word_count_change) db.session.add(document) # update segment index task @@ -2718,6 +2721,7 @@ class SegmentService: word_count_change = segment.word_count - word_count_change # update document word count if word_count_change != 0: + assert document.word_count is not None document.word_count = max(0, document.word_count + word_count_change) db.session.add(document) db.session.add(segment) @@ -2781,6 +2785,7 @@ class SegmentService: delete_segment_from_index_task.delay([segment.index_node_id], dataset.id, document.id) db.session.delete(segment) # update document word count + assert document.word_count is not None document.word_count -= segment.word_count db.session.add(document) db.session.commit() @@ -2825,7 +2830,7 @@ class SegmentService: ) if not segments: return - real_deal_segmment_ids = [] + real_deal_segment_ids = [] for segment in segments: indexing_cache_key = f"segment_{segment.id}_indexing" cache_result = redis_client.get(indexing_cache_key) @@ -2835,10 +2840,10 @@ class SegmentService: segment.disabled_at = None segment.disabled_by = None db.session.add(segment) - real_deal_segmment_ids.append(segment.id) + real_deal_segment_ids.append(segment.id) db.session.commit() - enable_segments_to_index_task.delay(real_deal_segmment_ids, dataset.id, document.id) + enable_segments_to_index_task.delay(real_deal_segment_ids, dataset.id, document.id) elif action == "disable": segments = ( db.session.query(DocumentSegment) @@ -2852,7 +2857,7 @@ class SegmentService: ) if not segments: return - real_deal_segmment_ids = [] + real_deal_segment_ids = [] for segment in segments: indexing_cache_key = f"segment_{segment.id}_indexing" cache_result = redis_client.get(indexing_cache_key) @@ -2862,10 +2867,10 @@ class SegmentService: segment.disabled_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) segment.disabled_by = current_user.id db.session.add(segment) - real_deal_segmment_ids.append(segment.id) + real_deal_segment_ids.append(segment.id) db.session.commit() - disable_segments_from_index_task.delay(real_deal_segmment_ids, dataset.id, document.id) + disable_segments_from_index_task.delay(real_deal_segment_ids, dataset.id, document.id) else: raise InvalidActionError() @@ -3123,7 +3128,7 @@ class SegmentService: # check segment segment = ( db.session.query(DocumentSegment) - .where(DocumentSegment.id == segment_id, DocumentSegment.tenant_id == user_id) + .where(DocumentSegment.id == segment_id, DocumentSegment.tenant_id == tenant_id) .first() ) if not segment: diff --git a/api/services/errors/app.py b/api/services/errors/app.py index 5d348c61be..390716a47f 100644 --- a/api/services/errors/app.py +++ b/api/services/errors/app.py @@ -8,3 +8,11 @@ class WorkflowHashNotEqualError(Exception): class IsDraftWorkflowError(Exception): pass + + +class WorkflowNotFoundError(Exception): + pass + + +class WorkflowIdFormatError(Exception): + pass diff --git a/api/services/errors/conversation.py b/api/services/errors/conversation.py index f8051e3417..a123f99b59 100644 --- a/api/services/errors/conversation.py +++ b/api/services/errors/conversation.py @@ -15,3 +15,7 @@ class ConversationCompletedError(Exception): class ConversationVariableNotExistsError(BaseServiceError): pass + + +class ConversationVariableTypeMismatchError(BaseServiceError): + pass diff --git a/api/services/metadata_service.py b/api/services/metadata_service.py index cfcb121153..2a83588f41 100644 --- a/api/services/metadata_service.py +++ b/api/services/metadata_service.py @@ -79,7 +79,10 @@ class MetadataService: document_ids = [binding.document_id for binding in dataset_metadata_bindings] documents = DocumentService.get_document_by_ids(document_ids) for document in documents: - doc_metadata = copy.deepcopy(document.doc_metadata) + if not document.doc_metadata: + doc_metadata = {} + else: + doc_metadata = copy.deepcopy(document.doc_metadata) value = doc_metadata.pop(old_name, None) doc_metadata[name] = value document.doc_metadata = doc_metadata @@ -109,7 +112,10 @@ class MetadataService: document_ids = [binding.document_id for binding in dataset_metadata_bindings] documents = DocumentService.get_document_by_ids(document_ids) for document in documents: - doc_metadata = copy.deepcopy(document.doc_metadata) + if not document.doc_metadata: + doc_metadata = {} + else: + doc_metadata = copy.deepcopy(document.doc_metadata) doc_metadata.pop(metadata.name, None) document.doc_metadata = doc_metadata db.session.add(document) @@ -137,7 +143,6 @@ class MetadataService: lock_key = f"dataset_metadata_lock_{dataset.id}" try: MetadataService.knowledge_base_metadata_lock_check(dataset.id, None) - dataset.built_in_field_enabled = True db.session.add(dataset) documents = DocumentService.get_working_documents_by_dataset_id(dataset.id) if documents: @@ -153,6 +158,7 @@ class MetadataService: doc_metadata[BuiltInField.source.value] = MetadataDataSource[document.data_source_type].value document.doc_metadata = doc_metadata db.session.add(document) + dataset.built_in_field_enabled = True db.session.commit() except Exception: logging.exception("Enable built-in field failed") @@ -166,13 +172,15 @@ class MetadataService: lock_key = f"dataset_metadata_lock_{dataset.id}" try: MetadataService.knowledge_base_metadata_lock_check(dataset.id, None) - dataset.built_in_field_enabled = False db.session.add(dataset) documents = DocumentService.get_working_documents_by_dataset_id(dataset.id) document_ids = [] if documents: for document in documents: - doc_metadata = copy.deepcopy(document.doc_metadata) + if not document.doc_metadata: + doc_metadata = {} + else: + doc_metadata = copy.deepcopy(document.doc_metadata) doc_metadata.pop(BuiltInField.document_name.value, None) doc_metadata.pop(BuiltInField.uploader.value, None) doc_metadata.pop(BuiltInField.upload_date.value, None) @@ -181,6 +189,7 @@ class MetadataService: document.doc_metadata = doc_metadata db.session.add(document) document_ids.append(document.id) + dataset.built_in_field_enabled = False db.session.commit() except Exception: logging.exception("Disable built-in field failed") diff --git a/api/services/plugin/data_migration.py b/api/services/plugin/data_migration.py index 7a4f886bf5..c5ad65ec87 100644 --- a/api/services/plugin/data_migration.py +++ b/api/services/plugin/data_migration.py @@ -2,6 +2,7 @@ import json import logging import click +import sqlalchemy as sa from core.plugin.entities.plugin import GenericProviderID, ModelProviderID, ToolProviderID from models.engine import db @@ -38,7 +39,7 @@ class PluginDataMigration: where {provider_column_name} not like '%/%' and {provider_column_name} is not null and {provider_column_name} != '' limit 1000""" with db.engine.begin() as conn: - rs = conn.execute(db.text(sql)) + rs = conn.execute(sa.text(sql)) current_iter_count = 0 for i in rs: @@ -94,7 +95,7 @@ limit 1000""" :provider_name {update_retrieval_model_sql} where id = :record_id""" - conn.execute(db.text(sql), params) + conn.execute(sa.text(sql), params) click.echo( click.style( f"[{processed_count}] Migrated [{table_name}] {record_id} ({provider_name})", @@ -148,7 +149,7 @@ limit 1000""" params = {"last_id": last_id or ""} with db.engine.begin() as conn: - rs = conn.execute(db.text(sql), params) + rs = conn.execute(sa.text(sql), params) current_iter_count = 0 batch_updates = [] @@ -193,7 +194,7 @@ limit 1000""" SET {provider_column_name} = :updated_value WHERE id = :record_id """ - conn.execute(db.text(update_sql), [{"updated_value": u, "record_id": r} for u, r in batch_updates]) + conn.execute(sa.text(update_sql), [{"updated_value": u, "record_id": r} for u, r in batch_updates]) click.echo( click.style( f"[{processed_count}] Batch migrated [{len(batch_updates)}] records from [{table_name}]", diff --git a/api/services/plugin/plugin_migration.py b/api/services/plugin/plugin_migration.py index 222d70a317..221069b2b3 100644 --- a/api/services/plugin/plugin_migration.py +++ b/api/services/plugin/plugin_migration.py @@ -9,6 +9,7 @@ from typing import Any, Optional from uuid import uuid4 import click +import sqlalchemy as sa import tqdm from flask import Flask, current_app from sqlalchemy.orm import Session @@ -197,7 +198,7 @@ class PluginMigration: """ with Session(db.engine) as session: rs = session.execute( - db.text(f"SELECT DISTINCT {column} FROM {table} WHERE tenant_id = :tenant_id"), {"tenant_id": tenant_id} + sa.text(f"SELECT DISTINCT {column} FROM {table} WHERE tenant_id = :tenant_id"), {"tenant_id": tenant_id} ) result = [] for row in rs: diff --git a/api/services/tools/builtin_tools_manage_service.py b/api/services/tools/builtin_tools_manage_service.py index 02c78a0833..d781ee07c8 100644 --- a/api/services/tools/builtin_tools_manage_service.py +++ b/api/services/tools/builtin_tools_manage_service.py @@ -486,10 +486,10 @@ class BuiltinToolManageService: oauth_params = encrypter.decrypt(user_client.oauth_params) return oauth_params - # only verified provider can use custom oauth client - is_verified = not isinstance(provider, PluginToolProviderController) or PluginService.is_plugin_verified( - tenant_id, provider.plugin_unique_identifier - ) + # only verified provider can use official oauth client + is_verified = not isinstance( + provider_controller, PluginToolProviderController + ) or PluginService.is_plugin_verified(tenant_id, provider_controller.plugin_unique_identifier) if not is_verified: return oauth_params diff --git a/api/services/workflow_draft_variable_service.py b/api/services/workflow_draft_variable_service.py index 3164e010b4..2d62d49d91 100644 --- a/api/services/workflow_draft_variable_service.py +++ b/api/services/workflow_draft_variable_service.py @@ -422,7 +422,7 @@ class WorkflowDraftVariableService: description=conv_var.description, ) draft_conv_vars.append(draft_var) - _batch_upsert_draft_varaible( + _batch_upsert_draft_variable( self._session, draft_conv_vars, policy=_UpsertPolicy.IGNORE, @@ -434,7 +434,7 @@ class _UpsertPolicy(StrEnum): OVERWRITE = "overwrite" -def _batch_upsert_draft_varaible( +def _batch_upsert_draft_variable( session: Session, draft_vars: Sequence[WorkflowDraftVariable], policy: _UpsertPolicy = _UpsertPolicy.OVERWRITE, @@ -721,7 +721,7 @@ class DraftVariableSaver: draft_vars = self._build_variables_from_start_mapping(outputs) else: draft_vars = self._build_variables_from_mapping(outputs) - _batch_upsert_draft_varaible(self._session, draft_vars) + _batch_upsert_draft_variable(self._session, draft_vars) @staticmethod def _should_variable_be_editable(node_id: str, name: str) -> bool: diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index 7feed8754c..4fa4c6b5c2 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -129,7 +129,10 @@ class WorkflowService: if not workflow: return None if workflow.version == Workflow.VERSION_DRAFT: - raise IsDraftWorkflowError(f"Workflow is draft version, id={workflow_id}") + raise IsDraftWorkflowError( + f"Cannot use draft workflow version. Workflow ID: {workflow_id}. " + f"Please use a published workflow version or leave workflow_id empty." + ) return workflow def get_published_workflow(self, app_model: App) -> Optional[Workflow]: @@ -442,9 +445,9 @@ class WorkflowService: self, node_data: dict, tenant_id: str, user_id: str, node_id: str, user_inputs: dict[str, Any] ) -> WorkflowNodeExecution: """ - Run draft workflow node + Run free workflow node """ - # run draft workflow node + # run free workflow node start_at = time.perf_counter() node_execution = self._handle_node_run_result( diff --git a/api/tasks/batch_create_segment_to_index_task.py b/api/tasks/batch_create_segment_to_index_task.py index 714e30acc3..dee43cd854 100644 --- a/api/tasks/batch_create_segment_to_index_task.py +++ b/api/tasks/batch_create_segment_to_index_task.py @@ -134,6 +134,7 @@ def batch_create_segment_to_index_task( db.session.add(segment_document) document_segments.append(segment_document) # update document word count + assert dataset_document.word_count is not None dataset_document.word_count += word_count_change db.session.add(dataset_document) # add index to db diff --git a/api/tasks/remove_app_and_related_data_task.py b/api/tasks/remove_app_and_related_data_task.py index b6f772dd60..929b60e529 100644 --- a/api/tasks/remove_app_and_related_data_task.py +++ b/api/tasks/remove_app_and_related_data_task.py @@ -3,6 +3,7 @@ import time from collections.abc import Callable import click +import sqlalchemy as sa from celery import shared_task # type: ignore from sqlalchemy import delete from sqlalchemy.exc import SQLAlchemyError @@ -331,7 +332,7 @@ def _delete_trace_app_configs(tenant_id: str, app_id: str): def _delete_records(query_sql: str, params: dict, delete_func: Callable, name: str) -> None: while True: with db.engine.begin() as conn: - rs = conn.execute(db.text(query_sql), params) + rs = conn.execute(sa.text(query_sql), params) if rs.rowcount == 0: break diff --git a/api/tests/integration_tests/vdb/tablestore/test_tablestore.py b/api/tests/integration_tests/vdb/tablestore/test_tablestore.py index da549af1b6..aebf3fbda1 100644 --- a/api/tests/integration_tests/vdb/tablestore/test_tablestore.py +++ b/api/tests/integration_tests/vdb/tablestore/test_tablestore.py @@ -2,6 +2,7 @@ import os import uuid import tablestore +from _pytest.python_api import approx from core.rag.datasource.vdb.tablestore.tablestore_vector import ( TableStoreConfig, @@ -16,7 +17,7 @@ from tests.integration_tests.vdb.test_vector_store import ( class TableStoreVectorTest(AbstractVectorTest): - def __init__(self): + def __init__(self, normalize_full_text_score: bool = False): super().__init__() self.vector = TableStoreVector( collection_name=self.collection_name, @@ -25,6 +26,7 @@ class TableStoreVectorTest(AbstractVectorTest): instance_name=os.getenv("TABLESTORE_INSTANCE_NAME"), access_key_id=os.getenv("TABLESTORE_ACCESS_KEY_ID"), access_key_secret=os.getenv("TABLESTORE_ACCESS_KEY_SECRET"), + normalize_full_text_bm25_score=normalize_full_text_score, ), ) @@ -64,7 +66,21 @@ class TableStoreVectorTest(AbstractVectorTest): docs = self.vector.search_by_full_text(get_example_text(), document_ids_filter=[self.example_doc_id]) assert len(docs) == 1 assert docs[0].metadata["doc_id"] == self.example_doc_id - assert not hasattr(docs[0], "score") + if self.vector._config.normalize_full_text_bm25_score: + assert docs[0].metadata["score"] == approx(0.1214, abs=1e-3) + else: + assert docs[0].metadata.get("score") is None + + # return none if normalize_full_text_score=true and score_threshold > 0 + docs = self.vector.search_by_full_text( + get_example_text(), document_ids_filter=[self.example_doc_id], score_threshold=0.5 + ) + if self.vector._config.normalize_full_text_bm25_score: + assert len(docs) == 0 + else: + assert len(docs) == 1 + assert docs[0].metadata["doc_id"] == self.example_doc_id + assert docs[0].metadata.get("score") is None docs = self.vector.search_by_full_text(get_example_text(), document_ids_filter=[str(uuid.uuid4())]) assert len(docs) == 0 @@ -80,3 +96,5 @@ class TableStoreVectorTest(AbstractVectorTest): def test_tablestore_vector(setup_mock_redis): TableStoreVectorTest().run_all_tests() + TableStoreVectorTest(normalize_full_text_score=True).run_all_tests() + TableStoreVectorTest(normalize_full_text_score=False).run_all_tests() diff --git a/api/tests/test_containers_integration_tests/__init__.py b/api/tests/test_containers_integration_tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/test_containers_integration_tests/conftest.py b/api/tests/test_containers_integration_tests/conftest.py new file mode 100644 index 0000000000..0369a5cbd0 --- /dev/null +++ b/api/tests/test_containers_integration_tests/conftest.py @@ -0,0 +1,328 @@ +""" +TestContainers-based integration test configuration for Dify API. + +This module provides containerized test infrastructure using TestContainers library +to spin up real database and service instances for integration testing. This approach +ensures tests run against actual service implementations rather than mocks, providing +more reliable and realistic test scenarios. +""" + +import logging +import os +from collections.abc import Generator +from typing import Optional + +import pytest +from flask import Flask +from flask.testing import FlaskClient +from sqlalchemy.orm import Session +from testcontainers.core.container import DockerContainer +from testcontainers.core.waiting_utils import wait_for_logs +from testcontainers.postgres import PostgresContainer +from testcontainers.redis import RedisContainer + +from app_factory import create_app +from models import db + +# Configure logging for test containers +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + + +class DifyTestContainers: + """ + Manages all test containers required for Dify integration tests. + + This class provides a centralized way to manage multiple containers + needed for comprehensive integration testing, including databases, + caches, and search engines. + """ + + def __init__(self): + """Initialize container management with default configurations.""" + self.postgres: Optional[PostgresContainer] = None + self.redis: Optional[RedisContainer] = None + self.dify_sandbox: Optional[DockerContainer] = None + self._containers_started = False + logger.info("DifyTestContainers initialized - ready to manage test containers") + + def start_containers_with_env(self) -> None: + """ + Start all required containers for integration testing. + + This method initializes and starts PostgreSQL, Redis + containers with appropriate configurations for Dify testing. Containers + are started in dependency order to ensure proper initialization. + """ + if self._containers_started: + logger.info("Containers already started - skipping container startup") + return + + logger.info("Starting test containers for Dify integration tests...") + + # Start PostgreSQL container for main application database + # PostgreSQL is used for storing user data, workflows, and application state + logger.info("Initializing PostgreSQL container...") + self.postgres = PostgresContainer( + image="postgres:16-alpine", + ) + self.postgres.start() + db_host = self.postgres.get_container_host_ip() + db_port = self.postgres.get_exposed_port(5432) + os.environ["DB_HOST"] = db_host + os.environ["DB_PORT"] = str(db_port) + os.environ["DB_USERNAME"] = self.postgres.username + os.environ["DB_PASSWORD"] = self.postgres.password + os.environ["DB_DATABASE"] = self.postgres.dbname + logger.info( + "PostgreSQL container started successfully - Host: %s, Port: %s User: %s, Database: %s", + db_host, + db_port, + self.postgres.username, + self.postgres.dbname, + ) + + # Wait for PostgreSQL to be ready + logger.info("Waiting for PostgreSQL to be ready to accept connections...") + wait_for_logs(self.postgres, "is ready to accept connections", timeout=30) + logger.info("PostgreSQL container is ready and accepting connections") + + # Install uuid-ossp extension for UUID generation + logger.info("Installing uuid-ossp extension...") + try: + import psycopg2 + + conn = psycopg2.connect( + host=db_host, + port=db_port, + user=self.postgres.username, + password=self.postgres.password, + database=self.postgres.dbname, + ) + conn.autocommit = True + cursor = conn.cursor() + cursor.execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp";') + cursor.close() + conn.close() + logger.info("uuid-ossp extension installed successfully") + except Exception as e: + logger.warning("Failed to install uuid-ossp extension: %s", e) + + # Set up storage environment variables + os.environ["STORAGE_TYPE"] = "opendal" + os.environ["OPENDAL_SCHEME"] = "fs" + os.environ["OPENDAL_FS_ROOT"] = "storage" + + # Start Redis container for caching and session management + # Redis is used for storing session data, cache entries, and temporary data + logger.info("Initializing Redis container...") + self.redis = RedisContainer(image="redis:latest", port=6379) + self.redis.start() + redis_host = self.redis.get_container_host_ip() + redis_port = self.redis.get_exposed_port(6379) + os.environ["REDIS_HOST"] = redis_host + os.environ["REDIS_PORT"] = str(redis_port) + logger.info("Redis container started successfully - Host: %s, Port: %s", redis_host, redis_port) + + # Wait for Redis to be ready + logger.info("Waiting for Redis to be ready to accept connections...") + wait_for_logs(self.redis, "Ready to accept connections", timeout=30) + logger.info("Redis container is ready and accepting connections") + + # Start Dify Sandbox container for code execution environment + # Dify Sandbox provides a secure environment for executing user code + logger.info("Initializing Dify Sandbox container...") + self.dify_sandbox = DockerContainer(image="langgenius/dify-sandbox:latest") + self.dify_sandbox.with_exposed_ports(8194) + self.dify_sandbox.env = { + "API_KEY": "test_api_key", + } + self.dify_sandbox.start() + sandbox_host = self.dify_sandbox.get_container_host_ip() + sandbox_port = self.dify_sandbox.get_exposed_port(8194) + os.environ["CODE_EXECUTION_ENDPOINT"] = f"http://{sandbox_host}:{sandbox_port}" + os.environ["CODE_EXECUTION_API_KEY"] = "test_api_key" + logger.info("Dify Sandbox container started successfully - Host: %s, Port: %s", sandbox_host, sandbox_port) + + # Wait for Dify Sandbox to be ready + logger.info("Waiting for Dify Sandbox to be ready to accept connections...") + wait_for_logs(self.dify_sandbox, "config init success", timeout=60) + logger.info("Dify Sandbox container is ready and accepting connections") + + self._containers_started = True + logger.info("All test containers started successfully") + + def stop_containers(self) -> None: + """ + Stop and clean up all test containers. + + This method ensures proper cleanup of all containers to prevent + resource leaks and conflicts between test runs. + """ + if not self._containers_started: + logger.info("No containers to stop - containers were not started") + return + + logger.info("Stopping and cleaning up test containers...") + containers = [self.redis, self.postgres, self.dify_sandbox] + for container in containers: + if container: + try: + container_name = container.image + logger.info("Stopping container: %s", container_name) + container.stop() + logger.info("Successfully stopped container: %s", container_name) + except Exception as e: + # Log error but don't fail the test cleanup + logger.warning("Failed to stop container %s: %s", container, e) + + self._containers_started = False + logger.info("All test containers stopped and cleaned up successfully") + + +# Global container manager instance +_container_manager = DifyTestContainers() + + +def _create_app_with_containers() -> Flask: + """ + Create Flask application configured to use test containers. + + This function creates a Flask application instance that is configured + to connect to the test containers instead of the default development + or production databases. + + Returns: + Flask: Configured Flask application for containerized testing + """ + logger.info("Creating Flask application with test container configuration...") + + # Re-create the config after environment variables have been set + from configs import dify_config + + # Force re-creation of config with new environment variables + dify_config.__dict__.clear() + dify_config.__init__() + + # Create and configure the Flask application + logger.info("Initializing Flask application...") + app = create_app() + logger.info("Flask application created successfully") + + # Initialize database schema + logger.info("Creating database schema...") + with app.app_context(): + db.create_all() + logger.info("Database schema created successfully") + + logger.info("Flask application configured and ready for testing") + return app + + +@pytest.fixture(scope="session") +def set_up_containers_and_env() -> Generator[DifyTestContainers, None, None]: + """ + Session-scoped fixture to manage test containers. + + This fixture ensures containers are started once per test session + and properly cleaned up when all tests are complete. This approach + improves test performance by reusing containers across multiple tests. + + Yields: + DifyTestContainers: Container manager instance + """ + logger.info("=== Starting test session container management ===") + _container_manager.start_containers_with_env() + logger.info("Test containers ready for session") + yield _container_manager + logger.info("=== Cleaning up test session containers ===") + _container_manager.stop_containers() + logger.info("Test session container cleanup completed") + + +@pytest.fixture(scope="session") +def flask_app_with_containers(set_up_containers_and_env) -> Flask: + """ + Session-scoped Flask application fixture using test containers. + + This fixture provides a Flask application instance that is configured + to use the test containers for all database and service connections. + + Args: + containers: Container manager fixture + + Returns: + Flask: Configured Flask application + """ + logger.info("=== Creating session-scoped Flask application ===") + app = _create_app_with_containers() + logger.info("Session-scoped Flask application created successfully") + return app + + +@pytest.fixture +def flask_req_ctx_with_containers(flask_app_with_containers) -> Generator[None, None, None]: + """ + Request context fixture for containerized Flask application. + + This fixture provides a Flask request context for tests that need + to interact with the Flask application within a request scope. + + Args: + flask_app_with_containers: Flask application fixture + + Yields: + None: Request context is active during yield + """ + logger.debug("Creating Flask request context...") + with flask_app_with_containers.test_request_context(): + logger.debug("Flask request context active") + yield + logger.debug("Flask request context closed") + + +@pytest.fixture +def test_client_with_containers(flask_app_with_containers) -> Generator[FlaskClient, None, None]: + """ + Test client fixture for containerized Flask application. + + This fixture provides a Flask test client that can be used to make + HTTP requests to the containerized application for integration testing. + + Args: + flask_app_with_containers: Flask application fixture + + Yields: + FlaskClient: Test client instance + """ + logger.debug("Creating Flask test client...") + with flask_app_with_containers.test_client() as client: + logger.debug("Flask test client ready") + yield client + logger.debug("Flask test client closed") + + +@pytest.fixture +def db_session_with_containers(flask_app_with_containers) -> Generator[Session, None, None]: + """ + Database session fixture for containerized testing. + + This fixture provides a SQLAlchemy database session that is connected + to the test PostgreSQL container, allowing tests to interact with + the database directly. + + Args: + flask_app_with_containers: Flask application fixture + + Yields: + Session: Database session instance + """ + logger.debug("Creating database session...") + with flask_app_with_containers.app_context(): + session = db.session() + logger.debug("Database session created and ready") + try: + yield session + finally: + session.close() + logger.debug("Database session closed") diff --git a/api/tests/test_containers_integration_tests/factories/__init__.py b/api/tests/test_containers_integration_tests/factories/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/test_containers_integration_tests/factories/test_storage_key_loader.py b/api/tests/test_containers_integration_tests/factories/test_storage_key_loader.py new file mode 100644 index 0000000000..d6e14f3f54 --- /dev/null +++ b/api/tests/test_containers_integration_tests/factories/test_storage_key_loader.py @@ -0,0 +1,371 @@ +import unittest +from datetime import UTC, datetime +from typing import Optional +from unittest.mock import patch +from uuid import uuid4 + +import pytest +from sqlalchemy.orm import Session + +from core.file import File, FileTransferMethod, FileType +from extensions.ext_database import db +from factories.file_factory import StorageKeyLoader +from models import ToolFile, UploadFile +from models.enums import CreatorUserRole + + +@pytest.mark.usefixtures("flask_req_ctx_with_containers") +class TestStorageKeyLoader(unittest.TestCase): + """ + Integration tests for StorageKeyLoader class. + + Tests the batched loading of storage keys from the database for files + with different transfer methods: LOCAL_FILE, REMOTE_URL, and TOOL_FILE. + """ + + def setUp(self): + """Set up test data before each test method.""" + self.session = db.session() + self.tenant_id = str(uuid4()) + self.user_id = str(uuid4()) + self.conversation_id = str(uuid4()) + + # Create test data that will be cleaned up after each test + self.test_upload_files = [] + self.test_tool_files = [] + + # Create StorageKeyLoader instance + self.loader = StorageKeyLoader(self.session, self.tenant_id) + + def tearDown(self): + """Clean up test data after each test method.""" + self.session.rollback() + + def _create_upload_file( + self, file_id: Optional[str] = None, storage_key: Optional[str] = None, tenant_id: Optional[str] = None + ) -> UploadFile: + """Helper method to create an UploadFile record for testing.""" + if file_id is None: + file_id = str(uuid4()) + if storage_key is None: + storage_key = f"test_storage_key_{uuid4()}" + if tenant_id is None: + tenant_id = self.tenant_id + + upload_file = UploadFile( + tenant_id=tenant_id, + storage_type="local", + key=storage_key, + name="test_file.txt", + size=1024, + extension=".txt", + mime_type="text/plain", + created_by_role=CreatorUserRole.ACCOUNT, + created_by=self.user_id, + created_at=datetime.now(UTC), + used=False, + ) + upload_file.id = file_id + + self.session.add(upload_file) + self.session.flush() + self.test_upload_files.append(upload_file) + + return upload_file + + def _create_tool_file( + self, file_id: Optional[str] = None, file_key: Optional[str] = None, tenant_id: Optional[str] = None + ) -> ToolFile: + """Helper method to create a ToolFile record for testing.""" + if file_id is None: + file_id = str(uuid4()) + if file_key is None: + file_key = f"test_file_key_{uuid4()}" + if tenant_id is None: + tenant_id = self.tenant_id + + tool_file = ToolFile() + tool_file.id = file_id + tool_file.user_id = self.user_id + tool_file.tenant_id = tenant_id + tool_file.conversation_id = self.conversation_id + tool_file.file_key = file_key + tool_file.mimetype = "text/plain" + tool_file.original_url = "http://example.com/file.txt" + tool_file.name = "test_tool_file.txt" + tool_file.size = 2048 + + self.session.add(tool_file) + self.session.flush() + self.test_tool_files.append(tool_file) + + return tool_file + + def _create_file( + self, related_id: str, transfer_method: FileTransferMethod, tenant_id: Optional[str] = None + ) -> File: + """Helper method to create a File object for testing.""" + if tenant_id is None: + tenant_id = self.tenant_id + + # Set related_id for LOCAL_FILE and TOOL_FILE transfer methods + file_related_id = None + remote_url = None + + if transfer_method in (FileTransferMethod.LOCAL_FILE, FileTransferMethod.TOOL_FILE): + file_related_id = related_id + elif transfer_method == FileTransferMethod.REMOTE_URL: + remote_url = "https://example.com/test_file.txt" + file_related_id = related_id + + return File( + id=str(uuid4()), # Generate new UUID for File.id + tenant_id=tenant_id, + type=FileType.DOCUMENT, + transfer_method=transfer_method, + related_id=file_related_id, + remote_url=remote_url, + filename="test_file.txt", + extension=".txt", + mime_type="text/plain", + size=1024, + storage_key="initial_key", + ) + + def test_load_storage_keys_local_file(self): + """Test loading storage keys for LOCAL_FILE transfer method.""" + # Create test data + upload_file = self._create_upload_file() + file = self._create_file(related_id=upload_file.id, transfer_method=FileTransferMethod.LOCAL_FILE) + + # Load storage keys + self.loader.load_storage_keys([file]) + + # Verify storage key was loaded correctly + assert file._storage_key == upload_file.key + + def test_load_storage_keys_remote_url(self): + """Test loading storage keys for REMOTE_URL transfer method.""" + # Create test data + upload_file = self._create_upload_file() + file = self._create_file(related_id=upload_file.id, transfer_method=FileTransferMethod.REMOTE_URL) + + # Load storage keys + self.loader.load_storage_keys([file]) + + # Verify storage key was loaded correctly + assert file._storage_key == upload_file.key + + def test_load_storage_keys_tool_file(self): + """Test loading storage keys for TOOL_FILE transfer method.""" + # Create test data + tool_file = self._create_tool_file() + file = self._create_file(related_id=tool_file.id, transfer_method=FileTransferMethod.TOOL_FILE) + + # Load storage keys + self.loader.load_storage_keys([file]) + + # Verify storage key was loaded correctly + assert file._storage_key == tool_file.file_key + + def test_load_storage_keys_mixed_methods(self): + """Test batch loading with mixed transfer methods.""" + # Create test data for different transfer methods + upload_file1 = self._create_upload_file() + upload_file2 = self._create_upload_file() + tool_file = self._create_tool_file() + + file1 = self._create_file(related_id=upload_file1.id, transfer_method=FileTransferMethod.LOCAL_FILE) + file2 = self._create_file(related_id=upload_file2.id, transfer_method=FileTransferMethod.REMOTE_URL) + file3 = self._create_file(related_id=tool_file.id, transfer_method=FileTransferMethod.TOOL_FILE) + + files = [file1, file2, file3] + + # Load storage keys + self.loader.load_storage_keys(files) + + # Verify all storage keys were loaded correctly + assert file1._storage_key == upload_file1.key + assert file2._storage_key == upload_file2.key + assert file3._storage_key == tool_file.file_key + + def test_load_storage_keys_empty_list(self): + """Test with empty file list.""" + # Should not raise any exceptions + self.loader.load_storage_keys([]) + + def test_load_storage_keys_tenant_mismatch(self): + """Test tenant_id validation.""" + # Create file with different tenant_id + upload_file = self._create_upload_file() + file = self._create_file( + related_id=upload_file.id, transfer_method=FileTransferMethod.LOCAL_FILE, tenant_id=str(uuid4()) + ) + + # Should raise ValueError for tenant mismatch + with pytest.raises(ValueError) as context: + self.loader.load_storage_keys([file]) + + assert "invalid file, expected tenant_id" in str(context.value) + + def test_load_storage_keys_missing_file_id(self): + """Test with None file.related_id.""" + # Create a file with valid parameters first, then manually set related_id to None + file = self._create_file(related_id=str(uuid4()), transfer_method=FileTransferMethod.LOCAL_FILE) + file.related_id = None + + # Should raise ValueError for None file related_id + with pytest.raises(ValueError) as context: + self.loader.load_storage_keys([file]) + + assert str(context.value) == "file id should not be None." + + def test_load_storage_keys_nonexistent_upload_file_records(self): + """Test with missing UploadFile database records.""" + # Create file with non-existent upload file id + non_existent_id = str(uuid4()) + file = self._create_file(related_id=non_existent_id, transfer_method=FileTransferMethod.LOCAL_FILE) + + # Should raise ValueError for missing record + with pytest.raises(ValueError): + self.loader.load_storage_keys([file]) + + def test_load_storage_keys_nonexistent_tool_file_records(self): + """Test with missing ToolFile database records.""" + # Create file with non-existent tool file id + non_existent_id = str(uuid4()) + file = self._create_file(related_id=non_existent_id, transfer_method=FileTransferMethod.TOOL_FILE) + + # Should raise ValueError for missing record + with pytest.raises(ValueError): + self.loader.load_storage_keys([file]) + + def test_load_storage_keys_invalid_uuid(self): + """Test with invalid UUID format.""" + # Create a file with valid parameters first, then manually set invalid related_id + file = self._create_file(related_id=str(uuid4()), transfer_method=FileTransferMethod.LOCAL_FILE) + file.related_id = "invalid-uuid-format" + + # Should raise ValueError for invalid UUID + with pytest.raises(ValueError): + self.loader.load_storage_keys([file]) + + def test_load_storage_keys_batch_efficiency(self): + """Test batched operations use efficient queries.""" + # Create multiple files of different types + upload_files = [self._create_upload_file() for _ in range(3)] + tool_files = [self._create_tool_file() for _ in range(2)] + + files = [] + files.extend( + [self._create_file(related_id=uf.id, transfer_method=FileTransferMethod.LOCAL_FILE) for uf in upload_files] + ) + files.extend( + [self._create_file(related_id=tf.id, transfer_method=FileTransferMethod.TOOL_FILE) for tf in tool_files] + ) + + # Mock the session to count queries + with patch.object(self.session, "scalars", wraps=self.session.scalars) as mock_scalars: + self.loader.load_storage_keys(files) + + # Should make exactly 2 queries (one for upload_files, one for tool_files) + assert mock_scalars.call_count == 2 + + # Verify all storage keys were loaded correctly + for i, file in enumerate(files[:3]): + assert file._storage_key == upload_files[i].key + for i, file in enumerate(files[3:]): + assert file._storage_key == tool_files[i].file_key + + def test_load_storage_keys_tenant_isolation(self): + """Test that tenant isolation works correctly.""" + # Create files for different tenants + other_tenant_id = str(uuid4()) + + # Create upload file for current tenant + upload_file_current = self._create_upload_file() + file_current = self._create_file( + related_id=upload_file_current.id, transfer_method=FileTransferMethod.LOCAL_FILE + ) + + # Create upload file for other tenant (but don't add to cleanup list) + upload_file_other = UploadFile( + tenant_id=other_tenant_id, + storage_type="local", + key="other_tenant_key", + name="other_file.txt", + size=1024, + extension=".txt", + mime_type="text/plain", + created_by_role=CreatorUserRole.ACCOUNT, + created_by=self.user_id, + created_at=datetime.now(UTC), + used=False, + ) + upload_file_other.id = str(uuid4()) + self.session.add(upload_file_other) + self.session.flush() + + # Create file for other tenant but try to load with current tenant's loader + file_other = self._create_file( + related_id=upload_file_other.id, transfer_method=FileTransferMethod.LOCAL_FILE, tenant_id=other_tenant_id + ) + + # Should raise ValueError due to tenant mismatch + with pytest.raises(ValueError) as context: + self.loader.load_storage_keys([file_other]) + + assert "invalid file, expected tenant_id" in str(context.value) + + # Current tenant's file should still work + self.loader.load_storage_keys([file_current]) + assert file_current._storage_key == upload_file_current.key + + def test_load_storage_keys_mixed_tenant_batch(self): + """Test batch with mixed tenant files (should fail on first mismatch).""" + # Create files for current tenant + upload_file_current = self._create_upload_file() + file_current = self._create_file( + related_id=upload_file_current.id, transfer_method=FileTransferMethod.LOCAL_FILE + ) + + # Create file for different tenant + other_tenant_id = str(uuid4()) + file_other = self._create_file( + related_id=str(uuid4()), transfer_method=FileTransferMethod.LOCAL_FILE, tenant_id=other_tenant_id + ) + + # Should raise ValueError on tenant mismatch + with pytest.raises(ValueError) as context: + self.loader.load_storage_keys([file_current, file_other]) + + assert "invalid file, expected tenant_id" in str(context.value) + + def test_load_storage_keys_duplicate_file_ids(self): + """Test handling of duplicate file IDs in the batch.""" + # Create upload file + upload_file = self._create_upload_file() + + # Create two File objects with same related_id + file1 = self._create_file(related_id=upload_file.id, transfer_method=FileTransferMethod.LOCAL_FILE) + file2 = self._create_file(related_id=upload_file.id, transfer_method=FileTransferMethod.LOCAL_FILE) + + # Should handle duplicates gracefully + self.loader.load_storage_keys([file1, file2]) + + # Both files should have the same storage key + assert file1._storage_key == upload_file.key + assert file2._storage_key == upload_file.key + + def test_load_storage_keys_session_isolation(self): + """Test that the loader uses the provided session correctly.""" + # Create test data + upload_file = self._create_upload_file() + file = self._create_file(related_id=upload_file.id, transfer_method=FileTransferMethod.LOCAL_FILE) + + # Create loader with different session (same underlying connection) + + with Session(bind=db.engine) as other_session: + other_loader = StorageKeyLoader(other_session, self.tenant_id) + with pytest.raises(ValueError): + other_loader.load_storage_keys([file]) diff --git a/api/tests/test_containers_integration_tests/workflow/__init__.py b/api/tests/test_containers_integration_tests/workflow/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/test_containers_integration_tests/workflow/nodes/__init__.py b/api/tests/test_containers_integration_tests/workflow/nodes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/test_containers_integration_tests/workflow/nodes/code_executor/__init__.py b/api/tests/test_containers_integration_tests/workflow/nodes/code_executor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/test_containers_integration_tests/workflow/nodes/code_executor/test_code_executor.py b/api/tests/test_containers_integration_tests/workflow/nodes/code_executor/test_code_executor.py new file mode 100644 index 0000000000..487178ff58 --- /dev/null +++ b/api/tests/test_containers_integration_tests/workflow/nodes/code_executor/test_code_executor.py @@ -0,0 +1,11 @@ +import pytest + +from core.helper.code_executor.code_executor import CodeExecutionError, CodeExecutor + +CODE_LANGUAGE = "unsupported_language" + + +def test_unsupported_with_code_template(): + with pytest.raises(CodeExecutionError) as e: + CodeExecutor.execute_workflow_code_template(language=CODE_LANGUAGE, code="", inputs={}) + assert str(e.value) == f"Unsupported language {CODE_LANGUAGE}" diff --git a/api/tests/test_containers_integration_tests/workflow/nodes/code_executor/test_code_javascript.py b/api/tests/test_containers_integration_tests/workflow/nodes/code_executor/test_code_javascript.py new file mode 100644 index 0000000000..19a41b6186 --- /dev/null +++ b/api/tests/test_containers_integration_tests/workflow/nodes/code_executor/test_code_javascript.py @@ -0,0 +1,47 @@ +from textwrap import dedent + +from .test_utils import CodeExecutorTestMixin + + +class TestJavaScriptCodeExecutor(CodeExecutorTestMixin): + """Test class for JavaScript code executor functionality.""" + + def test_javascript_plain(self, flask_app_with_containers): + """Test basic JavaScript code execution with console.log output""" + CodeExecutor, CodeLanguage = self.code_executor_imports + + code = 'console.log("Hello World")' + result_message = CodeExecutor.execute_code(language=CodeLanguage.JAVASCRIPT, preload="", code=code) + assert result_message == "Hello World\n" + + def test_javascript_json(self, flask_app_with_containers): + """Test JavaScript code execution with JSON output""" + CodeExecutor, CodeLanguage = self.code_executor_imports + + code = dedent(""" + obj = {'Hello': 'World'} + console.log(JSON.stringify(obj)) + """) + result = CodeExecutor.execute_code(language=CodeLanguage.JAVASCRIPT, preload="", code=code) + assert result == '{"Hello":"World"}\n' + + def test_javascript_with_code_template(self, flask_app_with_containers): + """Test JavaScript workflow code template execution with inputs""" + CodeExecutor, CodeLanguage = self.code_executor_imports + JavascriptCodeProvider, _ = self.javascript_imports + + result = CodeExecutor.execute_workflow_code_template( + language=CodeLanguage.JAVASCRIPT, + code=JavascriptCodeProvider.get_default_code(), + inputs={"arg1": "Hello", "arg2": "World"}, + ) + assert result == {"result": "HelloWorld"} + + def test_javascript_get_runner_script(self, flask_app_with_containers): + """Test JavaScript template transformer runner script generation""" + _, NodeJsTemplateTransformer = self.javascript_imports + + runner_script = NodeJsTemplateTransformer.get_runner_script() + assert runner_script.count(NodeJsTemplateTransformer._code_placeholder) == 1 + assert runner_script.count(NodeJsTemplateTransformer._inputs_placeholder) == 1 + assert runner_script.count(NodeJsTemplateTransformer._result_tag) == 2 diff --git a/api/tests/test_containers_integration_tests/workflow/nodes/code_executor/test_code_jinja2.py b/api/tests/test_containers_integration_tests/workflow/nodes/code_executor/test_code_jinja2.py new file mode 100644 index 0000000000..c764801170 --- /dev/null +++ b/api/tests/test_containers_integration_tests/workflow/nodes/code_executor/test_code_jinja2.py @@ -0,0 +1,42 @@ +import base64 + +from .test_utils import CodeExecutorTestMixin + + +class TestJinja2CodeExecutor(CodeExecutorTestMixin): + """Test class for Jinja2 code executor functionality.""" + + def test_jinja2(self, flask_app_with_containers): + """Test basic Jinja2 template execution with variable substitution""" + CodeExecutor, CodeLanguage = self.code_executor_imports + _, Jinja2TemplateTransformer = self.jinja2_imports + + template = "Hello {{template}}" + inputs = base64.b64encode(b'{"template": "World"}').decode("utf-8") + code = ( + Jinja2TemplateTransformer.get_runner_script() + .replace(Jinja2TemplateTransformer._code_placeholder, template) + .replace(Jinja2TemplateTransformer._inputs_placeholder, inputs) + ) + result = CodeExecutor.execute_code( + language=CodeLanguage.JINJA2, preload=Jinja2TemplateTransformer.get_preload_script(), code=code + ) + assert result == "<>Hello World<>\n" + + def test_jinja2_with_code_template(self, flask_app_with_containers): + """Test Jinja2 workflow code template execution with inputs""" + CodeExecutor, CodeLanguage = self.code_executor_imports + + result = CodeExecutor.execute_workflow_code_template( + language=CodeLanguage.JINJA2, code="Hello {{template}}", inputs={"template": "World"} + ) + assert result == {"result": "Hello World"} + + def test_jinja2_get_runner_script(self, flask_app_with_containers): + """Test Jinja2 template transformer runner script generation""" + _, Jinja2TemplateTransformer = self.jinja2_imports + + runner_script = Jinja2TemplateTransformer.get_runner_script() + assert runner_script.count(Jinja2TemplateTransformer._code_placeholder) == 1 + assert runner_script.count(Jinja2TemplateTransformer._inputs_placeholder) == 1 + assert runner_script.count(Jinja2TemplateTransformer._result_tag) == 2 diff --git a/api/tests/test_containers_integration_tests/workflow/nodes/code_executor/test_code_python3.py b/api/tests/test_containers_integration_tests/workflow/nodes/code_executor/test_code_python3.py new file mode 100644 index 0000000000..6d93df2472 --- /dev/null +++ b/api/tests/test_containers_integration_tests/workflow/nodes/code_executor/test_code_python3.py @@ -0,0 +1,47 @@ +from textwrap import dedent + +from .test_utils import CodeExecutorTestMixin + + +class TestPython3CodeExecutor(CodeExecutorTestMixin): + """Test class for Python3 code executor functionality.""" + + def test_python3_plain(self, flask_app_with_containers): + """Test basic Python3 code execution with print output""" + CodeExecutor, CodeLanguage = self.code_executor_imports + + code = 'print("Hello World")' + result = CodeExecutor.execute_code(language=CodeLanguage.PYTHON3, preload="", code=code) + assert result == "Hello World\n" + + def test_python3_json(self, flask_app_with_containers): + """Test Python3 code execution with JSON output""" + CodeExecutor, CodeLanguage = self.code_executor_imports + + code = dedent(""" + import json + print(json.dumps({'Hello': 'World'})) + """) + result = CodeExecutor.execute_code(language=CodeLanguage.PYTHON3, preload="", code=code) + assert result == '{"Hello": "World"}\n' + + def test_python3_with_code_template(self, flask_app_with_containers): + """Test Python3 workflow code template execution with inputs""" + CodeExecutor, CodeLanguage = self.code_executor_imports + Python3CodeProvider, _ = self.python3_imports + + result = CodeExecutor.execute_workflow_code_template( + language=CodeLanguage.PYTHON3, + code=Python3CodeProvider.get_default_code(), + inputs={"arg1": "Hello", "arg2": "World"}, + ) + assert result == {"result": "HelloWorld"} + + def test_python3_get_runner_script(self, flask_app_with_containers): + """Test Python3 template transformer runner script generation""" + _, Python3TemplateTransformer = self.python3_imports + + runner_script = Python3TemplateTransformer.get_runner_script() + assert runner_script.count(Python3TemplateTransformer._code_placeholder) == 1 + assert runner_script.count(Python3TemplateTransformer._inputs_placeholder) == 1 + assert runner_script.count(Python3TemplateTransformer._result_tag) == 2 diff --git a/api/tests/test_containers_integration_tests/workflow/nodes/code_executor/test_utils.py b/api/tests/test_containers_integration_tests/workflow/nodes/code_executor/test_utils.py new file mode 100644 index 0000000000..35a095b049 --- /dev/null +++ b/api/tests/test_containers_integration_tests/workflow/nodes/code_executor/test_utils.py @@ -0,0 +1,115 @@ +""" +Test utilities for code executor integration tests. + +This module provides lazy import functions to avoid module loading issues +that occur when modules are imported before the flask_app_with_containers fixture +has set up the proper environment variables and configuration. +""" + +import importlib +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + pass + + +def force_reload_code_executor(): + """ + Force reload the code_executor module to reinitialize code_execution_endpoint_url. + + This function should be called after setting up environment variables + to ensure the code_execution_endpoint_url is initialized with the correct value. + """ + try: + import core.helper.code_executor.code_executor + + importlib.reload(core.helper.code_executor.code_executor) + except Exception as e: + # Log the error but don't fail the test + print(f"Warning: Failed to reload code_executor module: {e}") + + +def get_code_executor_imports(): + """ + Lazy import function for core CodeExecutor classes. + + Returns: + tuple: (CodeExecutor, CodeLanguage) classes + """ + from core.helper.code_executor.code_executor import CodeExecutor, CodeLanguage + + return CodeExecutor, CodeLanguage + + +def get_javascript_imports(): + """ + Lazy import function for JavaScript-specific modules. + + Returns: + tuple: (JavascriptCodeProvider, NodeJsTemplateTransformer) classes + """ + from core.helper.code_executor.javascript.javascript_code_provider import JavascriptCodeProvider + from core.helper.code_executor.javascript.javascript_transformer import NodeJsTemplateTransformer + + return JavascriptCodeProvider, NodeJsTemplateTransformer + + +def get_python3_imports(): + """ + Lazy import function for Python3-specific modules. + + Returns: + tuple: (Python3CodeProvider, Python3TemplateTransformer) classes + """ + from core.helper.code_executor.python3.python3_code_provider import Python3CodeProvider + from core.helper.code_executor.python3.python3_transformer import Python3TemplateTransformer + + return Python3CodeProvider, Python3TemplateTransformer + + +def get_jinja2_imports(): + """ + Lazy import function for Jinja2-specific modules. + + Returns: + tuple: (None, Jinja2TemplateTransformer) classes + """ + from core.helper.code_executor.jinja2.jinja2_transformer import Jinja2TemplateTransformer + + return None, Jinja2TemplateTransformer + + +class CodeExecutorTestMixin: + """ + Mixin class providing lazy import methods for code executor tests. + + This mixin helps avoid module loading issues by deferring imports + until after the flask_app_with_containers fixture has set up the environment. + """ + + def setup_method(self): + """ + Setup method called before each test method. + Force reload the code_executor module to ensure fresh initialization. + """ + force_reload_code_executor() + + @property + def code_executor_imports(self): + """Property to get CodeExecutor and CodeLanguage classes.""" + return get_code_executor_imports() + + @property + def javascript_imports(self): + """Property to get JavaScript-specific classes.""" + return get_javascript_imports() + + @property + def python3_imports(self): + """Property to get Python3-specific classes.""" + return get_python3_imports() + + @property + def jinja2_imports(self): + """Property to get Jinja2-specific classes.""" + return get_jinja2_imports() diff --git a/api/tests/unit_tests/controllers/console/test_files_security.py b/api/tests/unit_tests/controllers/console/test_files_security.py new file mode 100644 index 0000000000..cb5562d345 --- /dev/null +++ b/api/tests/unit_tests/controllers/console/test_files_security.py @@ -0,0 +1,278 @@ +import io +from unittest.mock import patch + +import pytest +from werkzeug.exceptions import Forbidden + +from controllers.common.errors import FilenameNotExistsError +from controllers.console.error import ( + FileTooLargeError, + NoFileUploadedError, + TooManyFilesError, + UnsupportedFileTypeError, +) +from services.errors.file import FileTooLargeError as ServiceFileTooLargeError +from services.errors.file import UnsupportedFileTypeError as ServiceUnsupportedFileTypeError + + +class TestFileUploadSecurity: + """Test file upload security logic without complex framework setup""" + + # Test 1: Basic file validation + def test_should_validate_file_presence(self): + """Test that missing file is detected""" + from flask import Flask, request + + app = Flask(__name__) + + with app.test_request_context(method="POST", data={}): + # Simulate the check in FileApi.post() + if "file" not in request.files: + with pytest.raises(NoFileUploadedError): + raise NoFileUploadedError() + + def test_should_validate_multiple_files(self): + """Test that multiple files are rejected""" + from flask import Flask, request + + app = Flask(__name__) + + file_data = { + "file": (io.BytesIO(b"content1"), "file1.txt", "text/plain"), + "file2": (io.BytesIO(b"content2"), "file2.txt", "text/plain"), + } + + with app.test_request_context(method="POST", data=file_data, content_type="multipart/form-data"): + # Simulate the check in FileApi.post() + if len(request.files) > 1: + with pytest.raises(TooManyFilesError): + raise TooManyFilesError() + + def test_should_validate_empty_filename(self): + """Test that empty filename is rejected""" + from flask import Flask, request + + app = Flask(__name__) + + file_data = {"file": (io.BytesIO(b"content"), "", "text/plain")} + + with app.test_request_context(method="POST", data=file_data, content_type="multipart/form-data"): + file = request.files["file"] + if not file.filename: + with pytest.raises(FilenameNotExistsError): + raise FilenameNotExistsError + + # Test 2: Security - Filename sanitization + def test_should_detect_path_traversal_in_filename(self): + """Test protection against directory traversal attacks""" + dangerous_filenames = [ + "../../../etc/passwd", + "..\\..\\windows\\system32\\config\\sam", + "../../../../etc/shadow", + "./../../../sensitive.txt", + ] + + for filename in dangerous_filenames: + # Any filename containing .. should be considered dangerous + assert ".." in filename, f"Filename {filename} should be detected as path traversal" + + def test_should_detect_null_byte_injection(self): + """Test protection against null byte injection""" + dangerous_filenames = [ + "file.jpg\x00.php", + "document.pdf\x00.exe", + "image.png\x00.sh", + ] + + for filename in dangerous_filenames: + # Null bytes should be detected + assert "\x00" in filename, f"Filename {filename} should be detected as null byte injection" + + def test_should_sanitize_special_characters(self): + """Test that special characters in filenames are handled safely""" + # Characters that could be problematic in various contexts + dangerous_chars = ["/", "\\", ":", "*", "?", '"', "<", ">", "|", "\x00"] + + for char in dangerous_chars: + filename = f"file{char}name.txt" + # These characters should be detected or sanitized + assert any(c in filename for c in dangerous_chars) + + # Test 3: Permission validation + def test_should_validate_dataset_permissions(self): + """Test dataset upload permission logic""" + + class MockUser: + is_dataset_editor = False + + user = MockUser() + source = "datasets" + + # Simulate the permission check in FileApi.post() + if source == "datasets" and not user.is_dataset_editor: + with pytest.raises(Forbidden): + raise Forbidden() + + def test_should_allow_general_upload_without_permission(self): + """Test general upload doesn't require dataset permission""" + + class MockUser: + is_dataset_editor = False + + user = MockUser() + source = None # General upload + + # This should not raise an exception + if source == "datasets" and not user.is_dataset_editor: + raise Forbidden() + # Test passes if no exception is raised + + # Test 4: Service error handling + @patch("services.file_service.FileService.upload_file") + def test_should_handle_file_too_large_error(self, mock_upload): + """Test that service FileTooLargeError is properly converted""" + mock_upload.side_effect = ServiceFileTooLargeError("File too large") + + try: + mock_upload(filename="test.txt", content=b"data", mimetype="text/plain", user=None, source=None) + except ServiceFileTooLargeError as e: + # Simulate the error conversion in FileApi.post() + with pytest.raises(FileTooLargeError): + raise FileTooLargeError(e.description) + + @patch("services.file_service.FileService.upload_file") + def test_should_handle_unsupported_file_type_error(self, mock_upload): + """Test that service UnsupportedFileTypeError is properly converted""" + mock_upload.side_effect = ServiceUnsupportedFileTypeError() + + try: + mock_upload( + filename="test.exe", content=b"data", mimetype="application/octet-stream", user=None, source=None + ) + except ServiceUnsupportedFileTypeError: + # Simulate the error conversion in FileApi.post() + with pytest.raises(UnsupportedFileTypeError): + raise UnsupportedFileTypeError() + + # Test 5: File type security + def test_should_identify_dangerous_file_extensions(self): + """Test detection of potentially dangerous file extensions""" + dangerous_extensions = [ + ".php", + ".PHP", + ".pHp", # PHP files (case variations) + ".exe", + ".EXE", # Executables + ".sh", + ".SH", # Shell scripts + ".bat", + ".BAT", # Batch files + ".cmd", + ".CMD", # Command files + ".ps1", + ".PS1", # PowerShell + ".jar", + ".JAR", # Java archives + ".vbs", + ".VBS", # VBScript + ] + + safe_extensions = [".txt", ".pdf", ".jpg", ".png", ".doc", ".docx"] + + # Just verify our test data is correct + for ext in dangerous_extensions: + assert ext.lower() in [".php", ".exe", ".sh", ".bat", ".cmd", ".ps1", ".jar", ".vbs"] + + for ext in safe_extensions: + assert ext.lower() not in [".php", ".exe", ".sh", ".bat", ".cmd", ".ps1", ".jar", ".vbs"] + + def test_should_detect_double_extensions(self): + """Test detection of double extension attacks""" + suspicious_filenames = [ + "image.jpg.php", + "document.pdf.exe", + "photo.png.sh", + "file.txt.bat", + ] + + for filename in suspicious_filenames: + # Check that these have multiple extensions + parts = filename.split(".") + assert len(parts) > 2, f"Filename {filename} should have multiple extensions" + + # Test 6: Configuration validation + def test_upload_configuration_structure(self): + """Test that upload configuration has correct structure""" + # Simulate the configuration returned by FileApi.get() + config = { + "file_size_limit": 15, + "batch_count_limit": 5, + "image_file_size_limit": 10, + "video_file_size_limit": 500, + "audio_file_size_limit": 50, + "workflow_file_upload_limit": 10, + } + + # Verify all required fields are present + required_fields = [ + "file_size_limit", + "batch_count_limit", + "image_file_size_limit", + "video_file_size_limit", + "audio_file_size_limit", + "workflow_file_upload_limit", + ] + + for field in required_fields: + assert field in config, f"Missing required field: {field}" + assert isinstance(config[field], int), f"Field {field} should be an integer" + assert config[field] > 0, f"Field {field} should be positive" + + # Test 7: Source parameter handling + def test_source_parameter_normalization(self): + """Test that source parameter is properly normalized""" + test_cases = [ + ("datasets", "datasets"), + ("other", None), + ("", None), + (None, None), + ] + + for input_source, expected in test_cases: + # Simulate the source normalization in FileApi.post() + source = "datasets" if input_source == "datasets" else None + if source not in ("datasets", None): + source = None + assert source == expected + + # Test 8: Boundary conditions + def test_should_handle_edge_case_file_sizes(self): + """Test handling of boundary file sizes""" + test_cases = [ + (0, "Empty file"), # 0 bytes + (1, "Single byte"), # 1 byte + (15 * 1024 * 1024 - 1, "Just under limit"), # Just under 15MB + (15 * 1024 * 1024, "At limit"), # Exactly 15MB + (15 * 1024 * 1024 + 1, "Just over limit"), # Just over 15MB + ] + + for size, description in test_cases: + # Just verify our test data + assert isinstance(size, int), f"{description}: Size should be integer" + assert size >= 0, f"{description}: Size should be non-negative" + + def test_should_handle_special_mime_types(self): + """Test handling of various MIME types""" + mime_type_tests = [ + ("application/octet-stream", "Generic binary"), + ("text/plain", "Plain text"), + ("image/jpeg", "JPEG image"), + ("application/pdf", "PDF document"), + ("", "Empty MIME type"), + (None, "None MIME type"), + ] + + for mime_type, description in mime_type_tests: + # Verify test data structure + if mime_type is not None: + assert isinstance(mime_type, str), f"{description}: MIME type should be string or None" diff --git a/api/tests/unit_tests/core/variables/test_segment_type.py b/api/tests/unit_tests/core/variables/test_segment_type.py index 64d0d8c7e7..b33a83ba77 100644 --- a/api/tests/unit_tests/core/variables/test_segment_type.py +++ b/api/tests/unit_tests/core/variables/test_segment_type.py @@ -1,4 +1,4 @@ -from core.variables.types import SegmentType +from core.variables.types import ArrayValidation, SegmentType class TestSegmentTypeIsArrayType: @@ -17,7 +17,6 @@ class TestSegmentTypeIsArrayType: value is tested for the is_array_type method. """ # Arrange - all_segment_types = set(SegmentType) expected_array_types = [ SegmentType.ARRAY_ANY, SegmentType.ARRAY_STRING, @@ -58,3 +57,27 @@ class TestSegmentTypeIsArrayType: for seg_type in enum_values: is_array = seg_type.is_array_type() assert isinstance(is_array, bool), f"is_array_type does not return a boolean for segment type {seg_type}" + + +class TestSegmentTypeIsValidArrayValidation: + """ + Test SegmentType.is_valid with array types using different validation strategies. + """ + + def test_array_validation_all_success(self): + value = ["hello", "world", "foo"] + assert SegmentType.ARRAY_STRING.is_valid(value, array_validation=ArrayValidation.ALL) + + def test_array_validation_all_fail(self): + value = ["hello", 123, "world"] + # Should return False, since 123 is not a string + assert not SegmentType.ARRAY_STRING.is_valid(value, array_validation=ArrayValidation.ALL) + + def test_array_validation_first(self): + value = ["hello", 123, None] + assert SegmentType.ARRAY_STRING.is_valid(value, array_validation=ArrayValidation.FIRST) + + def test_array_validation_none(self): + value = [1, 2, 3] + # validation is None, skip + assert SegmentType.ARRAY_STRING.is_valid(value, array_validation=ArrayValidation.NONE) diff --git a/api/uv.lock b/api/uv.lock index 0bce38812e..4dced728ac 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1318,6 +1318,7 @@ dev = [ { name = "pytest-mock" }, { name = "ruff" }, { name = "scipy-stubs" }, + { name = "testcontainers" }, { name = "types-aiofiles" }, { name = "types-beautifulsoup4" }, { name = "types-cachetools" }, @@ -1500,6 +1501,7 @@ dev = [ { name = "pytest-mock", specifier = "~=3.14.0" }, { name = "ruff", specifier = "~=0.12.3" }, { name = "scipy-stubs", specifier = ">=1.15.3.0" }, + { name = "testcontainers", specifier = "~=4.10.0" }, { name = "types-aiofiles", specifier = "~=24.1.0" }, { name = "types-beautifulsoup4", specifier = "~=4.12.0" }, { name = "types-cachetools", specifier = "~=5.5.0" }, @@ -1600,6 +1602,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, ] +[[package]] +name = "docker" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, +] + [[package]] name = "docstring-parser" version = "0.16" @@ -5468,6 +5484,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, ] +[[package]] +name = "testcontainers" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docker" }, + { name = "python-dotenv" }, + { name = "typing-extensions" }, + { name = "urllib3" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/49/9c618aff1c50121d183cdfbc3a4a5cf2727a2cde1893efe6ca55c7009196/testcontainers-4.10.0.tar.gz", hash = "sha256:03f85c3e505d8b4edeb192c72a961cebbcba0dd94344ae778b4a159cb6dcf8d3", size = 63327, upload-time = "2025-04-02T16:13:27.582Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/0a/824b0c1ecf224802125279c3effff2e25ed785ed046e67da6e53d928de4c/testcontainers-4.10.0-py3-none-any.whl", hash = "sha256:31ed1a81238c7e131a2a29df6db8f23717d892b592fa5a1977fd0dcd0c23fc23", size = 107414, upload-time = "2025-04-02T16:13:25.785Z" }, +] + [[package]] name = "tidb-vector" version = "0.0.9" diff --git a/dev/pytest/pytest_all_tests.sh b/dev/pytest/pytest_all_tests.sh index 30898b4fcf..9123b2f8ad 100755 --- a/dev/pytest/pytest_all_tests.sh +++ b/dev/pytest/pytest_all_tests.sh @@ -15,3 +15,6 @@ dev/pytest/pytest_workflow.sh # Unit tests dev/pytest/pytest_unit_tests.sh + +# TestContainers tests +dev/pytest/pytest_testcontainers.sh diff --git a/dev/pytest/pytest_testcontainers.sh b/dev/pytest/pytest_testcontainers.sh new file mode 100755 index 0000000000..e55a436138 --- /dev/null +++ b/dev/pytest/pytest_testcontainers.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -x + +SCRIPT_DIR="$(dirname "$(realpath "$0")")" +cd "$SCRIPT_DIR/../.." + +pytest api/tests/test_containers_integration_tests diff --git a/docker/.env.example b/docker/.env.example index 7ecdf899fe..13cac189aa 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -653,6 +653,7 @@ TABLESTORE_ENDPOINT=https://instance-name.cn-hangzhou.ots.aliyuncs.com TABLESTORE_INSTANCE_NAME=instance-name TABLESTORE_ACCESS_KEY_ID=xxx TABLESTORE_ACCESS_KEY_SECRET=xxx +TABLESTORE_NORMALIZE_FULLTEXT_BM25_SCORE=false # ------------------------------ # Knowledge Configuration diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index ae83aa758d..690dccb1a8 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -312,6 +312,7 @@ x-shared-env: &shared-api-worker-env TABLESTORE_INSTANCE_NAME: ${TABLESTORE_INSTANCE_NAME:-instance-name} TABLESTORE_ACCESS_KEY_ID: ${TABLESTORE_ACCESS_KEY_ID:-xxx} TABLESTORE_ACCESS_KEY_SECRET: ${TABLESTORE_ACCESS_KEY_SECRET:-xxx} + TABLESTORE_NORMALIZE_FULLTEXT_BM25_SCORE: ${TABLESTORE_NORMALIZE_FULLTEXT_BM25_SCORE:-false} UPLOAD_FILE_SIZE_LIMIT: ${UPLOAD_FILE_SIZE_LIMIT:-15} UPLOAD_FILE_BATCH_LIMIT: ${UPLOAD_FILE_BATCH_LIMIT:-5} ETL_TYPE: ${ETL_TYPE:-dify} diff --git a/web/__tests__/check-i18n.test.ts b/web/__tests__/check-i18n.test.ts index 3bde095f4b..b4c4f1540d 100644 --- a/web/__tests__/check-i18n.test.ts +++ b/web/__tests__/check-i18n.test.ts @@ -265,7 +265,6 @@ export default translation fs.writeFileSync(path.join(testZhDir, 'pages.ts'), file2Content) const allEnKeys = await getKeysFromLanguage('en-US') - const allZhKeys = await getKeysFromLanguage('zh-Hans') // Test file filtering logic const targetFile = 'components' @@ -563,4 +562,201 @@ export default translation expect(enKeys.length - zhKeysExtra.length).toBe(-2) // -2 means 2 extra keys }) }) + + describe('Auto-remove multiline key-value pairs', () => { + // Helper function to simulate removeExtraKeysFromFile logic + function removeExtraKeysFromFile(content: string, keysToRemove: string[]): string { + const lines = content.split('\n') + const linesToRemove: number[] = [] + + for (const keyToRemove of keysToRemove) { + let targetLineIndex = -1 + const linesToRemoveForKey: number[] = [] + + // Find the key line (simplified for single-level keys in test) + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + const keyPattern = new RegExp(`^\\s*${keyToRemove}\\s*:`) + if (keyPattern.test(line)) { + targetLineIndex = i + break + } + } + + if (targetLineIndex !== -1) { + linesToRemoveForKey.push(targetLineIndex) + + // Check if this is a multiline key-value pair + const keyLine = lines[targetLineIndex] + const trimmedKeyLine = keyLine.trim() + + // If key line ends with ":" (not complete value), it's likely multiline + if (trimmedKeyLine.endsWith(':') && !trimmedKeyLine.includes('{') && !trimmedKeyLine.match(/:\s*['"`]/)) { + // Find the value lines that belong to this key + let currentLine = targetLineIndex + 1 + let foundValue = false + + while (currentLine < lines.length) { + const line = lines[currentLine] + const trimmed = line.trim() + + // Skip empty lines + if (trimmed === '') { + currentLine++ + continue + } + + // Check if this line starts a new key (indicates end of current value) + if (trimmed.match(/^\w+\s*:/)) + break + + // Check if this line is part of the value + if (trimmed.startsWith('\'') || trimmed.startsWith('"') || trimmed.startsWith('`') || foundValue) { + linesToRemoveForKey.push(currentLine) + foundValue = true + + // Check if this line ends the value (ends with quote and comma/no comma) + if ((trimmed.endsWith('\',') || trimmed.endsWith('",') || trimmed.endsWith('`,') + || trimmed.endsWith('\'') || trimmed.endsWith('"') || trimmed.endsWith('`')) + && !trimmed.startsWith('//')) + break + } + else { + break + } + + currentLine++ + } + } + + linesToRemove.push(...linesToRemoveForKey) + } + } + + // Remove duplicates and sort in reverse order + const uniqueLinesToRemove = [...new Set(linesToRemove)].sort((a, b) => b - a) + + for (const lineIndex of uniqueLinesToRemove) + lines.splice(lineIndex, 1) + + return lines.join('\n') + } + + it('should remove single-line key-value pairs correctly', () => { + const content = `const translation = { + keepThis: 'This should stay', + removeThis: 'This should be removed', + alsoKeep: 'This should also stay', +} + +export default translation` + + const result = removeExtraKeysFromFile(content, ['removeThis']) + + expect(result).toContain('keepThis: \'This should stay\'') + expect(result).toContain('alsoKeep: \'This should also stay\'') + expect(result).not.toContain('removeThis: \'This should be removed\'') + }) + + it('should remove multiline key-value pairs completely', () => { + const content = `const translation = { + keepThis: 'This should stay', + removeMultiline: + 'This is a multiline value that should be removed completely', + alsoKeep: 'This should also stay', +} + +export default translation` + + const result = removeExtraKeysFromFile(content, ['removeMultiline']) + + expect(result).toContain('keepThis: \'This should stay\'') + expect(result).toContain('alsoKeep: \'This should also stay\'') + expect(result).not.toContain('removeMultiline:') + expect(result).not.toContain('This is a multiline value that should be removed completely') + }) + + it('should handle mixed single-line and multiline removals', () => { + const content = `const translation = { + keepThis: 'Keep this', + removeSingle: 'Remove this single line', + removeMultiline: + 'Remove this multiline value', + anotherMultiline: + 'Another multiline that spans multiple lines', + keepAnother: 'Keep this too', +} + +export default translation` + + const result = removeExtraKeysFromFile(content, ['removeSingle', 'removeMultiline', 'anotherMultiline']) + + expect(result).toContain('keepThis: \'Keep this\'') + expect(result).toContain('keepAnother: \'Keep this too\'') + expect(result).not.toContain('removeSingle:') + expect(result).not.toContain('removeMultiline:') + expect(result).not.toContain('anotherMultiline:') + expect(result).not.toContain('Remove this single line') + expect(result).not.toContain('Remove this multiline value') + expect(result).not.toContain('Another multiline that spans multiple lines') + }) + + it('should properly detect multiline vs single-line patterns', () => { + const multilineContent = `const translation = { + singleLine: 'This is single line', + multilineKey: + 'This is multiline', + keyWithColon: 'Value with: colon inside', + objectKey: { + nested: 'value' + }, +} + +export default translation` + + // Test that single line with colon in value is not treated as multiline + const result1 = removeExtraKeysFromFile(multilineContent, ['keyWithColon']) + expect(result1).not.toContain('keyWithColon:') + expect(result1).not.toContain('Value with: colon inside') + + // Test that true multiline is handled correctly + const result2 = removeExtraKeysFromFile(multilineContent, ['multilineKey']) + expect(result2).not.toContain('multilineKey:') + expect(result2).not.toContain('This is multiline') + + // Test that object key removal works (note: this is a simplified test) + // In real scenario, object removal would be more complex + const result3 = removeExtraKeysFromFile(multilineContent, ['objectKey']) + expect(result3).not.toContain('objectKey: {') + // Note: Our simplified test function doesn't handle nested object removal perfectly + // This is acceptable as it's testing the main multiline string removal functionality + }) + + it('should handle real-world Polish translation structure', () => { + const polishContent = `const translation = { + createApp: 'UTWÓRZ APLIKACJĘ', + newApp: { + captionAppType: 'Jaki typ aplikacji chcesz stworzyć?', + chatbotDescription: + 'Zbuduj aplikację opartą na czacie. Ta aplikacja używa formatu pytań i odpowiedzi.', + agentDescription: + 'Zbuduj inteligentnego agenta, który może autonomicznie wybierać narzędzia.', + basic: 'Podstawowy', + }, +} + +export default translation` + + const result = removeExtraKeysFromFile(polishContent, ['captionAppType', 'chatbotDescription', 'agentDescription']) + + expect(result).toContain('createApp: \'UTWÓRZ APLIKACJĘ\'') + expect(result).toContain('basic: \'Podstawowy\'') + expect(result).not.toContain('captionAppType:') + expect(result).not.toContain('chatbotDescription:') + expect(result).not.toContain('agentDescription:') + expect(result).not.toContain('Jaki typ aplikacji') + expect(result).not.toContain('Zbuduj aplikację opartą na czacie') + expect(result).not.toContain('Zbuduj inteligentnego agenta') + }) + }) }) diff --git a/web/__tests__/document-detail-navigation-fix.test.tsx b/web/__tests__/document-detail-navigation-fix.test.tsx new file mode 100644 index 0000000000..200ed09ea9 --- /dev/null +++ b/web/__tests__/document-detail-navigation-fix.test.tsx @@ -0,0 +1,305 @@ +/** + * Document Detail Navigation Fix Verification Test + * + * This test specifically validates that the backToPrev function in the document detail + * component correctly preserves pagination and filter states. + */ + +import { fireEvent, render, screen } from '@testing-library/react' +import { useRouter } from 'next/navigation' +import { useDocumentDetail, useDocumentMetadata } from '@/service/knowledge/use-document' + +// Mock Next.js router +const mockPush = jest.fn() +jest.mock('next/navigation', () => ({ + useRouter: jest.fn(() => ({ + push: mockPush, + })), +})) + +// Mock the document service hooks +jest.mock('@/service/knowledge/use-document', () => ({ + useDocumentDetail: jest.fn(), + useDocumentMetadata: jest.fn(), + useInvalidDocumentList: jest.fn(() => jest.fn()), +})) + +// Mock other dependencies +jest.mock('@/context/dataset-detail', () => ({ + useDatasetDetailContext: jest.fn(() => [null]), +})) + +jest.mock('@/service/use-base', () => ({ + useInvalid: jest.fn(() => jest.fn()), +})) + +jest.mock('@/service/knowledge/use-segment', () => ({ + useSegmentListKey: jest.fn(), + useChildSegmentListKey: jest.fn(), +})) + +// Create a minimal version of the DocumentDetail component that includes our fix +const DocumentDetailWithFix = ({ datasetId, documentId }: { datasetId: string; documentId: string }) => { + const router = useRouter() + + // This is the FIXED implementation from detail/index.tsx + const backToPrev = () => { + // Preserve pagination and filter states when navigating back + const searchParams = new URLSearchParams(window.location.search) + const queryString = searchParams.toString() + const separator = queryString ? '?' : '' + const backPath = `/datasets/${datasetId}/documents${separator}${queryString}` + router.push(backPath) + } + + return ( +
+ +
+ Dataset: {datasetId}, Document: {documentId} +
+
+ ) +} + +describe('Document Detail Navigation Fix Verification', () => { + beforeEach(() => { + jest.clearAllMocks() + + // Mock successful API responses + ;(useDocumentDetail as jest.Mock).mockReturnValue({ + data: { + id: 'doc-123', + name: 'Test Document', + display_status: 'available', + enabled: true, + archived: false, + }, + error: null, + }) + + ;(useDocumentMetadata as jest.Mock).mockReturnValue({ + data: null, + error: null, + }) + }) + + describe('Query Parameter Preservation', () => { + test('preserves pagination state (page 3, limit 25)', () => { + // Simulate user coming from page 3 with 25 items per page + Object.defineProperty(window, 'location', { + value: { + search: '?page=3&limit=25', + }, + writable: true, + }) + + render() + + // User clicks back button + fireEvent.click(screen.getByTestId('back-button-fixed')) + + // Should preserve the pagination state + expect(mockPush).toHaveBeenCalledWith('/datasets/dataset-123/documents?page=3&limit=25') + + console.log('✅ Pagination state preserved: page=3&limit=25') + }) + + test('preserves search keyword and filters', () => { + // Simulate user with search and filters applied + Object.defineProperty(window, 'location', { + value: { + search: '?page=2&limit=10&keyword=API%20documentation&status=active', + }, + writable: true, + }) + + render() + + fireEvent.click(screen.getByTestId('back-button-fixed')) + + // Should preserve all query parameters + expect(mockPush).toHaveBeenCalledWith('/datasets/dataset-123/documents?page=2&limit=10&keyword=API+documentation&status=active') + + console.log('✅ Search and filters preserved') + }) + + test('handles complex query parameters with special characters', () => { + // Test with complex query string including encoded characters + Object.defineProperty(window, 'location', { + value: { + search: '?page=1&limit=50&keyword=test%20%26%20debug&sort=name&order=desc&filter=%7B%22type%22%3A%22pdf%22%7D', + }, + writable: true, + }) + + render() + + fireEvent.click(screen.getByTestId('back-button-fixed')) + + // URLSearchParams will normalize the encoding, but preserve all parameters + const expectedCall = mockPush.mock.calls[0][0] + expect(expectedCall).toMatch(/^\/datasets\/dataset-123\/documents\?/) + expect(expectedCall).toMatch(/page=1/) + expect(expectedCall).toMatch(/limit=50/) + expect(expectedCall).toMatch(/keyword=test/) + expect(expectedCall).toMatch(/sort=name/) + expect(expectedCall).toMatch(/order=desc/) + + console.log('✅ Complex query parameters handled:', expectedCall) + }) + + test('handles empty query parameters gracefully', () => { + // No query parameters in URL + Object.defineProperty(window, 'location', { + value: { + search: '', + }, + writable: true, + }) + + render() + + fireEvent.click(screen.getByTestId('back-button-fixed')) + + // Should navigate to clean documents URL + expect(mockPush).toHaveBeenCalledWith('/datasets/dataset-123/documents') + + console.log('✅ Empty parameters handled gracefully') + }) + }) + + describe('Different Dataset IDs', () => { + test('works with different dataset identifiers', () => { + Object.defineProperty(window, 'location', { + value: { + search: '?page=5&limit=10', + }, + writable: true, + }) + + // Test with different dataset ID format + render() + + fireEvent.click(screen.getByTestId('back-button-fixed')) + + expect(mockPush).toHaveBeenCalledWith('/datasets/ds-prod-2024-001/documents?page=5&limit=10') + + console.log('✅ Works with different dataset ID formats') + }) + }) + + describe('Real User Scenarios', () => { + test('scenario: user searches, goes to page 3, views document, clicks back', () => { + // User searched for "API" and navigated to page 3 + Object.defineProperty(window, 'location', { + value: { + search: '?keyword=API&page=3&limit=10', + }, + writable: true, + }) + + render() + + // User decides to go back to continue browsing + fireEvent.click(screen.getByTestId('back-button-fixed')) + + // Should return to page 3 of API search results + expect(mockPush).toHaveBeenCalledWith('/datasets/main-dataset/documents?keyword=API&page=3&limit=10') + + console.log('✅ Real user scenario: search + pagination preserved') + }) + + test('scenario: user applies multiple filters, goes to document, returns', () => { + // User has applied multiple filters and is on page 2 + Object.defineProperty(window, 'location', { + value: { + search: '?page=2&limit=25&status=active&type=pdf&sort=created_at&order=desc', + }, + writable: true, + }) + + render() + + fireEvent.click(screen.getByTestId('back-button-fixed')) + + // All filters should be preserved + expect(mockPush).toHaveBeenCalledWith('/datasets/filtered-dataset/documents?page=2&limit=25&status=active&type=pdf&sort=created_at&order=desc') + + console.log('✅ Complex filtering scenario preserved') + }) + }) + + describe('Error Handling and Edge Cases', () => { + test('handles malformed query parameters gracefully', () => { + // Test with potentially problematic query string + Object.defineProperty(window, 'location', { + value: { + search: '?page=invalid&limit=&keyword=test&=emptykey&malformed', + }, + writable: true, + }) + + render() + + // Should not throw errors + expect(() => { + fireEvent.click(screen.getByTestId('back-button-fixed')) + }).not.toThrow() + + // Should still attempt navigation (URLSearchParams will clean up the parameters) + expect(mockPush).toHaveBeenCalled() + const navigationPath = mockPush.mock.calls[0][0] + expect(navigationPath).toMatch(/^\/datasets\/dataset-123\/documents/) + + console.log('✅ Malformed parameters handled gracefully:', navigationPath) + }) + + test('handles very long query strings', () => { + // Test with a very long query string + const longKeyword = 'a'.repeat(1000) + Object.defineProperty(window, 'location', { + value: { + search: `?page=1&keyword=${longKeyword}`, + }, + writable: true, + }) + + render() + + expect(() => { + fireEvent.click(screen.getByTestId('back-button-fixed')) + }).not.toThrow() + + expect(mockPush).toHaveBeenCalled() + + console.log('✅ Long query strings handled') + }) + }) + + describe('Performance Verification', () => { + test('navigation function executes quickly', () => { + Object.defineProperty(window, 'location', { + value: { + search: '?page=1&limit=10&keyword=test', + }, + writable: true, + }) + + render() + + const startTime = performance.now() + fireEvent.click(screen.getByTestId('back-button-fixed')) + const endTime = performance.now() + + const executionTime = endTime - startTime + + // Should execute in less than 10ms + expect(executionTime).toBeLessThan(10) + + console.log(`⚡ Navigation execution time: ${executionTime.toFixed(2)}ms`) + }) + }) +}) diff --git a/web/__tests__/document-list-sorting.test.tsx b/web/__tests__/document-list-sorting.test.tsx new file mode 100644 index 0000000000..1510dbec23 --- /dev/null +++ b/web/__tests__/document-list-sorting.test.tsx @@ -0,0 +1,83 @@ +/** + * Document List Sorting Tests + */ + +describe('Document List Sorting', () => { + const mockDocuments = [ + { id: '1', name: 'Beta.pdf', word_count: 500, hit_count: 10, created_at: 1699123456 }, + { id: '2', name: 'Alpha.txt', word_count: 200, hit_count: 25, created_at: 1699123400 }, + { id: '3', name: 'Gamma.docx', word_count: 800, hit_count: 5, created_at: 1699123500 }, + ] + + const sortDocuments = (docs: any[], field: string, order: 'asc' | 'desc') => { + return [...docs].sort((a, b) => { + let aValue: any + let bValue: any + + switch (field) { + case 'name': + aValue = a.name?.toLowerCase() || '' + bValue = b.name?.toLowerCase() || '' + break + case 'word_count': + aValue = a.word_count || 0 + bValue = b.word_count || 0 + break + case 'hit_count': + aValue = a.hit_count || 0 + bValue = b.hit_count || 0 + break + case 'created_at': + aValue = a.created_at + bValue = b.created_at + break + default: + return 0 + } + + if (field === 'name') { + const result = aValue.localeCompare(bValue) + return order === 'asc' ? result : -result + } + else { + const result = aValue - bValue + return order === 'asc' ? result : -result + } + }) + } + + test('sorts by name descending (default for UI consistency)', () => { + const sorted = sortDocuments(mockDocuments, 'name', 'desc') + expect(sorted.map(doc => doc.name)).toEqual(['Gamma.docx', 'Beta.pdf', 'Alpha.txt']) + }) + + test('sorts by name ascending (after toggle)', () => { + const sorted = sortDocuments(mockDocuments, 'name', 'asc') + expect(sorted.map(doc => doc.name)).toEqual(['Alpha.txt', 'Beta.pdf', 'Gamma.docx']) + }) + + test('sorts by word_count descending', () => { + const sorted = sortDocuments(mockDocuments, 'word_count', 'desc') + expect(sorted.map(doc => doc.word_count)).toEqual([800, 500, 200]) + }) + + test('sorts by hit_count descending', () => { + const sorted = sortDocuments(mockDocuments, 'hit_count', 'desc') + expect(sorted.map(doc => doc.hit_count)).toEqual([25, 10, 5]) + }) + + test('sorts by created_at descending (newest first)', () => { + const sorted = sortDocuments(mockDocuments, 'created_at', 'desc') + expect(sorted.map(doc => doc.created_at)).toEqual([1699123500, 1699123456, 1699123400]) + }) + + test('handles empty values correctly', () => { + const docsWithEmpty = [ + { id: '1', name: 'Test', word_count: 100, hit_count: 5, created_at: 1699123456 }, + { id: '2', name: 'Empty', word_count: 0, hit_count: 0, created_at: 1699123400 }, + ] + + const sorted = sortDocuments(docsWithEmpty, 'word_count', 'desc') + expect(sorted.map(doc => doc.word_count)).toEqual([100, 0]) + }) +}) diff --git a/web/__tests__/navigation-utils.test.ts b/web/__tests__/navigation-utils.test.ts new file mode 100644 index 0000000000..9a388505d6 --- /dev/null +++ b/web/__tests__/navigation-utils.test.ts @@ -0,0 +1,290 @@ +/** + * Navigation Utilities Test + * + * Tests for the navigation utility functions to ensure they handle + * query parameter preservation correctly across different scenarios. + */ + +import { + createBackNavigation, + createNavigationPath, + createNavigationPathWithParams, + datasetNavigation, + extractQueryParams, + mergeQueryParams, +} from '@/utils/navigation' + +// Mock router for testing +const mockPush = jest.fn() +const mockRouter = { push: mockPush } + +describe('Navigation Utilities', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('createNavigationPath', () => { + test('preserves query parameters by default', () => { + Object.defineProperty(window, 'location', { + value: { search: '?page=3&limit=10&keyword=test' }, + writable: true, + }) + + const path = createNavigationPath('/datasets/123/documents') + expect(path).toBe('/datasets/123/documents?page=3&limit=10&keyword=test') + }) + + test('returns clean path when preserveParams is false', () => { + Object.defineProperty(window, 'location', { + value: { search: '?page=3&limit=10' }, + writable: true, + }) + + const path = createNavigationPath('/datasets/123/documents', false) + expect(path).toBe('/datasets/123/documents') + }) + + test('handles empty query parameters', () => { + Object.defineProperty(window, 'location', { + value: { search: '' }, + writable: true, + }) + + const path = createNavigationPath('/datasets/123/documents') + expect(path).toBe('/datasets/123/documents') + }) + + test('handles errors gracefully', () => { + // Mock window.location to throw an error + Object.defineProperty(window, 'location', { + get: () => { + throw new Error('Location access denied') + }, + configurable: true, + }) + + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation() + const path = createNavigationPath('/datasets/123/documents') + + expect(path).toBe('/datasets/123/documents') + expect(consoleSpy).toHaveBeenCalledWith('Failed to preserve query parameters:', expect.any(Error)) + + consoleSpy.mockRestore() + }) + }) + + describe('createBackNavigation', () => { + test('creates function that navigates with preserved params', () => { + Object.defineProperty(window, 'location', { + value: { search: '?page=2&limit=25' }, + writable: true, + }) + + const backFn = createBackNavigation(mockRouter, '/datasets/123/documents') + backFn() + + expect(mockPush).toHaveBeenCalledWith('/datasets/123/documents?page=2&limit=25') + }) + + test('creates function that navigates without params when specified', () => { + Object.defineProperty(window, 'location', { + value: { search: '?page=2&limit=25' }, + writable: true, + }) + + const backFn = createBackNavigation(mockRouter, '/datasets/123/documents', false) + backFn() + + expect(mockPush).toHaveBeenCalledWith('/datasets/123/documents') + }) + }) + + describe('extractQueryParams', () => { + test('extracts specified parameters', () => { + Object.defineProperty(window, 'location', { + value: { search: '?page=3&limit=10&keyword=test&other=value' }, + writable: true, + }) + + const params = extractQueryParams(['page', 'limit', 'keyword']) + expect(params).toEqual({ + page: '3', + limit: '10', + keyword: 'test', + }) + }) + + test('handles missing parameters', () => { + Object.defineProperty(window, 'location', { + value: { search: '?page=3' }, + writable: true, + }) + + const params = extractQueryParams(['page', 'limit', 'missing']) + expect(params).toEqual({ + page: '3', + }) + }) + + test('handles errors gracefully', () => { + Object.defineProperty(window, 'location', { + get: () => { + throw new Error('Location access denied') + }, + configurable: true, + }) + + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation() + const params = extractQueryParams(['page', 'limit']) + + expect(params).toEqual({}) + expect(consoleSpy).toHaveBeenCalledWith('Failed to extract query parameters:', expect.any(Error)) + + consoleSpy.mockRestore() + }) + }) + + describe('createNavigationPathWithParams', () => { + test('creates path with specified parameters', () => { + const path = createNavigationPathWithParams('/datasets/123/documents', { + page: 1, + limit: 25, + keyword: 'search term', + }) + + expect(path).toBe('/datasets/123/documents?page=1&limit=25&keyword=search+term') + }) + + test('filters out empty values', () => { + const path = createNavigationPathWithParams('/datasets/123/documents', { + page: 1, + limit: '', + keyword: 'test', + empty: null, + undefined, + }) + + expect(path).toBe('/datasets/123/documents?page=1&keyword=test') + }) + + test('handles errors gracefully', () => { + // Mock URLSearchParams to throw an error + const originalURLSearchParams = globalThis.URLSearchParams + globalThis.URLSearchParams = jest.fn(() => { + throw new Error('URLSearchParams error') + }) as any + + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation() + const path = createNavigationPathWithParams('/datasets/123/documents', { page: 1 }) + + expect(path).toBe('/datasets/123/documents') + expect(consoleSpy).toHaveBeenCalledWith('Failed to create navigation path with params:', expect.any(Error)) + + consoleSpy.mockRestore() + globalThis.URLSearchParams = originalURLSearchParams + }) + }) + + describe('mergeQueryParams', () => { + test('merges new params with existing ones', () => { + Object.defineProperty(window, 'location', { + value: { search: '?page=3&limit=10' }, + writable: true, + }) + + const merged = mergeQueryParams({ keyword: 'test', page: '1' }) + const result = merged.toString() + + expect(result).toContain('page=1') // overridden + expect(result).toContain('limit=10') // preserved + expect(result).toContain('keyword=test') // added + }) + + test('removes parameters when value is null', () => { + Object.defineProperty(window, 'location', { + value: { search: '?page=3&limit=10&keyword=test' }, + writable: true, + }) + + const merged = mergeQueryParams({ keyword: null, filter: 'active' }) + const result = merged.toString() + + expect(result).toContain('page=3') + expect(result).toContain('limit=10') + expect(result).not.toContain('keyword') + expect(result).toContain('filter=active') + }) + + test('creates fresh params when preserveExisting is false', () => { + Object.defineProperty(window, 'location', { + value: { search: '?page=3&limit=10' }, + writable: true, + }) + + const merged = mergeQueryParams({ keyword: 'test' }, false) + const result = merged.toString() + + expect(result).toBe('keyword=test') + }) + }) + + describe('datasetNavigation', () => { + test('backToDocuments creates correct navigation function', () => { + Object.defineProperty(window, 'location', { + value: { search: '?page=2&limit=25' }, + writable: true, + }) + + const backFn = datasetNavigation.backToDocuments(mockRouter, 'dataset-123') + backFn() + + expect(mockPush).toHaveBeenCalledWith('/datasets/dataset-123/documents?page=2&limit=25') + }) + + test('toDocumentDetail creates correct navigation function', () => { + const detailFn = datasetNavigation.toDocumentDetail(mockRouter, 'dataset-123', 'doc-456') + detailFn() + + expect(mockPush).toHaveBeenCalledWith('/datasets/dataset-123/documents/doc-456') + }) + + test('toDocumentSettings creates correct navigation function', () => { + const settingsFn = datasetNavigation.toDocumentSettings(mockRouter, 'dataset-123', 'doc-456') + settingsFn() + + expect(mockPush).toHaveBeenCalledWith('/datasets/dataset-123/documents/doc-456/settings') + }) + }) + + describe('Real-world Integration Scenarios', () => { + test('complete user workflow: list -> detail -> back', () => { + // User starts on page 3 with search + Object.defineProperty(window, 'location', { + value: { search: '?page=3&keyword=API&limit=25' }, + writable: true, + }) + + // Create back navigation function (as would be done in detail component) + const backToDocuments = datasetNavigation.backToDocuments(mockRouter, 'main-dataset') + + // User clicks back + backToDocuments() + + // Should return to exact same list state + expect(mockPush).toHaveBeenCalledWith('/datasets/main-dataset/documents?page=3&keyword=API&limit=25') + }) + + test('user applies filters then views document', () => { + // Complex filter state + Object.defineProperty(window, 'location', { + value: { search: '?page=1&limit=50&status=active&type=pdf&sort=created_at&order=desc' }, + writable: true, + }) + + const backFn = createBackNavigation(mockRouter, '/datasets/filtered-set/documents') + backFn() + + expect(mockPush).toHaveBeenCalledWith('/datasets/filtered-set/documents?page=1&limit=50&status=active&type=pdf&sort=created_at&order=desc') + }) + }) +}) diff --git a/web/__tests__/unified-tags-logic.test.ts b/web/__tests__/unified-tags-logic.test.ts new file mode 100644 index 0000000000..c920e28e0a --- /dev/null +++ b/web/__tests__/unified-tags-logic.test.ts @@ -0,0 +1,396 @@ +/** + * Unified Tags Editing - Pure Logic Tests + * + * This test file validates the core business logic and state management + * behaviors introduced in the recent 7 commits without requiring complex mocks. + */ + +describe('Unified Tags Editing - Pure Logic Tests', () => { + describe('Tag State Management Logic', () => { + it('should detect when tag values have changed', () => { + const currentValue = ['tag1', 'tag2'] + const newSelectedTagIDs = ['tag1', 'tag3'] + + // This is the valueNotChanged logic from TagSelector component + const valueNotChanged + = currentValue.length === newSelectedTagIDs.length + && currentValue.every(v => newSelectedTagIDs.includes(v)) + && newSelectedTagIDs.every(v => currentValue.includes(v)) + + expect(valueNotChanged).toBe(false) + }) + + it('should correctly identify unchanged tag values', () => { + const currentValue = ['tag1', 'tag2'] + const newSelectedTagIDs = ['tag2', 'tag1'] // Same tags, different order + + const valueNotChanged + = currentValue.length === newSelectedTagIDs.length + && currentValue.every(v => newSelectedTagIDs.includes(v)) + && newSelectedTagIDs.every(v => currentValue.includes(v)) + + expect(valueNotChanged).toBe(true) + }) + + it('should calculate correct tag operations for binding/unbinding', () => { + const currentValue = ['tag1', 'tag2'] + const selectedTagIDs = ['tag2', 'tag3'] + + // This is the handleValueChange logic from TagSelector + const addTagIDs = selectedTagIDs.filter(v => !currentValue.includes(v)) + const removeTagIDs = currentValue.filter(v => !selectedTagIDs.includes(v)) + + expect(addTagIDs).toEqual(['tag3']) + expect(removeTagIDs).toEqual(['tag1']) + }) + + it('should handle empty tag arrays correctly', () => { + const currentValue: string[] = [] + const selectedTagIDs = ['tag1'] + + const addTagIDs = selectedTagIDs.filter(v => !currentValue.includes(v)) + const removeTagIDs = currentValue.filter(v => !selectedTagIDs.includes(v)) + + expect(addTagIDs).toEqual(['tag1']) + expect(removeTagIDs).toEqual([]) + expect(currentValue.length).toBe(0) // Verify empty array usage + }) + + it('should handle removing all tags', () => { + const currentValue = ['tag1', 'tag2'] + const selectedTagIDs: string[] = [] + + const addTagIDs = selectedTagIDs.filter(v => !currentValue.includes(v)) + const removeTagIDs = currentValue.filter(v => !selectedTagIDs.includes(v)) + + expect(addTagIDs).toEqual([]) + expect(removeTagIDs).toEqual(['tag1', 'tag2']) + expect(selectedTagIDs.length).toBe(0) // Verify empty array usage + }) + }) + + describe('Fallback Logic (from layout-main.tsx)', () => { + it('should trigger fallback when tags are missing or empty', () => { + const appDetailWithoutTags = { tags: [] } + const appDetailWithTags = { tags: [{ id: 'tag1' }] } + const appDetailWithUndefinedTags = { tags: undefined as any } + + // This simulates the condition in layout-main.tsx + const shouldFallback1 = !appDetailWithoutTags.tags || appDetailWithoutTags.tags.length === 0 + const shouldFallback2 = !appDetailWithTags.tags || appDetailWithTags.tags.length === 0 + const shouldFallback3 = !appDetailWithUndefinedTags.tags || appDetailWithUndefinedTags.tags.length === 0 + + expect(shouldFallback1).toBe(true) // Empty array should trigger fallback + expect(shouldFallback2).toBe(false) // Has tags, no fallback needed + expect(shouldFallback3).toBe(true) // Undefined tags should trigger fallback + }) + + it('should preserve tags when fallback succeeds', () => { + const originalAppDetail = { tags: [] as any[] } + const fallbackResult = { tags: [{ id: 'tag1', name: 'fallback-tag' }] } + + // This simulates the successful fallback in layout-main.tsx + if (fallbackResult?.tags) + originalAppDetail.tags = fallbackResult.tags + + expect(originalAppDetail.tags).toEqual(fallbackResult.tags) + expect(originalAppDetail.tags.length).toBe(1) + }) + + it('should continue with empty tags when fallback fails', () => { + const originalAppDetail: { tags: any[] } = { tags: [] } + const fallbackResult: { tags?: any[] } | null = null + + // This simulates fallback failure in layout-main.tsx + if (fallbackResult?.tags) + originalAppDetail.tags = fallbackResult.tags + + expect(originalAppDetail.tags).toEqual([]) + }) + }) + + describe('TagSelector Auto-initialization Logic', () => { + it('should trigger getTagList when tagList is empty', () => { + const tagList: any[] = [] + let getTagListCalled = false + const getTagList = () => { + getTagListCalled = true + } + + // This simulates the useEffect in TagSelector + if (tagList.length === 0) + getTagList() + + expect(getTagListCalled).toBe(true) + }) + + it('should not trigger getTagList when tagList has items', () => { + const tagList = [{ id: 'tag1', name: 'existing-tag' }] + let getTagListCalled = false + const getTagList = () => { + getTagListCalled = true + } + + // This simulates the useEffect in TagSelector + if (tagList.length === 0) + getTagList() + + expect(getTagListCalled).toBe(false) + }) + }) + + describe('State Initialization Patterns', () => { + it('should maintain AppCard tag state pattern', () => { + const app = { tags: [{ id: 'tag1', name: 'test' }] } + + // Original AppCard pattern: useState(app.tags) + const initialTags = app.tags + expect(Array.isArray(initialTags)).toBe(true) + expect(initialTags.length).toBe(1) + expect(initialTags).toBe(app.tags) // Reference equality for AppCard + }) + + it('should maintain AppInfo tag state pattern', () => { + const appDetail = { tags: [{ id: 'tag1', name: 'test' }] } + + // New AppInfo pattern: useState(appDetail?.tags || []) + const initialTags = appDetail?.tags || [] + expect(Array.isArray(initialTags)).toBe(true) + expect(initialTags.length).toBe(1) + }) + + it('should handle undefined appDetail gracefully in AppInfo', () => { + const appDetail = undefined + + // AppInfo pattern with undefined appDetail + const initialTags = (appDetail as any)?.tags || [] + expect(Array.isArray(initialTags)).toBe(true) + expect(initialTags.length).toBe(0) + }) + }) + + describe('CSS Class and Layout Logic', () => { + it('should apply correct minimum width condition', () => { + const minWidth = 'true' + + // This tests the minWidth logic in TagSelector + const shouldApplyMinWidth = minWidth && '!min-w-80' + expect(shouldApplyMinWidth).toBe('!min-w-80') + }) + + it('should not apply minimum width when not specified', () => { + const minWidth = undefined + + const shouldApplyMinWidth = minWidth && '!min-w-80' + expect(shouldApplyMinWidth).toBeFalsy() + }) + + it('should handle overflow layout classes correctly', () => { + // This tests the layout pattern from AppCard and new AppInfo + const overflowLayoutClasses = { + container: 'flex w-0 grow items-center', + inner: 'w-full', + truncate: 'truncate', + } + + expect(overflowLayoutClasses.container).toContain('w-0 grow') + expect(overflowLayoutClasses.inner).toContain('w-full') + expect(overflowLayoutClasses.truncate).toBe('truncate') + }) + }) + + describe('fetchAppWithTags Service Logic', () => { + it('should correctly find app by ID from app list', () => { + const appList = [ + { id: 'app1', name: 'App 1', tags: [] }, + { id: 'test-app-id', name: 'Test App', tags: [{ id: 'tag1', name: 'test' }] }, + { id: 'app3', name: 'App 3', tags: [] }, + ] + const targetAppId = 'test-app-id' + + // This simulates the logic in fetchAppWithTags + const foundApp = appList.find(app => app.id === targetAppId) + + expect(foundApp).toBeDefined() + expect(foundApp?.id).toBe('test-app-id') + expect(foundApp?.tags.length).toBe(1) + }) + + it('should return null when app not found', () => { + const appList = [ + { id: 'app1', name: 'App 1' }, + { id: 'app2', name: 'App 2' }, + ] + const targetAppId = 'nonexistent-app' + + const foundApp = appList.find(app => app.id === targetAppId) || null + + expect(foundApp).toBeNull() + }) + + it('should handle empty app list', () => { + const appList: any[] = [] + const targetAppId = 'any-app' + + const foundApp = appList.find(app => app.id === targetAppId) || null + + expect(foundApp).toBeNull() + expect(appList.length).toBe(0) // Verify empty array usage + }) + }) + + describe('Data Structure Validation', () => { + it('should maintain consistent tag data structure', () => { + const tag = { + id: 'tag1', + name: 'test-tag', + type: 'app', + binding_count: 1, + } + + expect(tag).toHaveProperty('id') + expect(tag).toHaveProperty('name') + expect(tag).toHaveProperty('type') + expect(tag).toHaveProperty('binding_count') + expect(tag.type).toBe('app') + expect(typeof tag.binding_count).toBe('number') + }) + + it('should handle tag arrays correctly', () => { + const tags = [ + { id: 'tag1', name: 'Tag 1', type: 'app', binding_count: 1 }, + { id: 'tag2', name: 'Tag 2', type: 'app', binding_count: 0 }, + ] + + expect(Array.isArray(tags)).toBe(true) + expect(tags.length).toBe(2) + expect(tags.every(tag => tag.type === 'app')).toBe(true) + }) + + it('should validate app data structure with tags', () => { + const app = { + id: 'test-app', + name: 'Test App', + tags: [ + { id: 'tag1', name: 'Tag 1', type: 'app', binding_count: 1 }, + ], + } + + expect(app).toHaveProperty('id') + expect(app).toHaveProperty('name') + expect(app).toHaveProperty('tags') + expect(Array.isArray(app.tags)).toBe(true) + expect(app.tags.length).toBe(1) + }) + }) + + describe('Performance and Edge Cases', () => { + it('should handle large tag arrays efficiently', () => { + const largeTags = Array.from({ length: 100 }, (_, i) => `tag${i}`) + const selectedTags = ['tag1', 'tag50', 'tag99'] + + // Performance test: filtering should be efficient + const startTime = Date.now() + const addTags = selectedTags.filter(tag => !largeTags.includes(tag)) + const removeTags = largeTags.filter(tag => !selectedTags.includes(tag)) + const endTime = Date.now() + + expect(endTime - startTime).toBeLessThan(10) // Should be very fast + expect(addTags.length).toBe(0) // All selected tags exist + expect(removeTags.length).toBe(97) // 100 - 3 = 97 tags to remove + }) + + it('should handle malformed tag data gracefully', () => { + const mixedData = [ + { id: 'valid1', name: 'Valid Tag', type: 'app', binding_count: 1 }, + { id: 'invalid1' }, // Missing required properties + null, + undefined, + { id: 'valid2', name: 'Another Valid', type: 'app', binding_count: 0 }, + ] + + // Filter out invalid entries + const validTags = mixedData.filter((tag): tag is { id: string; name: string; type: string; binding_count: number } => + tag != null + && typeof tag === 'object' + && 'id' in tag + && 'name' in tag + && 'type' in tag + && 'binding_count' in tag + && typeof tag.binding_count === 'number', + ) + + expect(validTags.length).toBe(2) + expect(validTags.every(tag => tag.id && tag.name)).toBe(true) + }) + + it('should handle concurrent tag operations correctly', () => { + const operations = [ + { type: 'add', tagIds: ['tag1', 'tag2'] }, + { type: 'remove', tagIds: ['tag3'] }, + { type: 'add', tagIds: ['tag4'] }, + ] + + // Simulate processing operations + const results = operations.map(op => ({ + ...op, + processed: true, + timestamp: Date.now(), + })) + + expect(results.length).toBe(3) + expect(results.every(result => result.processed)).toBe(true) + }) + }) + + describe('Backward Compatibility Verification', () => { + it('should not break existing AppCard behavior', () => { + // Verify AppCard continues to work with original patterns + const originalAppCardLogic = { + initializeTags: (app: any) => app.tags, + updateTags: (_currentTags: any[], newTags: any[]) => newTags, + shouldRefresh: true, + } + + const app = { tags: [{ id: 'tag1', name: 'original' }] } + const initializedTags = originalAppCardLogic.initializeTags(app) + + expect(initializedTags).toBe(app.tags) + expect(originalAppCardLogic.shouldRefresh).toBe(true) + }) + + it('should ensure AppInfo follows AppCard patterns', () => { + // Verify AppInfo uses compatible state management + const appCardPattern = (app: any) => app.tags + const appInfoPattern = (appDetail: any) => appDetail?.tags || [] + + const appWithTags = { tags: [{ id: 'tag1' }] } + const appWithoutTags = { tags: [] } + const undefinedApp = undefined + + expect(appCardPattern(appWithTags)).toEqual(appInfoPattern(appWithTags)) + expect(appInfoPattern(appWithoutTags)).toEqual([]) + expect(appInfoPattern(undefinedApp)).toEqual([]) + }) + + it('should maintain consistent API parameters', () => { + // Verify service layer maintains expected parameters + const fetchAppListParams = { + url: '/apps', + params: { page: 1, limit: 100 }, + } + + const tagApiParams = { + bindTag: (tagIDs: string[], targetID: string, type: string) => ({ tagIDs, targetID, type }), + unBindTag: (tagID: string, targetID: string, type: string) => ({ tagID, targetID, type }), + } + + expect(fetchAppListParams.url).toBe('/apps') + expect(fetchAppListParams.params.limit).toBe(100) + + const bindResult = tagApiParams.bindTag(['tag1'], 'app1', 'app') + expect(bindResult.tagIDs).toEqual(['tag1']) + expect(bindResult.type).toBe('app') + }) + }) +}) diff --git a/web/__tests__/xss-fix-verification.test.tsx b/web/__tests__/xss-fix-verification.test.tsx new file mode 100644 index 0000000000..2fa5ab3c05 --- /dev/null +++ b/web/__tests__/xss-fix-verification.test.tsx @@ -0,0 +1,212 @@ +/** + * XSS Fix Verification Test + * + * This test verifies that the XSS vulnerability in check-code pages has been + * properly fixed by replacing dangerouslySetInnerHTML with safe React rendering. + */ + +import React from 'react' +import { cleanup, render } from '@testing-library/react' +import '@testing-library/jest-dom' + +// Mock i18next with the new safe translation structure +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => { + if (key === 'login.checkCode.tipsPrefix') + return 'We send a verification code to ' + + return key + }, + }), +})) + +// Mock Next.js useSearchParams +jest.mock('next/navigation', () => ({ + useSearchParams: () => ({ + get: (key: string) => { + if (key === 'email') + return 'test@example.com' + return null + }, + }), +})) + +// Fixed CheckCode component implementation (current secure version) +const SecureCheckCodeComponent = ({ email }: { email: string }) => { + const { t } = require('react-i18next').useTranslation() + + return ( +
+

Check Code

+

+ + {t('login.checkCode.tipsPrefix')} + {email} + +

+
+ ) +} + +// Vulnerable implementation for comparison (what we fixed) +const VulnerableCheckCodeComponent = ({ email }: { email: string }) => { + const mockTranslation = (key: string, params?: any) => { + if (key === 'login.checkCode.tips' && params?.email) + return `We send a verification code to ${params.email}` + + return key + } + + return ( +
+

Check Code

+

+ +

+
+ ) +} + +describe('XSS Fix Verification - Check Code Pages Security', () => { + afterEach(() => { + cleanup() + }) + + const maliciousEmail = 'test@example.com' + + it('should securely render email with HTML characters as text (FIXED VERSION)', () => { + console.log('\n🔒 Security Fix Verification Report') + console.log('===================================') + + const { container } = render() + + const spanElement = container.querySelector('span') + const strongElement = container.querySelector('strong') + const scriptElements = container.querySelectorAll('script') + + console.log('\n✅ Fixed Implementation Results:') + console.log('- Email rendered in strong tag:', strongElement?.textContent) + console.log('- HTML tags visible as text:', strongElement?.textContent?.includes('', + 'normal@email.com', + ] + + testCases.forEach((testEmail, index) => { + const { container } = render() + + const strongElement = container.querySelector('strong') + const scriptElements = container.querySelectorAll('script') + const imgElements = container.querySelectorAll('img') + const divElements = container.querySelectorAll('div:not([data-testid])') + + console.log(`\n📧 Test Case ${index + 1}: ${testEmail.substring(0, 20)}...`) + console.log(` - Script elements: ${scriptElements.length}`) + console.log(` - Img elements: ${imgElements.length}`) + console.log(` - Malicious divs: ${divElements.length - 1}`) // -1 for container div + console.log(` - Text content: ${strongElement?.textContent === testEmail ? 'SAFE' : 'ISSUE'}`) + + // All should be safe + expect(scriptElements).toHaveLength(0) + expect(imgElements).toHaveLength(0) + expect(strongElement?.textContent).toBe(testEmail) + }) + + console.log('\n✅ All test cases passed - secure rendering confirmed') + }) + + it('should validate the translation structure is secure', () => { + console.log('\n🔍 Translation Security Analysis') + console.log('=================================') + + const { t } = require('react-i18next').useTranslation() + const prefix = t('login.checkCode.tipsPrefix') + + console.log('- Translation key used: login.checkCode.tipsPrefix') + console.log('- Translation value:', prefix) + console.log('- Contains HTML tags:', prefix.includes('<')) + console.log('- Pure text content:', !prefix.includes('<') && !prefix.includes('>')) + + // Verify translation is plain text + expect(prefix).toBe('We send a verification code to ') + expect(prefix).not.toContain('<') + expect(prefix).not.toContain('>') + expect(typeof prefix).toBe('string') + + console.log('\n✅ Translation structure is secure - no HTML content') + }) + + it('should confirm React automatic escaping works correctly', () => { + console.log('\n⚡ React Security Mechanism Test') + console.log('=================================') + + // Test React's automatic escaping with various inputs + const dangerousInputs = [ + '', + '', + '">', + '\'>alert(3)', + '
click
', + ] + + dangerousInputs.forEach((input, index) => { + const TestComponent = () => {input} + const { container } = render() + + const strongElement = container.querySelector('strong') + const scriptElements = container.querySelectorAll('script') + + console.log(`\n🧪 Input ${index + 1}: ${input.substring(0, 30)}...`) + console.log(` - Rendered as text: ${strongElement?.textContent === input}`) + console.log(` - No script execution: ${scriptElements.length === 0}`) + + expect(strongElement?.textContent).toBe(input) + expect(scriptElements).toHaveLength(0) + }) + + console.log('\n🛡️ React automatic escaping is working perfectly') + }) +}) + +export {} diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx index 1339c0ef5a..a36a7e281d 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx @@ -20,12 +20,18 @@ import cn from '@/utils/classnames' import { useStore } from '@/app/components/app/store' import AppSideBar from '@/app/components/app-sidebar' import type { NavIcon } from '@/app/components/app-sidebar/navLink' -import { fetchAppDetail } from '@/service/apps' +import { fetchAppDetailDirect } from '@/service/apps' import { useAppContext } from '@/context/app-context' import Loading from '@/app/components/base/loading' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import type { App } from '@/types/app' import useDocumentTitle from '@/hooks/use-document-title' +import { useStore as useTagStore } from '@/app/components/base/tag-management/store' +import dynamic from 'next/dynamic' + +const TagManagementModal = dynamic(() => import('@/app/components/base/tag-management'), { + ssr: false, +}) export type IAppDetailLayoutProps = { children: React.ReactNode @@ -48,6 +54,7 @@ const AppDetailLayout: FC = (props) => { setAppDetail: state.setAppDetail, setAppSidebarExpand: state.setAppSidebarExpand, }))) + const showTagManagementModal = useTagStore(s => s.showTagManagementModal) const [isLoadingAppDetail, setIsLoadingAppDetail] = useState(false) const [appDetailRes, setAppDetailRes] = useState(null) const [navigation, setNavigation] = useState = (props) => { useEffect(() => { setAppDetail() setIsLoadingAppDetail(true) - fetchAppDetail({ url: '/apps', id: appId }).then((res) => { + fetchAppDetailDirect({ url: '/apps', id: appId }).then((res: App) => { setAppDetailRes(res) }).catch((e: any) => { if (e.status === 404) @@ -165,6 +172,9 @@ const AppDetailLayout: FC = (props) => {
{children}
+ {showTagManagementModal && ( + + )} ) } diff --git a/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx b/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx index da754794b1..91e1021610 100644 --- a/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx +++ b/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx @@ -70,7 +70,10 @@ export default function CheckCode() {

{t('login.checkCode.checkYourEmail')}

- + + {t('login.checkCode.tipsPrefix')} + {email} +
{t('login.checkCode.validTime')}

diff --git a/web/app/(shareLayout)/webapp-signin/check-code/page.tsx b/web/app/(shareLayout)/webapp-signin/check-code/page.tsx index a2ba620ace..c80a006583 100644 --- a/web/app/(shareLayout)/webapp-signin/check-code/page.tsx +++ b/web/app/(shareLayout)/webapp-signin/check-code/page.tsx @@ -93,7 +93,10 @@ export default function CheckCode() {

{t('login.checkCode.checkYourEmail')}

- + + {t('login.checkCode.tipsPrefix')} + {email} +
{t('login.checkCode.validTime')}

diff --git a/web/app/components/app-sidebar/app-info.tsx b/web/app/components/app-sidebar/app-info.tsx index b1a60155ad..a024403368 100644 --- a/web/app/components/app-sidebar/app-info.tsx +++ b/web/app/components/app-sidebar/app-info.tsx @@ -271,16 +271,17 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
- { - expand && ( -
-
-
{appDetail.name}
-
-
{appDetail.mode === 'advanced-chat' ? t('app.types.advanced') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}
-
- ) - } +
+
+
{appDetail.name}
+
+
{appDetail.mode === 'advanced-chat' ? t('app.types.advanced') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}
+
)} diff --git a/web/app/components/app-sidebar/navLink.spec.tsx b/web/app/components/app-sidebar/navLink.spec.tsx new file mode 100644 index 0000000000..6f26c44269 --- /dev/null +++ b/web/app/components/app-sidebar/navLink.spec.tsx @@ -0,0 +1,189 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import '@testing-library/jest-dom' +import NavLink from './navLink' +import type { NavLinkProps } from './navLink' + +// Mock Next.js navigation +jest.mock('next/navigation', () => ({ + useSelectedLayoutSegment: () => 'overview', +})) + +// Mock Next.js Link component +jest.mock('next/link', () => { + return function MockLink({ children, href, className, title }: any) { + return ( + + {children} + + ) + } +}) + +// Mock RemixIcon components +const MockIcon = ({ className }: { className?: string }) => ( + +) + +describe('NavLink Text Animation Issues', () => { + const mockProps: NavLinkProps = { + name: 'Orchestrate', + href: '/app/123/workflow', + iconMap: { + selected: MockIcon, + normal: MockIcon, + }, + } + + beforeEach(() => { + // Mock getComputedStyle for transition testing + Object.defineProperty(window, 'getComputedStyle', { + value: jest.fn((element) => { + const isExpanded = element.getAttribute('data-mode') === 'expand' + return { + transition: 'all 0.3s ease', + opacity: isExpanded ? '1' : '0', + width: isExpanded ? 'auto' : '0px', + overflow: 'hidden', + paddingLeft: isExpanded ? '12px' : '10px', // px-3 vs px-2.5 + paddingRight: isExpanded ? '12px' : '10px', + } + }), + writable: true, + }) + }) + + describe('Text Squeeze Animation Issue', () => { + it('should show text squeeze effect when switching from collapse to expand', async () => { + const { rerender } = render() + + // In collapse mode, text should be in DOM but hidden via CSS + const textElement = screen.getByText('Orchestrate') + expect(textElement).toBeInTheDocument() + expect(textElement).toHaveClass('opacity-0') + expect(textElement).toHaveClass('w-0') + expect(textElement).toHaveClass('overflow-hidden') + + // Icon should still be present + expect(screen.getByTestId('nav-icon')).toBeInTheDocument() + + // Check padding in collapse mode + const linkElement = screen.getByTestId('nav-link') + expect(linkElement).toHaveClass('px-2.5') + + // Switch to expand mode - this is where the squeeze effect occurs + rerender() + + // Text should now appear + expect(screen.getByText('Orchestrate')).toBeInTheDocument() + + // Check padding change - this contributes to the squeeze effect + expect(linkElement).toHaveClass('px-3') + + // The bug: text appears abruptly without smooth transition + // This test documents the current behavior that causes the squeeze effect + const expandedTextElement = screen.getByText('Orchestrate') + expect(expandedTextElement).toBeInTheDocument() + + // In a properly animated version, we would expect: + // - Opacity transition from 0 to 1 + // - Width transition from 0 to auto + // - No layout shift from padding changes + }) + + it('should maintain icon position consistency during text appearance', () => { + const { rerender } = render() + + const iconElement = screen.getByTestId('nav-icon') + const initialIconClasses = iconElement.className + + // Icon should have mr-0 in collapse mode + expect(iconElement).toHaveClass('mr-0') + + rerender() + + const expandedIconClasses = iconElement.className + + // Icon should have mr-2 in expand mode - this shift contributes to the squeeze effect + expect(iconElement).toHaveClass('mr-2') + + console.log('Collapsed icon classes:', initialIconClasses) + console.log('Expanded icon classes:', expandedIconClasses) + + // This margin change causes the icon to shift when text appears + }) + + it('should document the abrupt text rendering issue', () => { + const { rerender } = render() + + // Text is present in DOM but hidden via CSS classes + const collapsedText = screen.getByText('Orchestrate') + expect(collapsedText).toBeInTheDocument() + expect(collapsedText).toHaveClass('opacity-0') + expect(collapsedText).toHaveClass('pointer-events-none') + + rerender() + + // Text suddenly appears in DOM - no transition + expect(screen.getByText('Orchestrate')).toBeInTheDocument() + + // The issue: {mode === 'expand' && name} causes abrupt show/hide + // instead of smooth opacity/width transition + }) + }) + + describe('Layout Shift Issues', () => { + it('should detect padding differences causing layout shifts', () => { + const { rerender } = render() + + const linkElement = screen.getByTestId('nav-link') + + // Collapsed state padding + expect(linkElement).toHaveClass('px-2.5') + + rerender() + + // Expanded state padding - different value causes layout shift + expect(linkElement).toHaveClass('px-3') + + // This 2px difference (10px vs 12px) contributes to the squeeze effect + }) + + it('should detect icon margin changes causing shifts', () => { + const { rerender } = render() + + const iconElement = screen.getByTestId('nav-icon') + + // Collapsed: no right margin + expect(iconElement).toHaveClass('mr-0') + + rerender() + + // Expanded: 8px right margin (mr-2) + expect(iconElement).toHaveClass('mr-2') + + // This sudden margin appearance causes the squeeze effect + }) + }) + + describe('Active State Handling', () => { + it('should handle active state correctly in both modes', () => { + // Test non-active state + const { rerender } = render() + + let linkElement = screen.getByTestId('nav-link') + expect(linkElement).not.toHaveClass('bg-state-accent-active') + + // Test with active state (when href matches current segment) + const activeProps = { + ...mockProps, + href: '/app/123/overview', // matches mocked segment + } + + rerender() + + linkElement = screen.getByTestId('nav-link') + expect(linkElement).toHaveClass('bg-state-accent-active') + }) + }) +}) diff --git a/web/app/components/app-sidebar/navLink.tsx b/web/app/components/app-sidebar/navLink.tsx index b204a1cea4..37b965ea3a 100644 --- a/web/app/components/app-sidebar/navLink.tsx +++ b/web/app/components/app-sidebar/navLink.tsx @@ -86,7 +86,16 @@ const NavLink = ({ )} aria-hidden='true' /> - {mode === 'expand' && name} + + {name} + ) } diff --git a/web/app/components/app-sidebar/sidebar-animation-issues.spec.tsx b/web/app/components/app-sidebar/sidebar-animation-issues.spec.tsx new file mode 100644 index 0000000000..2cf22eb621 --- /dev/null +++ b/web/app/components/app-sidebar/sidebar-animation-issues.spec.tsx @@ -0,0 +1,297 @@ +import React from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import '@testing-library/jest-dom' + +// Simple Mock Components that reproduce the exact UI issues +const MockNavLink = ({ name, mode }: { name: string; mode: string }) => { + return ( + + {/* Icon with inconsistent margin - reproduces issue #2 */} + + {/* Text that appears/disappears abruptly - reproduces issue #2 */} + {mode === 'expand' && {name}} + + ) +} + +const MockSidebarToggleButton = ({ expand, onToggle }: { expand: boolean; onToggle: () => void }) => { + return ( +
+ {/* Top section with variable padding - reproduces issue #1 */} +
+ App Info Area +
+ + {/* Navigation section - reproduces issue #2 */} + + + {/* Toggle button section with consistent padding - issue #1 FIXED */} +
+ +
+
+ ) +} + +const MockAppInfo = ({ expand }: { expand: boolean }) => { + return ( +
+ +
+ ) +} + +describe('Sidebar Animation Issues Reproduction', () => { + beforeEach(() => { + // Mock getBoundingClientRect for position testing + Element.prototype.getBoundingClientRect = jest.fn(() => ({ + width: 200, + height: 40, + x: 10, + y: 10, + left: 10, + right: 210, + top: 10, + bottom: 50, + toJSON: jest.fn(), + })) + }) + + describe('Issue #1: Toggle Button Position Movement - FIXED', () => { + it('should verify consistent padding prevents button position shift', () => { + let expanded = false + const handleToggle = () => { + expanded = !expanded + } + + const { rerender } = render() + + // Check collapsed state padding + const toggleSection = screen.getByTestId('toggle-section') + expect(toggleSection).toHaveClass('px-4') // Consistent padding + expect(toggleSection).not.toHaveClass('px-5') + expect(toggleSection).not.toHaveClass('px-6') + + // Switch to expanded state + rerender() + + // Check expanded state padding - should be the same + expect(toggleSection).toHaveClass('px-4') // Same consistent padding + expect(toggleSection).not.toHaveClass('px-5') + expect(toggleSection).not.toHaveClass('px-6') + + // THE FIX: px-4 in both states prevents position movement + console.log('✅ Issue #1 FIXED: Toggle button now has consistent padding') + console.log(' - Before: px-4 (collapsed) vs px-6 (expanded) - 8px difference') + console.log(' - After: px-4 (both states) - 0px difference') + console.log(' - Result: No button position movement during transition') + }) + + it('should verify sidebar width animation is working correctly', () => { + const handleToggle = jest.fn() + const { rerender } = render() + + const container = screen.getByTestId('sidebar-container') + + // Collapsed state + expect(container).toHaveClass('w-14') + expect(container).toHaveClass('transition-all') + + // Expanded state + rerender() + expect(container).toHaveClass('w-[216px]') + + console.log('✅ Sidebar width transition is properly configured') + }) + }) + + describe('Issue #2: Navigation Text Squeeze Animation', () => { + it('should reproduce text squeeze effect from padding and margin changes', () => { + const { rerender } = render() + + const link = screen.getByTestId('nav-link-Orchestrate') + const icon = screen.getByTestId('nav-icon-Orchestrate') + + // Collapsed state checks + expect(link).toHaveClass('px-2.5') // 10px padding + expect(icon).toHaveClass('mr-0') // No margin + expect(screen.queryByTestId('nav-text-Orchestrate')).not.toBeInTheDocument() + + // Switch to expanded state + rerender() + + // Expanded state checks + expect(link).toHaveClass('px-3') // 12px padding (+2px) + expect(icon).toHaveClass('mr-2') // 8px margin (+8px) + expect(screen.getByTestId('nav-text-Orchestrate')).toBeInTheDocument() + + // THE BUG: Multiple simultaneous changes create squeeze effect + console.log('🐛 Issue #2 Reproduced: Text squeeze effect from multiple layout changes') + console.log(' - Link padding: px-2.5 → px-3 (+2px)') + console.log(' - Icon margin: mr-0 → mr-2 (+8px)') + console.log(' - Text appears: none → visible (abrupt)') + console.log(' - Result: Text appears with squeeze effect due to layout shifts') + }) + + it('should document the abrupt text rendering issue', () => { + const { rerender } = render() + + // Text completely absent + expect(screen.queryByTestId('nav-text-API Access')).not.toBeInTheDocument() + + rerender() + + // Text suddenly appears - no transition + expect(screen.getByTestId('nav-text-API Access')).toBeInTheDocument() + + console.log('🐛 Issue #2 Detail: Conditional rendering {mode === "expand" && name}') + console.log(' - Problem: Text appears/disappears abruptly without transition') + console.log(' - Should use: opacity or width transition for smooth appearance') + }) + }) + + describe('Issue #3: App Icon Bounce Animation', () => { + it('should reproduce icon bounce from layout mode switching', () => { + const { rerender } = render() + + const iconContainer = screen.getByTestId('icon-container') + const appIcon = screen.getByTestId('app-icon') + + // Expanded state layout + expect(iconContainer).toHaveClass('justify-between') + expect(iconContainer).not.toHaveClass('flex-col') + expect(appIcon).toHaveAttribute('data-size', 'large') + + // Switch to collapsed state + rerender() + + // Collapsed state layout - completely different layout mode + expect(iconContainer).toHaveClass('flex-col') + expect(iconContainer).toHaveClass('gap-1') + expect(iconContainer).not.toHaveClass('justify-between') + expect(appIcon).toHaveAttribute('data-size', 'small') + + // THE BUG: Layout mode switch causes icon to "bounce" + console.log('🐛 Issue #3 Reproduced: Icon bounce from layout mode switching') + console.log(' - Layout change: justify-between → flex-col gap-1') + console.log(' - Icon size: large (40px) → small (24px)') + console.log(' - Transition: transition-all causes excessive animation') + console.log(' - Result: Icon appears to bounce to right then back during collapse') + }) + + it('should identify the problematic transition-all property', () => { + render() + + const appIcon = screen.getByTestId('app-icon') + const computedStyle = window.getComputedStyle(appIcon) + + // The problematic broad transition + expect(computedStyle.transition).toContain('all') + + console.log('🐛 Issue #3 Detail: transition-all affects ALL CSS properties') + console.log(' - Problem: Animates layout properties that should not transition') + console.log(' - Solution: Use specific transition properties instead of "all"') + }) + }) + + describe('Interactive Toggle Test', () => { + it('should demonstrate all issues in a single interactive test', () => { + let expanded = false + const handleToggle = () => { + expanded = !expanded + } + + const { rerender } = render( +
+ + +
, + ) + + const toggleButton = screen.getByTestId('toggle-button') + + // Initial state verification + expect(expanded).toBe(false) + console.log('🔄 Starting interactive test - all issues will be reproduced') + + // Simulate toggle click + fireEvent.click(toggleButton) + expanded = true + rerender( +
+ + +
, + ) + + console.log('✨ All three issues successfully reproduced in interactive test:') + console.log(' 1. Toggle button position movement (padding inconsistency)') + console.log(' 2. Navigation text squeeze effect (multiple layout changes)') + console.log(' 3. App icon bounce animation (layout mode switching)') + }) + }) +}) diff --git a/web/app/components/app-sidebar/text-squeeze-fix-verification.spec.tsx b/web/app/components/app-sidebar/text-squeeze-fix-verification.spec.tsx new file mode 100644 index 0000000000..1612606e9d --- /dev/null +++ b/web/app/components/app-sidebar/text-squeeze-fix-verification.spec.tsx @@ -0,0 +1,235 @@ +/** + * Text Squeeze Fix Verification Test + * This test verifies that the CSS-based text rendering fixes work correctly + */ + +import React from 'react' +import { render } from '@testing-library/react' +import '@testing-library/jest-dom' + +// Mock Next.js navigation +jest.mock('next/navigation', () => ({ + useSelectedLayoutSegment: () => 'overview', +})) + +// Mock classnames utility +jest.mock('@/utils/classnames', () => ({ + __esModule: true, + default: (...classes: any[]) => classes.filter(Boolean).join(' '), +})) + +// Simplified NavLink component to test the fix +const TestNavLink = ({ mode }: { mode: 'expand' | 'collapse' }) => { + const name = 'Orchestrate' + + return ( +
+
+
+ Icon +
+ + {name} + +
+
+ ) +} + +// Simplified AppInfo component to test the fix +const TestAppInfo = ({ expand }: { expand: boolean }) => { + const appDetail = { + name: 'Test ChatBot App', + mode: 'chat' as const, + } + + return ( +
+
+
+
AppIcon
+
Dashboard
+
+
+
+
+ {appDetail.name} +
+
+
+ ChatBot +
+
+
+
+ ) +} + +describe('Text Squeeze Fix Verification', () => { + describe('NavLink Text Rendering Fix', () => { + it('should keep text in DOM and use CSS transitions', () => { + const { container, rerender } = render() + + // In collapsed state, text should be in DOM but hidden + const textElement = container.querySelector('[data-testid="nav-text"]') + expect(textElement).toBeInTheDocument() + expect(textElement).toHaveClass('opacity-0') + expect(textElement).toHaveClass('w-0') + expect(textElement).toHaveClass('overflow-hidden') + expect(textElement).toHaveClass('pointer-events-none') + expect(textElement).toHaveClass('whitespace-nowrap') + expect(textElement).toHaveClass('transition-all') + + console.log('✅ NavLink Collapsed State:') + console.log(' - Text is in DOM but visually hidden') + console.log(' - Uses opacity-0 and w-0 for hiding') + console.log(' - Has whitespace-nowrap to prevent wrapping') + console.log(' - Has transition-all for smooth animation') + + // Switch to expanded state + rerender() + + const expandedText = container.querySelector('[data-testid="nav-text"]') + expect(expandedText).toBeInTheDocument() + expect(expandedText).toHaveClass('opacity-100') + expect(expandedText).toHaveClass('w-auto') + expect(expandedText).not.toHaveClass('pointer-events-none') + + console.log('✅ NavLink Expanded State:') + console.log(' - Text is visible with opacity-100') + console.log(' - Uses w-auto for natural width') + console.log(' - No layout jumps during transition') + + console.log('🎯 NavLink Fix Result: Text squeeze effect ELIMINATED') + }) + + it('should verify smooth transition properties', () => { + const { container } = render() + + const textElement = container.querySelector('[data-testid="nav-text"]') + expect(textElement).toHaveClass('transition-all') + expect(textElement).toHaveClass('duration-200') + expect(textElement).toHaveClass('ease-in-out') + + console.log('✅ Transition Properties Verified:') + console.log(' - transition-all: Smooth property changes') + console.log(' - duration-200: 200ms transition time') + console.log(' - ease-in-out: Smooth easing function') + }) + }) + + describe('AppInfo Text Rendering Fix', () => { + it('should keep app text in DOM and use CSS transitions', () => { + const { container, rerender } = render() + + // In collapsed state, text container should be in DOM but hidden + const textContainer = container.querySelector('[data-testid="app-text-container"]') + expect(textContainer).toBeInTheDocument() + expect(textContainer).toHaveClass('opacity-0') + expect(textContainer).toHaveClass('w-0') + expect(textContainer).toHaveClass('overflow-hidden') + expect(textContainer).toHaveClass('pointer-events-none') + + // Text elements should still be in DOM + const appName = container.querySelector('[data-testid="app-name"]') + const appType = container.querySelector('[data-testid="app-type"]') + expect(appName).toBeInTheDocument() + expect(appType).toBeInTheDocument() + expect(appName).toHaveClass('whitespace-nowrap') + expect(appType).toHaveClass('whitespace-nowrap') + + console.log('✅ AppInfo Collapsed State:') + console.log(' - Text container is in DOM but visually hidden') + console.log(' - App name and type elements always present') + console.log(' - Uses whitespace-nowrap to prevent wrapping') + + // Switch to expanded state + rerender() + + const expandedContainer = container.querySelector('[data-testid="app-text-container"]') + expect(expandedContainer).toBeInTheDocument() + expect(expandedContainer).toHaveClass('opacity-100') + expect(expandedContainer).toHaveClass('w-auto') + expect(expandedContainer).not.toHaveClass('pointer-events-none') + + console.log('✅ AppInfo Expanded State:') + console.log(' - Text container is visible with opacity-100') + console.log(' - Uses w-auto for natural width') + console.log(' - No layout jumps during transition') + + console.log('🎯 AppInfo Fix Result: Text squeeze effect ELIMINATED') + }) + + it('should verify transition properties on text container', () => { + const { container } = render() + + const textContainer = container.querySelector('[data-testid="app-text-container"]') + expect(textContainer).toHaveClass('transition-all') + expect(textContainer).toHaveClass('duration-200') + expect(textContainer).toHaveClass('ease-in-out') + + console.log('✅ AppInfo Transition Properties Verified:') + console.log(' - Container has smooth CSS transitions') + console.log(' - Same 200ms duration as NavLink for consistency') + }) + }) + + describe('Fix Strategy Comparison', () => { + it('should document the fix strategy differences', () => { + console.log('\n📋 TEXT SQUEEZE FIX STRATEGY COMPARISON') + console.log('='.repeat(60)) + + console.log('\n❌ BEFORE (Problematic):') + console.log(' NavLink: {mode === "expand" && name}') + console.log(' AppInfo: {expand && (
...
)}') + console.log(' Problem: Conditional rendering causes abrupt appearance') + console.log(' Result: Text "squeezes" from center during layout changes') + + console.log('\n✅ AFTER (Fixed):') + console.log(' NavLink: {name}') + console.log(' AppInfo:
...
') + console.log(' Solution: CSS controls visibility, element always in DOM') + console.log(' Result: Smooth opacity and width transitions') + + console.log('\n🎯 KEY FIX PRINCIPLES:') + console.log(' 1. ✅ Always keep text elements in DOM') + console.log(' 2. ✅ Use opacity for show/hide transitions') + console.log(' 3. ✅ Use width (w-0/w-auto) for layout control') + console.log(' 4. ✅ Add whitespace-nowrap to prevent wrapping') + console.log(' 5. ✅ Use pointer-events-none when hidden') + console.log(' 6. ✅ Add overflow-hidden for clean hiding') + + console.log('\n🚀 BENEFITS:') + console.log(' - No more abrupt text appearance') + console.log(' - Smooth 200ms transitions') + console.log(' - No layout jumps or shifts') + console.log(' - Consistent animation timing') + console.log(' - Better user experience') + + // Always pass documentation test + expect(true).toBe(true) + }) + }) +}) diff --git a/web/app/components/app/annotation/index.tsx b/web/app/components/app/annotation/index.tsx index 0b0691eb7d..bb2a95b0b5 100644 --- a/web/app/components/app/annotation/index.tsx +++ b/web/app/components/app/annotation/index.tsx @@ -146,7 +146,7 @@ const Annotation: FC = (props) => { return (

{t('appLog.description')}

-
+
{isChatApp && ( diff --git a/web/app/components/app/configuration/config-var/config-modal/index.tsx b/web/app/components/app/configuration/config-var/config-modal/index.tsx index 27072f5208..861020545d 100644 --- a/web/app/components/app/configuration/config-var/config-modal/index.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/index.tsx @@ -11,7 +11,7 @@ import SelectTypeItem from '../select-type-item' import Field from './field' import Input from '@/app/components/base/input' import Toast from '@/app/components/base/toast' -import { checkKeys, getNewVarInWorkflow, replaceSpaceWithUnderscreInVarNameInput } from '@/utils/var' +import { checkKeys, getNewVarInWorkflow, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var' import ConfigContext from '@/context/debug-configuration' import type { InputVar, MoreInfo, UploadFileSetting } from '@/app/components/workflow/types' import Modal from '@/app/components/base/modal' @@ -111,7 +111,7 @@ const ConfigModal: FC = ({ }, [checkVariableName, tempPayload.label]) const handleVarNameChange = useCallback((e: ChangeEvent) => { - replaceSpaceWithUnderscreInVarNameInput(e.target) + replaceSpaceWithUnderscoreInVarNameInput(e.target) const value = e.target.value const { isValid, errorKey, errorMessageKey } = checkKeys([value], true) if (!isValid) { diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index b04148d484..b83e9e6a2a 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -688,7 +688,7 @@ const ConversationList: FC = ({ logs, appDetail, onRefresh }) return return ( -
+
diff --git a/web/app/components/app/overview/embedded/index.tsx b/web/app/components/app/overview/embedded/index.tsx index 9d97eae38d..cd25c4ca65 100644 --- a/web/app/components/app/overview/embedded/index.tsx +++ b/web/app/components/app/overview/embedded/index.tsx @@ -46,6 +46,12 @@ const OPTION_MAP = { ? `, baseUrl: '${url}${basePath}'` : ''}, + inputs: { + // You can define the inputs from the Start node here + // key is the variable name + // e.g. + // name: "NAME" + }, systemVariables: { // user_id: 'YOU CAN DEFINE USER ID HERE', // conversation_id: 'YOU CAN DEFINE CONVERSATION ID HERE, IT MUST BE A VALID UUID', diff --git a/web/app/components/apps/footer.tsx b/web/app/components/apps/footer.tsx index 7bee272342..1646474876 100644 --- a/web/app/components/apps/footer.tsx +++ b/web/app/components/apps/footer.tsx @@ -1,6 +1,6 @@ -import React from 'react' +import React, { useState } from 'react' import Link from 'next/link' -import { RiDiscordFill, RiGithubFill } from '@remixicon/react' +import { RiCloseLine, RiDiscordFill, RiGithubFill } from '@remixicon/react' import { useTranslation } from 'react-i18next' type CustomLinkProps = { @@ -26,9 +26,24 @@ const CustomLink = React.memo(({ const Footer = () => { const { t } = useTranslation() + const [isVisible, setIsVisible] = useState(true) + + const handleClose = () => { + setIsVisible(false) + } + + if (!isVisible) + return null return ( -
+
+

{t('app.join')}

{t('app.communityIntro')}

diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index 382ded3201..e88d28879b 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -159,9 +159,21 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { return currentConversationId }, [currentConversationId, newConversationId]) - const { data: appPinnedConversationData, mutate: mutateAppPinnedConversationData } = useSWR(['appConversationData', isInstalledApp, appId, true], () => fetchConversations(isInstalledApp, appId, undefined, true, 100)) - const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100)) - const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId)) + const { data: appPinnedConversationData, mutate: mutateAppPinnedConversationData } = useSWR( + appId ? ['appConversationData', isInstalledApp, appId, true] : null, + () => fetchConversations(isInstalledApp, appId, undefined, true, 100), + { revalidateOnFocus: false, revalidateOnReconnect: false }, + ) + const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR( + appId ? ['appConversationData', isInstalledApp, appId, false] : null, + () => fetchConversations(isInstalledApp, appId, undefined, false, 100), + { revalidateOnFocus: false, revalidateOnReconnect: false }, + ) + const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR( + chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, + () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId), + { revalidateOnFocus: false, revalidateOnReconnect: false }, + ) const [clearChatList, setClearChatList] = useState(false) const [isResponding, setIsResponding] = useState(false) diff --git a/web/app/components/base/emoji-picker/Inner.tsx b/web/app/components/base/emoji-picker/Inner.tsx index 6fc4b67181..6c747c1583 100644 --- a/web/app/components/base/emoji-picker/Inner.tsx +++ b/web/app/components/base/emoji-picker/Inner.tsx @@ -149,7 +149,8 @@ const EmojiPickerInner: FC = ({ {/* Color Select */}

Choose Style

- {showStyleColors ? setShowStyleColors(!showStyleColors)} /> : setShowStyleColors(!showStyleColors)} />} + {showStyleColors ? setShowStyleColors(!showStyleColors)} /> + : setShowStyleColors(!showStyleColors)} />}
{showStyleColors &&
{backgroundColors.map((color) => { diff --git a/web/app/components/base/form/components/base/base-field.tsx b/web/app/components/base/form/components/base/base-field.tsx index 8120fad6b0..00a1f9b2da 100644 --- a/web/app/components/base/form/components/base/base-field.tsx +++ b/web/app/components/base/form/components/base/base-field.tsx @@ -3,6 +3,7 @@ import { memo, useMemo, } from 'react' +import { RiExternalLinkLine } from '@remixicon/react' import type { AnyFieldApi } from '@tanstack/react-form' import { useStore } from '@tanstack/react-form' import cn from '@/utils/classnames' @@ -200,6 +201,22 @@ const BaseField = ({
) } + { + formSchema.url && ( + + + {renderI18nObject(formSchema?.help as any)} + + { + + } + + ) + }
) diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx index 7a546f2536..dffeeb7a1b 100644 --- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx +++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx @@ -10,10 +10,6 @@ import { } from 'lexical' import { mergeRegister } from '@lexical/utils' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' -import { - RiErrorWarningFill, - RiMoreLine, -} from '@remixicon/react' import { useReactFlow, useStoreApi } from 'reactflow' import { useSelectOrDelete } from '../../hooks' import type { WorkflowNodesMap } from './node' @@ -22,18 +18,15 @@ import { DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND, UPDATE_WORKFLOW_NODES_MAP, } from './index' -import cn from '@/utils/classnames' -import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' -import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others' -import { VarBlockIcon } from '@/app/components/workflow/block-icon' -import { Line3 } from '@/app/components/base/icons/src/public/common' import { isConversationVar, isENV, isRagVariableVar, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' import Tooltip from '@/app/components/base/tooltip' import { isExceptionVariable } from '@/app/components/workflow/utils' import VarFullPathPanel from '@/app/components/workflow/nodes/_base/components/variable/var-full-path-panel' import { Type } from '@/app/components/workflow/nodes/llm/types' -import { InputField } from '@/app/components/base/icons/src/vender/pipeline' import type { ValueSelector, Var } from '@/app/components/workflow/types' +import { + VariableLabelInEditor, +} from '@/app/components/workflow/nodes/_base/components/variable/variable-label' type WorkflowVariableBlockComponentProps = { nodeKey: string @@ -128,71 +121,22 @@ const WorkflowVariableBlockComponent = ({ }, [node, reactflow, store]) const Item = ( -
{ e.stopPropagation() handleVariableJump() }} + isExceptionVariable={isException} + errorMsg={!variableValid ? t('workflow.errorMsg.invalidVariable') : undefined} + isSelected={isSelected} ref={ref} - > - {!isEnv && !isChatVar && !isRagVar && ( -
- { - node?.type && ( -
- -
- ) - } -
{node?.title}
- -
- )} - {isShowAPart && ( -
- - -
- )} - -
- {!isEnv && !isChatVar && !isRagVar && } - {isEnv && } - {isChatVar && } - {isRagVar && } -
{varName}
- { - !variableValid && !isRagVar && ( - - ) - } -
-
+ notShowFullPath={isShowAPart} + /> ) - if (!variableValid && !isRagVar) { - return ( - - {Item} - - ) - } - if (!node) return Item diff --git a/web/app/components/base/tag-management/filter.tsx b/web/app/components/base/tag-management/filter.tsx index 1ce56e8f0d..ecc159b2fc 100644 --- a/web/app/components/base/tag-management/filter.tsx +++ b/web/app/components/base/tag-management/filter.tsx @@ -33,6 +33,7 @@ const TagFilter: FC = ({ const tagList = useTagStore(s => s.tagList) const setTagList = useTagStore(s => s.setTagList) + const setShowTagManagementModal = useTagStore(s => s.setShowTagManagementModal) const [keywords, setKeywords] = useState('') const [searchKeywords, setSearchKeywords] = useState('') @@ -136,6 +137,15 @@ const TagFilter: FC = ({ )} +
+
+
setShowTagManagementModal(true)}> + +
+ {t('common.tag.manageTags')} +
+
+
diff --git a/web/app/components/base/tag-management/selector.tsx b/web/app/components/base/tag-management/selector.tsx index cc5ffdeee6..bb1eb98642 100644 --- a/web/app/components/base/tag-management/selector.tsx +++ b/web/app/components/base/tag-management/selector.tsx @@ -17,6 +17,7 @@ export type TagSelectorProps = { selectedTags: Tag[] onCacheUpdate: (tags: Tag[]) => void onChange?: () => void + minWidth?: string } const TagSelector: FC = ({ @@ -28,6 +29,7 @@ const TagSelector: FC = ({ selectedTags, onCacheUpdate, onChange, + minWidth, }) => { const tagList = useTagStore(s => s.tagList) const setTagList = useTagStore(s => s.setTagList) @@ -67,7 +69,7 @@ const TagSelector: FC = ({ '!w-full !border-0 !p-0 !text-text-tertiary hover:!bg-state-base-hover hover:!text-text-secondary', ) } - popupClassName='!w-full !ring-0' + popupClassName={cn('!w-full !ring-0', minWidth && '!min-w-80')} className={'!z-20 h-fit !w-full'} /> )} diff --git a/web/app/components/base/toast/index.tsx b/web/app/components/base/toast/index.tsx index 40352f44c0..57f8c50439 100644 --- a/web/app/components/base/toast/index.tsx +++ b/web/app/components/base/toast/index.tsx @@ -29,6 +29,10 @@ type IToastContext = { close: () => void } +export type ToastHandle = { + clear?: VoidFunction +} + export const ToastContext = createContext({} as IToastContext) export const useToastContext = () => useContext(ToastContext) const Toast = ({ @@ -45,7 +49,9 @@ const Toast = ({ return null return
) => { +}: Pick): ToastHandle => { const defaultDuring = (type === 'success' || type === 'info') ? 3000 : 6000 + const toastHandler: ToastHandle = {} + if (typeof window === 'object') { const holder = document.createElement('div') const root = createRoot(holder) + toastHandler.clear = () => { + if (holder) { + root.unmount() + holder.remove() + } + onClose?.() + } + root.render( , ) document.body.appendChild(holder) - setTimeout(() => { - if (holder) { - root.unmount() - holder.remove() - } - onClose?.() - }, duration || defaultDuring) + setTimeout(toastHandler.clear, duration || defaultDuring) } + + return toastHandler } export default Toast diff --git a/web/app/components/datasets/documents/detail/completed/child-segment-detail.tsx b/web/app/components/datasets/documents/detail/completed/child-segment-detail.tsx index 4fb1e90657..e686226e5f 100644 --- a/web/app/components/datasets/documents/detail/completed/child-segment-detail.tsx +++ b/web/app/components/datasets/documents/detail/completed/child-segment-detail.tsx @@ -60,7 +60,6 @@ const ChildSegmentDetail: FC = ({ const wordCountText = useMemo(() => { const count = content.length return `${formatNumber(count)} ${t('datasetDocuments.segment.characters', { count })}` - // eslint-disable-next-line react-hooks/exhaustive-deps }, [content.length]) const EditTimeText = useMemo(() => { @@ -69,7 +68,6 @@ const ChildSegmentDetail: FC = ({ dateFormat: `${t('datasetDocuments.segment.dateTimeFormat')}`, }) return `${t('datasetDocuments.segment.editedAt')} ${timeText}` - // eslint-disable-next-line react-hooks/exhaustive-deps }, [childChunkInfo?.updated_at]) return ( diff --git a/web/app/components/datasets/documents/detail/index.tsx b/web/app/components/datasets/documents/detail/index.tsx index 9d687d99b1..920b5c60e8 100644 --- a/web/app/components/datasets/documents/detail/index.tsx +++ b/web/app/components/datasets/documents/detail/index.tsx @@ -139,7 +139,12 @@ const DocumentDetail: FC = ({ datasetId, documentId }) => { }) const backToPrev = () => { - router.push(`/datasets/${datasetId}/documents`) + // Preserve pagination and filter states when navigating back + const searchParams = new URLSearchParams(window.location.search) + const queryString = searchParams.toString() + const separator = queryString ? '?' : '' + const backPath = `/datasets/${datasetId}/documents${separator}${queryString}` + router.push(backPath) } const isDetailLoading = !documentDetail && !error diff --git a/web/app/components/datasets/documents/list.tsx b/web/app/components/datasets/documents/list.tsx index 4490af7a60..8457cb2250 100644 --- a/web/app/components/datasets/documents/list.tsx +++ b/web/app/components/datasets/documents/list.tsx @@ -9,7 +9,6 @@ import { } from '@remixicon/react' import { useRouter } from 'next/navigation' import { useTranslation } from 'react-i18next' -import dayjs from 'dayjs' import { Globe01 } from '../../base/icons/src/vender/line/mapsAndTravel' import ChunkingModeLabel from '../common/chunking-mode-label' import FileTypeIcon from '../../base/file-uploader/file-type-icon' @@ -88,7 +87,8 @@ const DocumentList: FC = ({ const isGeneralMode = chunkingMode !== ChunkingMode.parentChild const isQAMode = chunkingMode === ChunkingMode.qa const [localDocs, setLocalDocs] = useState(documents) - const [enableSort, setEnableSort] = useState(true) + const [sortField, setSortField] = useState<'name' | 'word_count' | 'hit_count' | 'created_at' | null>('created_at') + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc') const { isShowEditModal, showEditModal, @@ -103,18 +103,74 @@ const DocumentList: FC = ({ }) useEffect(() => { - setLocalDocs(documents) - }, [documents]) - - const onClickSort = () => { - setEnableSort(!enableSort) - if (enableSort) { - const sortedDocs = [...localDocs].sort((a, b) => dayjs(a.created_at).isBefore(dayjs(b.created_at)) ? -1 : 1) - setLocalDocs(sortedDocs) - } - else { + if (!sortField) { setLocalDocs(documents) + return } + + const sortedDocs = [...documents].sort((a, b) => { + let aValue: any + let bValue: any + + switch (sortField) { + case 'name': + aValue = a.name?.toLowerCase() || '' + bValue = b.name?.toLowerCase() || '' + break + case 'word_count': + aValue = a.word_count || 0 + bValue = b.word_count || 0 + break + case 'hit_count': + aValue = a.hit_count || 0 + bValue = b.hit_count || 0 + break + case 'created_at': + aValue = a.created_at + bValue = b.created_at + break + default: + return 0 + } + + if (sortField === 'name') { + const result = aValue.localeCompare(bValue) + return sortOrder === 'asc' ? result : -result + } + else { + const result = aValue - bValue + return sortOrder === 'asc' ? result : -result + } + }) + + setLocalDocs(sortedDocs) + }, [documents, sortField, sortOrder]) + + const handleSort = (field: 'name' | 'word_count' | 'hit_count' | 'created_at') => { + if (sortField === field) { + setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc') + } + else { + setSortField(field) + setSortOrder('desc') + } + } + + const renderSortHeader = (field: 'name' | 'word_count' | 'hit_count' | 'created_at', label: string) => { + const isActive = sortField === field + const isDesc = isActive && sortOrder === 'desc' + + return ( +
handleSort(field)}> + {label} + +
+ ) } const [currDocument, setCurrDocument] = useState(null) @@ -198,18 +254,17 @@ const DocumentList: FC = ({
- - + + diff --git a/web/app/components/develop/template/template_advanced_chat.en.mdx b/web/app/components/develop/template/template_advanced_chat.en.mdx index bafcb1f99a..66c45b6e4f 100644 --- a/web/app/components/develop/template/template_advanced_chat.en.mdx +++ b/web/app/components/develop/template/template_advanced_chat.en.mdx @@ -80,6 +80,9 @@ Chat applications support session persistence, allowing previous chat history to Auto-generate title, default is `true`. If set to `false`, can achieve async title generation by calling the conversation rename API and setting `auto_generate` to `true`. + + (Optional) Workflow ID to specify a specific version, if not provided, uses the default published version. + (Optional) Trace ID. Used for integration with existing business trace components to achieve end-to-end distributed tracing. If not provided, the system will automatically generate a trace_id. Supports the following three ways to pass, in order of priority:
- Header: via HTTP Header X-Trace-Id, highest priority.
@@ -225,6 +228,9 @@ Chat applications support session persistence, allowing previous chat history to - 400, `provider_not_initialize`, no available model credential configuration - 400, `provider_quota_exceeded`, model invocation quota insufficient - 400, `model_currently_not_support`, current model unavailable + - 400, `workflow_not_found`, specified workflow version not found + - 400, `draft_workflow_error`, cannot use draft workflow version + - 400, `workflow_id_format_error`, invalid workflow_id format, expected UUID format - 400, `completion_request_error`, text generation failed - 500, internal server error @@ -1011,6 +1017,121 @@ Chat applications support session persistence, allowing previous chat history to --- + + +
+ Update the value of a specific conversation variable. This endpoint allows you to modify the value of a variable that was captured during the conversation while preserving its name, type, and description. + + ### Path Parameters + + + + The ID of the conversation containing the variable to update. + + + The ID of the variable to update. + + + + ### Request Body + + + + The new value for the variable. Must match the variable's expected type (string, number, object, etc.). + + + The user identifier, defined by the developer, must ensure uniqueness within the application. + + + + ### Response + + Returns the updated variable object with: + - `id` (string) Variable ID + - `name` (string) Variable name + - `value_type` (string) Variable type (string, number, object, etc.) + - `value` (any) Updated variable value + - `description` (string) Variable description + - `created_at` (int) Creation timestamp + - `updated_at` (int) Last update timestamp + + ### Errors + - 400, `Type mismatch: variable expects {expected_type}, but got {actual_type} type`, Value type doesn't match variable's expected type + - 404, `conversation_not_exists`, Conversation not found + - 404, `conversation_variable_not_exists`, Variable not found + + + + + + + ```bash {{ title: 'cURL' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "Updated Value", + "user": "abc-123" + }' + ``` + + + + + ```bash {{ title: 'String Value' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "New string value", + "user": "abc-123" + }' + ``` + + ```bash {{ title: 'Number Value' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": 42, + "user": "abc-123" + }' + ``` + + ```bash {{ title: 'Object Value' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": {"product": "Widget", "quantity": 10, "price": 29.99}, + "user": "abc-123" + }' + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "variable-uuid-1", + "name": "customer_name", + "value_type": "string", + "value": "Updated Value", + "description": "Customer name extracted from the conversation", + "created_at": 1650000000000, + "updated_at": 1650000001000 + } + ``` + + + + +--- + + + (オプション)ワークフローID、特定のバージョンを指定するために使用、提供されない場合はデフォルトの公開バージョンを使用。 + (オプション)トレースID。既存の業務システムのトレースコンポーネントと連携し、エンドツーエンドの分散トレーシングを実現するために使用します。指定がない場合、システムが自動的に trace_id を生成します。以下の3つの方法で渡すことができ、優先順位は次のとおりです:
- Header:HTTPヘッダー X-Trace-Id で渡す(最優先)。
@@ -225,6 +228,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - 400, `provider_not_initialize`, 利用可能なモデル資格情報構成がありません - 400, `provider_quota_exceeded`, モデル呼び出しクォータが不足しています - 400, `model_currently_not_support`, 現在のモデルが利用できません + - 400, `workflow_not_found`, 指定されたワークフローバージョンが見つかりません + - 400, `draft_workflow_error`, ドラフトワークフローバージョンは使用できません + - 400, `workflow_id_format_error`, ワークフローID形式エラー、UUID形式が必要です - 400, `completion_request_error`, テキスト生成に失敗しました - 500, 内部サーバーエラー @@ -1011,6 +1017,121 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from --- + + +
+ 特定の会話変数の値を更新します。このエンドポイントは、名前、型、説明を保持しながら、会話中にキャプチャされた変数の値を変更することを可能にします。 + + ### パスパラメータ + + + + 更新する変数を含む会話のID。 + + + 更新する変数のID。 + + + + ### リクエストボディ + + + + 変数の新しい値。変数の期待される型(文字列、数値、オブジェクトなど)と一致する必要があります。 + + + ユーザー識別子。開発者によって定義されたルールに従い、アプリケーション内で一意である必要があります。 + + + + ### レスポンス + + 以下を含む更新された変数オブジェクトを返します: + - `id` (string) 変数ID + - `name` (string) 変数名 + - `value_type` (string) 変数型(文字列、数値、オブジェクトなど) + - `value` (any) 更新された変数値 + - `description` (string) 変数の説明 + - `created_at` (int) 作成タイムスタンプ + - `updated_at` (int) 最終更新タイムスタンプ + + ### エラー + - 400, `Type mismatch: variable expects {expected_type}, but got {actual_type} type`, 値の型が変数の期待される型と一致しません + - 404, `conversation_not_exists`, 会話が見つかりません + - 404, `conversation_variable_not_exists`, 変数が見つかりません + + + + + + + ```bash {{ title: 'cURL' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "Updated Value", + "user": "abc-123" + }' + ``` + + + + + ```bash {{ title: '文字列値' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "新しい文字列値", + "user": "abc-123" + }' + ``` + + ```bash {{ title: '数値' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": 42, + "user": "abc-123" + }' + ``` + + ```bash {{ title: 'オブジェクト値' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": {"product": "Widget", "quantity": 10, "price": 29.99}, + "user": "abc-123" + }' + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "variable-uuid-1", + "name": "customer_name", + "value_type": "string", + "value": "Updated Value", + "description": "会話から抽出された顧客名", + "created_at": 1650000000000, + "updated_at": 1650000001000 + } + ``` + + + + +--- + (选填)自动生成标题,默认 `true`。 若设置为 `false`,则可通过调用会话重命名接口并设置 `auto_generate` 为 `true` 实现异步生成标题。 + + (选填)工作流ID,用于指定特定版本,如果不提供则使用默认的已发布版本。 + (选填)链路追踪ID。适用于与业务系统已有的trace组件打通,实现端到端分布式追踪等场景。如果未指定,系统会自动生成trace_id。支持以下三种方式传递,具体优先级依次为:
- Header:通过 HTTP Header X-Trace-Id 传递,优先级最高。
@@ -224,6 +227,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' - 400,`provider_not_initialize`,无可用模型凭据配置 - 400,`provider_quota_exceeded`,模型调用额度不足 - 400,`model_currently_not_support`,当前模型不可用 + - 400,`workflow_not_found`,指定的工作流版本未找到 + - 400,`draft_workflow_error`,无法使用草稿工作流版本 + - 400,`workflow_id_format_error`,工作流ID格式错误,需要UUID格式 - 400,`completion_request_error`,文本生成失败 - 500,服务内部异常 @@ -1049,6 +1055,121 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' --- + + +
+ 更新特定对话变量的值。此端点允许您修改在对话过程中捕获的变量值,同时保留其名称、类型和描述。 + + ### 路径参数 + + + + 包含要更新变量的对话ID。 + + + 要更新的变量ID。 + + + + ### 请求体 + + + + 变量的新值。必须匹配变量的预期类型(字符串、数字、对象等)。 + + + 用户标识符,由开发人员定义的规则,在应用程序内必须唯一。 + + + + ### 响应 + + 返回包含以下内容的更新变量对象: + - `id` (string) 变量ID + - `name` (string) 变量名称 + - `value_type` (string) 变量类型(字符串、数字、对象等) + - `value` (any) 更新后的变量值 + - `description` (string) 变量描述 + - `created_at` (int) 创建时间戳 + - `updated_at` (int) 最后更新时间戳 + + ### 错误 + - 400, `Type mismatch: variable expects {expected_type}, but got {actual_type} type`, 值类型与变量的预期类型不匹配 + - 404, `conversation_not_exists`, 对话不存在 + - 404, `conversation_variable_not_exists`, 变量不存在 + + + + + + + ```bash {{ title: 'cURL' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "Updated Value", + "user": "abc-123" + }' + ``` + + + + + ```bash {{ title: '字符串值' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "新的字符串值", + "user": "abc-123" + }' + ``` + + ```bash {{ title: '数字值' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": 42, + "user": "abc-123" + }' + ``` + + ```bash {{ title: '对象值' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": {"product": "Widget", "quantity": 10, "price": 29.99}, + "user": "abc-123" + }' + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "variable-uuid-1", + "name": "customer_name", + "value_type": "string", + "value": "Updated Value", + "description": "客户名称(从对话中提取)", + "created_at": 1650000000000, + "updated_at": 1650000001000 + } + ``` + + + + +--- + + + (Optional) Workflow ID to specify a specific version, if not provided, uses the default published version. + (Optional) Trace ID. Used for integration with existing business trace components to achieve end-to-end distributed tracing. If not provided, the system will automatically generate a trace_id. Supports the following three ways to pass, in order of priority:
- Header: via HTTP Header X-Trace-Id, highest priority.
@@ -180,6 +183,9 @@ Chat applications support session persistence, allowing previous chat history to - 400, `provider_not_initialize`, no available model credential configuration - 400, `provider_quota_exceeded`, model invocation quota insufficient - 400, `model_currently_not_support`, current model unavailable + - 400, `workflow_not_found`, specified workflow version not found + - 400, `draft_workflow_error`, cannot use draft workflow version + - 400, `workflow_id_format_error`, invalid workflow_id format, expected UUID format - 400, `completion_request_error`, text generation failed - 500, internal server error @@ -1045,6 +1051,121 @@ Chat applications support session persistence, allowing previous chat history to --- + + +
+ Update the value of a specific conversation variable. This endpoint allows you to modify the value of a variable that was captured during the conversation while preserving its name, type, and description. + + ### Path Parameters + + + + The ID of the conversation containing the variable to update. + + + The ID of the variable to update. + + + + ### Request Body + + + + The new value for the variable. Must match the variable's expected type (string, number, object, etc.). + + + The user identifier, defined by the developer, must ensure uniqueness within the application. + + + + ### Response + + Returns the updated variable object with: + - `id` (string) Variable ID + - `name` (string) Variable name + - `value_type` (string) Variable type (string, number, object, etc.) + - `value` (any) Updated variable value + - `description` (string) Variable description + - `created_at` (int) Creation timestamp + - `updated_at` (int) Last update timestamp + + ### Errors + - 400, `Type mismatch: variable expects {expected_type}, but got {actual_type} type`, Value type doesn't match variable's expected type + - 404, `conversation_not_exists`, Conversation not found + - 404, `conversation_variable_not_exists`, Variable not found + + + + + + + ```bash {{ title: 'cURL' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "Updated Value", + "user": "abc-123" + }' + ``` + + + + + ```bash {{ title: 'String Value' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "New string value", + "user": "abc-123" + }' + ``` + + ```bash {{ title: 'Number Value' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": 42, + "user": "abc-123" + }' + ``` + + ```bash {{ title: 'Object Value' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": {"product": "Widget", "quantity": 10, "price": 29.99}, + "user": "abc-123" + }' + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "variable-uuid-1", + "name": "customer_name", + "value_type": "string", + "value": "Updated Value", + "description": "Customer name extracted from the conversation", + "created_at": 1650000000000, + "updated_at": 1650000001000 + } + ``` + + + + +--- + + + (オプション)ワークフローID、特定のバージョンを指定するために使用、提供されない場合はデフォルトの公開バージョンを使用。 + (オプション)トレースID。既存の業務システムのトレースコンポーネントと連携し、エンドツーエンドの分散トレーシングを実現するために使用します。指定がない場合、システムが自動的に trace_id を生成します。以下の3つの方法で渡すことができ、優先順位は次のとおりです:
- Header:HTTPヘッダー X-Trace-Id で渡す(最優先)。
@@ -180,6 +183,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - 400, `provider_not_initialize`, 利用可能なモデル資格情報構成がありません - 400, `provider_quota_exceeded`, モデル呼び出しクォータが不足しています - 400, `model_currently_not_support`, 現在のモデルは利用できません + - 400, `workflow_not_found`, 指定されたワークフローバージョンが見つかりません + - 400, `draft_workflow_error`, ドラフトワークフローバージョンは使用できません + - 400, `workflow_id_format_error`, ワークフローID形式エラー、UUID形式が必要です - 400, `completion_request_error`, テキスト生成に失敗しました - 500, 内部サーバーエラー @@ -1044,6 +1050,121 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from --- + + +
+ 特定の会話変数の値を更新します。このエンドポイントは、名前、型、説明を保持しながら、会話中にキャプチャされた変数の値を変更することを可能にします。 + + ### パスパラメータ + + + + 更新する変数を含む会話のID。 + + + 更新する変数のID。 + + + + ### リクエストボディ + + + + 変数の新しい値。変数の期待される型(文字列、数値、オブジェクトなど)と一致する必要があります。 + + + ユーザー識別子。開発者によって定義されたルールに従い、アプリケーション内で一意である必要があります。 + + + + ### レスポンス + + 以下を含む更新された変数オブジェクトを返します: + - `id` (string) 変数ID + - `name` (string) 変数名 + - `value_type` (string) 変数型(文字列、数値、オブジェクトなど) + - `value` (any) 更新された変数値 + - `description` (string) 変数の説明 + - `created_at` (int) 作成タイムスタンプ + - `updated_at` (int) 最終更新タイムスタンプ + + ### エラー + - 400, `Type mismatch: variable expects {expected_type}, but got {actual_type} type`, 値の型が変数の期待される型と一致しません + - 404, `conversation_not_exists`, 会話が見つかりません + - 404, `conversation_variable_not_exists`, 変数が見つかりません + + + + + + + ```bash {{ title: 'cURL' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "Updated Value", + "user": "abc-123" + }' + ``` + + + + + ```bash {{ title: '文字列値' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "新しい文字列値", + "user": "abc-123" + }' + ``` + + ```bash {{ title: '数値' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": 42, + "user": "abc-123" + }' + ``` + + ```bash {{ title: 'オブジェクト値' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": {"product": "Widget", "quantity": 10, "price": 29.99}, + "user": "abc-123" + }' + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "variable-uuid-1", + "name": "customer_name", + "value_type": "string", + "value": "Updated Value", + "description": "会話から抽出された顧客名", + "created_at": 1650000000000, + "updated_at": 1650000001000 + } + ``` + + + + +--- + (选填)自动生成标题,默认 `true`。 若设置为 `false`,则可通过调用会话重命名接口并设置 `auto_generate` 为 `true` 实现异步生成标题。 + + (选填)工作流ID,用于指定特定版本,如果不提供则使用默认的已发布版本。 + (选填)链路追踪ID。适用于与业务系统已有的trace组件打通,实现端到端分布式追踪等场景。如果未指定,系统会自动生成trace_id。支持以下三种方式传递,具体优先级依次为:
- Header:通过 HTTP Header X-Trace-Id 传递,优先级最高。
@@ -181,6 +184,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' - 400,`provider_not_initialize`,无可用模型凭据配置 - 400,`provider_quota_exceeded`,模型调用额度不足 - 400,`model_currently_not_support`,当前模型不可用 + - 400,`workflow_not_found`,指定的工作流版本未找到 + - 400,`draft_workflow_error`,无法使用草稿工作流版本 + - 400,`workflow_id_format_error`,工作流ID格式错误,需要UUID格式 - 400,`completion_request_error`,文本生成失败 - 500,服务内部异常 @@ -1060,6 +1066,121 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' --- + + +
+ 更新特定对话变量的值。此端点允许您修改在对话过程中捕获的变量值,同时保留其名称、类型和描述。 + + ### 路径参数 + + + + 包含要更新变量的对话ID。 + + + 要更新的变量ID。 + + + + ### 请求体 + + + + 变量的新值。必须匹配变量的预期类型(字符串、数字、对象等)。 + + + 用户标识符,由开发人员定义的规则,在应用程序内必须唯一。 + + + + ### 响应 + + 返回包含以下内容的更新变量对象: + - `id` (string) 变量ID + - `name` (string) 变量名称 + - `value_type` (string) 变量类型(字符串、数字、对象等) + - `value` (any) 更新后的变量值 + - `description` (string) 变量描述 + - `created_at` (int) 创建时间戳 + - `updated_at` (int) 最后更新时间戳 + + ### 错误 + - 400, `Type mismatch: variable expects {expected_type}, but got {actual_type} type`, 值类型与变量的预期类型不匹配 + - 404, `conversation_not_exists`, 对话不存在 + - 404, `conversation_variable_not_exists`, 变量不存在 + + + + + + + ```bash {{ title: 'cURL' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "Updated Value", + "user": "abc-123" + }' + ``` + + + + + ```bash {{ title: '字符串值' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "新的字符串值", + "user": "abc-123" + }' + ``` + + ```bash {{ title: '数字值' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": 42, + "user": "abc-123" + }' + ``` + + ```bash {{ title: '对象值' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": {"product": "Widget", "quantity": 10, "price": 29.99}, + "user": "abc-123" + }' + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "variable-uuid-1", + "name": "customer_name", + "value_type": "string", + "value": "Updated Value", + "description": "客户名称(从对话中提取)", + "created_at": 1650000000000, + "updated_at": 1650000001000 + } + ``` + + + + +--- + + + + Execute a specific version of workflow by specifying the workflow ID in the path parameter. + + ### Path + - `workflow_id` (string) Required Workflow ID to specify a specific version of workflow + + How to obtain: In the version history interface, click the copy icon on the right side of each version entry to copy the complete workflow ID. Each version entry contains a copyable ID field. + + ### Request Body + - `inputs` (object) Required + Allows the entry of various variable values defined by the App. + The `inputs` parameter contains multiple key/value pairs, with each key corresponding to a specific variable and each value being the specific value for that variable. + The workflow application requires at least one key/value pair to be inputted. The variable can be of File Array type. + File Array type variable is suitable for inputting files combined with text understanding and answering questions, available only when the model supports file parsing and understanding capability. + If the variable is of File Array type, the corresponding value should be a list whose elements contain following attributions: + - `type` (string) Supported type: + - `document` ('TXT', 'MD', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB') + - `image` ('JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG') + - `audio` ('MP3', 'M4A', 'WAV', 'WEBM', 'AMR') + - `video` ('MP4', 'MOV', 'MPEG', 'MPGA') + - `custom` (Other file types) + - `transfer_method` (string) Transfer method, `remote_url` for image URL / `local_file` for file upload + - `url` (string) Image URL (when the transfer method is `remote_url`) + - `upload_file_id` (string) Uploaded file ID, which must be obtained by uploading through the File Upload API in advance (when the transfer method is `local_file`) + + - `response_mode` (string) Required + The mode of response return, supporting: + - `streaming` Streaming mode (recommended), implements a typewriter-like output through SSE ([Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)). + - `blocking` Blocking mode, returns result after execution is complete. (Requests may be interrupted if the process is long) + Due to Cloudflare restrictions, the request will be interrupted without a return after 100 seconds. + - `user` (string) Required + User identifier, used to define the identity of the end-user for retrieval and statistics. + Should be uniquely defined by the developer within the application. +
+ The user identifier should be consistent with the user passed in the message sending interface. The Service API does not share conversations created by the WebApp. + - `files` (array[object]) Optional + - `trace_id` (string) Optional + Trace ID. Used for integration with existing business trace components to achieve end-to-end distributed tracing. If not provided, the system will automatically generate a trace_id. Supports the following three ways to pass, in order of priority: + 1. Header: via HTTP Header `X-Trace-Id`, highest priority. + 2. Query parameter: via URL query parameter `trace_id`. + 3. Request Body: via request body field `trace_id` (i.e., this field). + + ### Response + When `response_mode` is `blocking`, return a CompletionResponse object. + When `response_mode` is `streaming`, return a ChunkCompletionResponse stream. + + ### CompletionResponse + Returns the App result, `Content-Type` is `application/json`. + - `workflow_run_id` (string) Unique ID of workflow execution + - `task_id` (string) Task ID, used for request tracking and the below Stop Generate API + - `data` (object) detail of result + - `id` (string) ID of workflow execution + - `workflow_id` (string) ID of related workflow + - `status` (string) status of execution, `running` / `succeeded` / `failed` / `stopped` + - `outputs` (json) Optional content of output + - `error` (string) Optional reason of error + - `elapsed_time` (float) Optional total seconds to be used + - `total_tokens` (int) Optional tokens to be used + - `total_steps` (int) default 0 + - `created_at` (timestamp) start time + - `finished_at` (timestamp) end time + + ### ChunkCompletionResponse + Returns the stream chunks outputted by the App, `Content-Type` is `text/event-stream`. + Each streaming chunk starts with `data:`, separated by two newline characters `\n\n`, as shown below: + + ```streaming {{ title: 'Response' }} + data: {"event": "text_chunk", "workflow_run_id": "b85e5fc5-751b-454d-b14e-dc5f240b0a31", "task_id": "bd029338-b068-4d34-a331-fc85478922c2", "data": {"text": "\u4e3a\u4e86", "from_variable_selector": ["1745912968134", "text"]}}\n\n + ``` + + The structure of the streaming chunks varies depending on the `event`: + - `event: workflow_started` workflow starts execution + - `task_id` (string) Task ID, used for request tracking and the below Stop Generate API + - `workflow_run_id` (string) Unique ID of workflow execution + - `event` (string) fixed to `workflow_started` + - `data` (object) detail + - `id` (string) Unique ID of workflow execution + - `workflow_id` (string) ID of related workflow + - `created_at` (timestamp) Creation timestamp, e.g., 1705395332 + - `event: node_started` node execution started + - `task_id` (string) Task ID, used for request tracking and the below Stop Generate API + - `workflow_run_id` (string) Unique ID of workflow execution + - `event` (string) fixed to `node_started` + - `data` (object) detail + - `id` (string) Unique ID of workflow execution + - `node_id` (string) ID of node + - `node_type` (string) type of node + - `title` (string) name of node + - `index` (int) Execution sequence number, used to display Tracing Node sequence + - `predecessor_node_id` (string) optional Prefix node ID, used for canvas display execution path + - `inputs` (object) Contents of all preceding node variables used in the node + - `created_at` (timestamp) timestamp of start, e.g., 1705395332 + - `event: text_chunk` Text fragment + - `task_id` (string) Task ID, used for request tracking and the below Stop Generate API + - `workflow_run_id` (string) Unique ID of workflow execution + - `event` (string) fixed to `text_chunk` + - `data` (object) detail + - `text` (string) Text content + - `from_variable_selector` (array) Text source path, helps developers understand which variable of which node the text is generated from + - `event: node_finished` node execution finished, success and failure are different states in the same event + - `task_id` (string) Task ID, used for request tracking and the below Stop Generate API + - `workflow_run_id` (string) Unique ID of workflow execution + - `event` (string) fixed to `node_finished` + - `data` (object) detail + - `id` (string) Unique ID of node execution + - `node_id` (string) ID of node + - `index` (int) Execution sequence number, used to display Tracing Node sequence + - `predecessor_node_id` (string) optional Prefix node ID, used for canvas display execution path + - `inputs` (object) Contents of all preceding node variables used in the node + - `process_data` (json) Optional Process data of node + - `outputs` (json) Optional content of output + - `status` (string) status of execution `running` / `succeeded` / `failed` / `stopped` + - `error` (string) Optional reason of error + - `elapsed_time` (float) Optional total seconds to be used + - `execution_metadata` (json) metadata + - `total_tokens` (int) optional tokens to be used + - `total_price` (decimal) optional total cost + - `currency` (string) optional currency, such as `USD` / `RMB` + - `created_at` (timestamp) timestamp of start, e.g., 1705395332 + - `event: workflow_finished` workflow execution finished, success and failure are different states in the same event + - `task_id` (string) Task ID, used for request tracking and the below Stop Generate API + - `workflow_run_id` (string) Unique ID of workflow execution + - `event` (string) fixed to `workflow_finished` + - `data` (object) detail + - `id` (string) Unique ID of workflow execution + - `workflow_id` (string) ID of related workflow + - `status` (string) status of execution `running` / `succeeded` / `failed` / `stopped` + - `outputs` (json) Optional content of output + - `error` (string) Optional reason of error + - `elapsed_time` (float) Optional total seconds to be used + - `total_tokens` (int) Optional tokens to be used + - `total_steps` (int) default 0 + - `created_at` (timestamp) start time + - `finished_at` (timestamp) end time + - `event: tts_message` TTS audio stream event, i.e., speech synthesis output. The content is an audio block in Mp3 format, encoded as a base64 string, which can be decoded directly when playing. (Only available when auto-play is enabled) + - `task_id` (string) Task ID, used for request tracking and the below Stop Generate API + - `message_id` (string) Unique message ID + - `audio` (string) The audio block after speech synthesis is encoded as base64 text content, which can be directly base64 decoded and sent to the player when playing + - `created_at` (int) Creation timestamp, e.g., 1705395332 + - `event: tts_message_end` TTS audio stream end event, receiving this event indicates the end of audio stream return. + - `task_id` (string) Task ID, used for request tracking and the below Stop Generate API + - `message_id` (string) Unique message ID + - `audio` (string) The end event has no audio, so this is an empty string + - `created_at` (int) Creation timestamp, e.g., 1705395332 + - `event: ping` Ping event every 10s to keep the connection alive. + + ### Errors + - 400, `invalid_param`, Invalid input parameters + - 400, `app_unavailable`, App configuration unavailable + - 400, `provider_not_initialize`, No available model credentials configured + - 400, `provider_quota_exceeded`, Insufficient model call quota + - 400, `model_currently_not_support`, Current model unavailable + - 400, `workflow_not_found`, Specified workflow version not found + - 400, `draft_workflow_error`, Cannot use draft workflow version + - 400, `workflow_id_format_error`, Workflow ID format error, UUID format required + - 400, `workflow_request_error`, Workflow execution failed + - 500, Internal service error + + + + + ```bash {{ title: 'cURL' }} + curl -X POST '${props.appDetail.api_base_url}/workflows/{workflow_id}/run' \ + --header 'Authorization: Bearer {api_key}' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "inputs": {}, + "response_mode": "streaming", + "user": "abc-123" + }' + ``` + + + ```json {{ title: 'File variable example' }} + { + "inputs": { + "{variable_name}": + [ + { + "transfer_method": "local_file", + "upload_file_id": "{upload_file_id}", + "type": "{document_type}" + } + ] + } + } + ``` + + ### Blocking Mode + + ```json {{ title: 'Response' }} + { + "workflow_run_id": "djflajgkldjgd", + "task_id": "9da23599-e713-473b-982c-4328d4f5c78a", + "data": { + "id": "fdlsjfjejkghjda", + "workflow_id": "fldjaslkfjlsda", + "status": "succeeded", + "outputs": { + "text": "Nice to meet you." + }, + "error": null, + "elapsed_time": 0.875, + "total_tokens": 3562, + "total_steps": 8, + "created_at": 1705407629, + "finished_at": 1727807631 + } + } + ``` + + ### Streaming Mode + + ```streaming {{ title: 'Response' }} + data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "created_at": 1679586595}} + data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}} + data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}} + data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}} + data: {"event": "tts_message", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"} + data: {"event": "tts_message_end", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": ""} + ``` + + + + +--- + + + + パスパラメータでワークフローIDを指定して、特定バージョンのワークフローを実行します。 + + ### パス + - `workflow_id` (string) 必須 特定バージョンのワークフローを指定するためのワークフローID + + 取得方法:バージョン履歴で特定バージョンのワークフローIDを照会できます。 + + ### リクエストボディ + - `inputs` (object) 必須 + App で定義された各変数値を入力できます。 + inputs パラメータには複数のキー/値ペアが含まれており、各キーは特定の変数に対応し、各値はその変数の具体的な値です。変数はファイルリスト型にすることができます。 + ファイルリスト型変数は、ファイルをテキスト理解と組み合わせて質問に答えるために入力するのに適しており、モデルがファイル解析機能をサポートしている場合のみ使用できます。変数がファイルリスト型の場合、その変数に対応する値はリスト形式である必要があり、各要素には以下の内容が含まれます: + - `type` (string) サポートされるタイプ: + - `document` 具体的なタイプには以下が含まれます:'TXT', 'MD', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB' + - `image` 具体的なタイプには以下が含まれます:'JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG' + - `audio` 具体的なタイプには以下が含まれます:'MP3', 'M4A', 'WAV', 'WEBM', 'AMR' + - `video` 具体的なタイプには以下が含まれます:'MP4', 'MOV', 'MPEG', 'MPGA' + - `custom` 具体的なタイプには以下が含まれます:その他のファイルタイプ + - `transfer_method` (string) 転送方法、`remote_url` 画像URL / `local_file` ファイルアップロード + - `url` (string) 画像URL(転送方法が `remote_url` の場合のみ) + - `upload_file_id` (string) アップロードされたファイルID(転送方法が `local_file` の場合のみ) + - `response_mode` (string) 必須 + 応答返却モード、以下をサポート: + - `streaming` ストリーミングモード(推奨)。SSE(**[Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)**)をベースにタイプライター風の出力を実現。 + - `blocking` ブロッキングモード、実行完了後に結果を返却。(プロセスが長い場合、リクエストが中断される可能性があります)。 + Cloudflare の制限により、100秒後に応答がない場合、リクエストは中断されます。 + - `user` (string) 必須 + ユーザー識別子、エンドユーザーのアイデンティティを定義し、検索・統計を容易にするために使用されます。 + 開発者が定義するルールで、アプリケーション内でユーザー識別子が一意である必要があります。API は WebApp で作成されたセッションにアクセスできません。 + - `files` (array[object]) オプション + - `trace_id` (string) オプション + トレースID。既存のビジネスシステムのトレースコンポーネントと統合して、エンドツーエンドの分散トレーシングを実現するために使用されます。指定されていない場合、システムは自動的に `trace_id` を生成します。以下の3つの方法で渡すことができ、優先順位は以下の通りです: + 1. ヘッダー:HTTP ヘッダー `X-Trace-Id` で渡すことを推奨、最高優先度。 + 2. クエリパラメータ:URL クエリパラメータ `trace_id` で渡す。 + 3. リクエストボディ:リクエストボディフィールド `trace_id` で渡す(つまり、このフィールド)。 + + ### 応答 + `response_mode` が `blocking` の場合、CompletionResponse オブジェクトを返します。 + `response_mode` が `streaming` の場合、ChunkCompletionResponse オブジェクトのストリーミングシーケンスを返します。 + + ### CompletionResponse + 完全な App 結果を返し、`Content-Type` は `application/json` です。 + - `workflow_run_id` (string) ワークフロー実行ID + - `task_id` (string) タスクID、リクエスト追跡と以下の停止応答インターフェースに使用 + - `data` (object) 詳細内容 + - `id` (string) ワークフロー実行ID + - `workflow_id` (string) 関連するワークフローID + - `status` (string) 実行ステータス、`running` / `succeeded` / `failed` / `stopped` + - `outputs` (json) オプション 出力内容 + - `error` (string) オプション エラー理由 + - `elapsed_time` (float) オプション 使用時間(s) + - `total_tokens` (int) オプション 使用されるトークンの総数 + - `total_steps` (int) 総ステップ数(冗長)、デフォルト 0 + - `created_at` (timestamp) 開始時間 + - `finished_at` (timestamp) 終了時間 + + ### ChunkCompletionResponse + App の出力ストリーミングチャンクを返し、`Content-Type` は `text/event-stream` です。 + 各ストリーミングチャンクは `data:` で始まり、チャンク間は `\n\n` つまり2つの改行文字で区切られます。以下のようになります: + + ```streaming {{ title: '応答' }} + data: {"event": "text_chunk", "workflow_run_id": "b85e5fc5-751b-454d-b14e-dc5f240b0a31", "task_id": "bd029338-b068-4d34-a331-fc85478922c2", "data": {"text": "\u4e3a\u4e86", "from_variable_selector": ["1745912968134", "text"]}}\n\n + ``` + + ストリーミングチャンクは `event` によって構造が異なり、以下のタイプが含まれます: + - `event: workflow_started` ワークフロー実行開始 + - `task_id` (string) タスクID、リクエスト追跡と以下の停止応答インターフェースに使用 + - `workflow_run_id` (string) ワークフロー実行ID + - `event` (string) `workflow_started` に固定 + - `data` (object) 詳細内容 + - `id` (string) ワークフロー実行ID + - `workflow_id` (string) 関連するワークフローID + - `created_at` (timestamp) 開始時間 + - `event: node_started` ノード実行開始 + - `task_id` (string) タスクID、リクエスト追跡と以下の停止応答インターフェースに使用 + - `workflow_run_id` (string) ワークフロー実行ID + - `event` (string) `node_started` に固定 + - `data` (object) 詳細内容 + - `id` (string) ワークフロー実行ID + - `node_id` (string) ノードID + - `node_type` (string) ノードタイプ + - `title` (string) ノード名 + - `index` (int) 実行シーケンス番号、Tracing Node シーケンスの表示に使用 + - `predecessor_node_id` (string) 前置ノードID、キャンバス表示実行パスに使用 + - `inputs` (object) ノードで使用されるすべての前置ノード変数の内容 + - `created_at` (timestamp) 開始時間 + - `event: text_chunk` テキストフラグメント + - `task_id` (string) タスクID、リクエスト追跡と以下の停止応答インターフェースに使用 + - `workflow_run_id` (string) ワークフロー実行ID + - `event` (string) `text_chunk` に固定 + - `data` (object) 詳細内容 + - `text` (string) テキスト内容 + - `from_variable_selector` (array) テキストソースパス、開発者がテキストがどのノードのどの変数から生成されたかを理解するのに役立ちます + - `event: node_finished` ノード実行終了、成功と失敗は同じイベント内の異なる状態 + - `task_id` (string) タスクID、リクエスト追跡と以下の停止応答インターフェースに使用 + - `workflow_run_id` (string) ワークフロー実行ID + - `event` (string) `node_finished` に固定 + - `data` (object) 詳細内容 + - `id` (string) ノード実行ID + - `node_id` (string) ノードID + - `index` (int) 実行シーケンス番号、Tracing Node シーケンスの表示に使用 + - `predecessor_node_id` (string) オプション 前置ノードID、キャンバス表示実行パスに使用 + - `inputs` (object) ノードで使用されるすべての前置ノード変数の内容 + - `process_data` (json) オプション ノードプロセスデータ + - `outputs` (json) オプション 出力内容 + - `status` (string) 実行ステータス `running` / `succeeded` / `failed` / `stopped` + - `error` (string) オプション エラー理由 + - `elapsed_time` (float) オプション 使用時間(s) + - `execution_metadata` (json) メタデータ + - `total_tokens` (int) オプション 使用されるトークンの総数 + - `total_price` (decimal) オプション 総費用 + - `currency` (string) オプション 通貨、例:`USD` / `RMB` + - `created_at` (timestamp) 開始時間 + - `event: workflow_finished` ワークフロー実行終了、成功と失敗は同じイベント内の異なる状態 + - `task_id` (string) タスクID、リクエスト追跡と以下の停止応答インターフェースに使用 + - `workflow_run_id` (string) ワークフロー実行ID + - `event` (string) `workflow_finished` に固定 + - `data` (object) 詳細内容 + - `id` (string) ワークフロー実行ID + - `workflow_id` (string) 関連するワークフローID + - `status` (string) 実行ステータス `running` / `succeeded` / `failed` / `stopped` + - `outputs` (json) オプション 出力内容 + - `error` (string) オプション エラー理由 + - `elapsed_time` (float) オプション 使用時間(s) + - `total_tokens` (int) オプション 使用されるトークンの総数 + - `total_steps` (int) 総ステップ数(冗長)、デフォルト 0 + - `created_at` (timestamp) 開始時間 + - `finished_at` (timestamp) 終了時間 + - `event: tts_message` TTS オーディオストリームイベント、つまり:音声合成出力。内容はMp3形式のオーディオブロックで、base64エンコードされた文字列として、再生時に直接デコードできます。(自動再生が有効な場合のみこのメッセージがあります) + - `task_id` (string) タスクID、リクエスト追跡と以下の停止応答インターフェースに使用 + - `message_id` (string) メッセージ一意ID + - `audio` (string) 音声合成後のオーディオブロックはbase64エンコードされたテキスト内容として、再生時に直接base64デコードしてプレーヤーに送信できます + - `created_at` (int) 作成タイムスタンプ、例:1705395332 + - `event: tts_message_end` TTS オーディオストリーム終了イベント、このイベントを受信すると、オーディオストリームの返却が終了したことを示します。 + - `task_id` (string) タスクID、リクエスト追跡と以下の停止応答インターフェースに使用 + - `message_id` (string) メッセージ一意ID + - `audio` (string) 終了イベントにはオーディオがないため、ここは空文字列です + - `created_at` (int) 作成タイムスタンプ、例:1705395332 + - `event: ping` 10秒ごとのpingイベント、接続を維持します。 + + ### エラー + - 400,`invalid_param`,入力パラメータ異常 + - 400,`app_unavailable`,App 設定が利用できません + - 400,`provider_not_initialize`,利用可能なモデル認証情報設定がありません + - 400,`provider_quota_exceeded`,モデル呼び出しクォータが不足しています + - 400,`model_currently_not_support`,現在のモデルが利用できません + - 400,`workflow_not_found`,指定されたワークフローバージョンが見つかりません + - 400,`draft_workflow_error`,ドラフトワークフローバージョンを使用できません + - 400,`workflow_id_format_error`,ワークフローID形式エラー、UUID形式が必要です + - 400,`workflow_request_error`,ワークフロー実行に失敗しました + - 500,サービス内部異常 + + + + + ```bash {{ title: 'cURL' }} + curl -X POST '${props.appDetail.api_base_url}/workflows/{workflow_id}/run' \ + --header 'Authorization: Bearer {api_key}' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "inputs": {}, + "response_mode": "streaming", + "user": "abc-123" + }' + ``` + + + ```json {{ title: 'ファイル変数の例' }} + { + "inputs": { + "{variable_name}": + [ + { + "transfer_method": "local_file", + "upload_file_id": "{upload_file_id}", + "type": "{document_type}" + } + ] + } + } + ``` + + ### ブロッキングモード + + ```json {{ title: '応答' }} + { + "workflow_run_id": "djflajgkldjgd", + "task_id": "9da23599-e713-473b-982c-4328d4f5c78a", + "data": { + "id": "fdlsjfjejkghjda", + "workflow_id": "fldjaslkfjlsda", + "status": "succeeded", + "outputs": { + "text": "Nice to meet you." + }, + "error": null, + "elapsed_time": 0.875, + "total_tokens": 3562, + "total_steps": 8, + "created_at": 1705407629, + "finished_at": 1727807631 + } + } + ``` + + ### ストリーミングモード + + ```streaming {{ title: '応答' }} + data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "created_at": 1679586595}} + data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}} + data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}} + data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}} + data: {"event": "tts_message", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"} + data: {"event": "tts_message_end", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": ""} + ``` + + + + +--- + + + + 执行指定版本的工作流,通过路径参数指定工作流ID。 + + ### Path + - `workflow_id` (string) Required 工作流ID,用于指定特定版本的工作流 + + 获取方式:可以在版本历史中查询特定版本的工作流ID。 + + ### Request Body + - `inputs` (object) Required + 允许传入 App 定义的各变量值。 + inputs 参数包含了多组键值对(Key/Value pairs),每组的键对应一个特定变量,每组的值则是该变量的具体值。变量可以是文件列表类型。 + 文件列表类型变量适用于传入文件结合文本理解并回答问题,仅当模型支持该类型文件解析能力时可用。如果该变量是文件列表类型,该变量对应的值应是列表格式,其中每个元素应包含以下内容: + - `type` (string) 支持类型: + - `document` 具体类型包含:'TXT', 'MD', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB' + - `image` 具体类型包含:'JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG' + - `audio` 具体类型包含:'MP3', 'M4A', 'WAV', 'WEBM', 'AMR' + - `video` 具体类型包含:'MP4', 'MOV', 'MPEG', 'MPGA' + - `custom` 具体类型包含:其他文件类型 + - `transfer_method` (string) 传递方式,`remote_url` 图片地址 / `local_file` 上传文件 + - `url` (string) 图片地址(仅当传递方式为 `remote_url` 时) + - `upload_file_id` (string) 上传文件 ID(仅当传递方式为 `local_file` 时) + - `response_mode` (string) Required + 返回响应模式,支持: + - `streaming` 流式模式(推荐)。基于 SSE(**[Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)**)实现类似打字机输出方式的流式返回。 + - `blocking` 阻塞模式,等待执行完毕后返回结果。(请求若流程较长可能会被中断)。 + 由于 Cloudflare 限制,请求会在 100 秒超时无返回后中断。 + - `user` (string) Required + 用户标识,用于定义终端用户的身份,方便检索、统计。 + 由开发者定义规则,需保证用户标识在应用内唯一。API 无法访问 WebApp 创建的会话。 + - `files` (array[object]) 可选 + - `trace_id` (string) Optional + 链路追踪ID。适用于与业务系统已有的trace组件打通,实现端到端分布式追踪等场景。如果未指定,系统将自动生成 `trace_id`。支持以下三种方式传递,具体优先级依次为: + 1. Header:推荐通过 HTTP Header `X-Trace-Id` 传递,优先级最高。 + 2. Query 参数:通过 URL 查询参数 `trace_id` 传递。 + 3. Request Body:通过请求体字段 `trace_id` 传递(即本字段)。 + + ### Response + 当 `response_mode` 为 `blocking` 时,返回 CompletionResponse object。 + 当 `response_mode` 为 `streaming`时,返回 ChunkCompletionResponse object 流式序列。 + + ### CompletionResponse + 返回完整的 App 结果,`Content-Type` 为 `application/json` 。 + - `workflow_run_id` (string) workflow 执行 ID + - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 + - `data` (object) 详细内容 + - `id` (string) workflow 执行 ID + - `workflow_id` (string) 关联 Workflow ID + - `status` (string) 执行状态, `running` / `succeeded` / `failed` / `stopped` + - `outputs` (json) Optional 输出内容 + - `error` (string) Optional 错误原因 + - `elapsed_time` (float) Optional 耗时(s) + - `total_tokens` (int) Optional 总使用 tokens + - `total_steps` (int) 总步数(冗余),默认 0 + - `created_at` (timestamp) 开始时间 + - `finished_at` (timestamp) 结束时间 + + ### ChunkCompletionResponse + 返回 App 输出的流式块,`Content-Type` 为 `text/event-stream`。 + 每个流式块均为 data: 开头,块之间以 `\n\n` 即两个换行符分隔,如下所示: + + ```streaming {{ title: 'Response' }} + data: {"event": "text_chunk", "workflow_run_id": "b85e5fc5-751b-454d-b14e-dc5f240b0a31", "task_id": "bd029338-b068-4d34-a331-fc85478922c2", "data": {"text": "\u4e3a\u4e86", "from_variable_selector": ["1745912968134", "text"]}}\n\n + ``` + + 流式块中根据 `event` 不同,结构也不同,包含以下类型: + - `event: workflow_started` workflow 开始执行 + - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 + - `workflow_run_id` (string) workflow 执行 ID + - `event` (string) 固定为 `workflow_started` + - `data` (object) 详细内容 + - `id` (string) workflow 执行 ID + - `workflow_id` (string) 关联 Workflow ID + - `created_at` (timestamp) 开始时间 + - `event: node_started` node 开始执行 + - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 + - `workflow_run_id` (string) workflow 执行 ID + - `event` (string) 固定为 `node_started` + - `data` (object) 详细内容 + - `id` (string) workflow 执行 ID + - `node_id` (string) 节点 ID + - `node_type` (string) 节点类型 + - `title` (string) 节点名称 + - `index` (int) 执行序号,用于展示 Tracing Node 顺序 + - `predecessor_node_id` (string) 前置节点 ID,用于画布展示执行路径 + - `inputs` (object) 节点中所有使用到的前置节点变量内容 + - `created_at` (timestamp) 开始时间 + - `event: text_chunk` 文本片段 + - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 + - `workflow_run_id` (string) workflow 执行 ID + - `event` (string) 固定为 `text_chunk` + - `data` (object) 详细内容 + - `text` (string) 文本内容 + - `from_variable_selector` (array) 文本来源路径,帮助开发者了解文本是由哪个节点的哪个变量生成的 + - `event: node_finished` node 执行结束,成功失败同一事件中不同状态 + - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 + - `workflow_run_id` (string) workflow 执行 ID + - `event` (string) 固定为 `node_finished` + - `data` (object) 详细内容 + - `id` (string) node 执行 ID + - `node_id` (string) 节点 ID + - `index` (int) 执行序号,用于展示 Tracing Node 顺序 + - `predecessor_node_id` (string) optional 前置节点 ID,用于画布展示执行路径 + - `inputs` (object) 节点中所有使用到的前置节点变量内容 + - `process_data` (json) Optional 节点过程数据 + - `outputs` (json) Optional 输出内容 + - `status` (string) 执行状态 `running` / `succeeded` / `failed` / `stopped` + - `error` (string) Optional 错误原因 + - `elapsed_time` (float) Optional 耗时(s) + - `execution_metadata` (json) 元数据 + - `total_tokens` (int) optional 总使用 tokens + - `total_price` (decimal) optional 总费用 + - `currency` (string) optional 货币,如 `USD` / `RMB` + - `created_at` (timestamp) 开始时间 + - `event: workflow_finished` workflow 执行结束,成功失败同一事件中不同状态 + - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 + - `workflow_run_id` (string) workflow 执行 ID + - `event` (string) 固定为 `workflow_finished` + - `data` (object) 详细内容 + - `id` (string) workflow 执行 ID + - `workflow_id` (string) 关联 Workflow ID + - `status` (string) 执行状态 `running` / `succeeded` / `failed` / `stopped` + - `outputs` (json) Optional 输出内容 + - `error` (string) Optional 错误原因 + - `elapsed_time` (float) Optional 耗时(s) + - `total_tokens` (int) Optional 总使用 tokens + - `total_steps` (int) 总步数(冗余),默认 0 + - `created_at` (timestamp) 开始时间 + - `finished_at` (timestamp) 结束时间 + - `event: tts_message` TTS 音频流事件,即:语音合成输出。内容是Mp3格式的音频块,使用 base64 编码后的字符串,播放的时候直接解码即可。(开启自动播放才有此消息) + - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 + - `message_id` (string) 消息唯一 ID + - `audio` (string) 语音合成之后的音频块使用 Base64 编码之后的文本内容,播放的时候直接 base64 解码送入播放器即可 + - `created_at` (int) 创建时间戳,如:1705395332 + - `event: tts_message_end` TTS 音频流结束事件,收到这个事件表示音频流返回结束。 + - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 + - `message_id` (string) 消息唯一 ID + - `audio` (string) 结束事件是没有音频的,所以这里是空字符串 + - `created_at` (int) 创建时间戳,如:1705395332 + - `event: ping` 每 10s 一次的 ping 事件,保持连接存活。 + + ### Errors + - 400,`invalid_param`,传入参数异常 + - 400,`app_unavailable`,App 配置不可用 + - 400,`provider_not_initialize`,无可用模型凭据配置 + - 400,`provider_quota_exceeded`,模型调用额度不足 + - 400,`model_currently_not_support`,当前模型不可用 + - 400,`workflow_not_found`,指定的工作流版本未找到 + - 400,`draft_workflow_error`,无法使用草稿工作流版本 + - 400,`workflow_id_format_error`,工作流ID格式错误,需要UUID格式 + - 400,`workflow_request_error`,workflow 执行失败 + - 500,服务内部异常 + + + + + ```bash {{ title: 'cURL' }} + curl -X POST '${props.appDetail.api_base_url}/workflows/{workflow_id}/run' \ + --header 'Authorization: Bearer {api_key}' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "inputs": {}, + "response_mode": "streaming", + "user": "abc-123" + }' + ``` + + + ```json {{ title: 'File variable example' }} + { + "inputs": { + "{variable_name}": + [ + { + "transfer_method": "local_file", + "upload_file_id": "{upload_file_id}", + "type": "{document_type}" + } + ] + } + } + ``` + + ### Blocking Mode + + ```json {{ title: 'Response' }} + { + "workflow_run_id": "djflajgkldjgd", + "task_id": "9da23599-e713-473b-982c-4328d4f5c78a", + "data": { + "id": "fdlsjfjejkghjda", + "workflow_id": "fldjaslkfjlsda", + "status": "succeeded", + "outputs": { + "text": "Nice to meet you." + }, + "error": null, + "elapsed_time": 0.875, + "total_tokens": 3562, + "total_steps": 8, + "created_at": 1705407629, + "finished_at": 1727807631 + } + } + ``` + + ### Streaming Mode + + ```streaming {{ title: 'Response' }} + data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "created_at": 1679586595}} + data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}} + data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}} + data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}} + data: {"event": "tts_message", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"} + data: {"event": "tts_message_end", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": ""} + ``` + + + + +--- + 11 && 'mb-0', lightCard && 'bg-background-default-lighter opacity-75', @@ -46,7 +46,7 @@ const Empty = ({ }
- + diff --git a/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx b/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx index b43bfb1970..95676c656e 100644 --- a/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx @@ -6,7 +6,6 @@ import { useState, } from 'react' import { useTranslation } from 'react-i18next' -import { RiExternalLinkLine } from '@remixicon/react' import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security' import Modal from '@/app/components/base/modal/modal' import { CredentialTypeEnum } from '../types' @@ -24,7 +23,6 @@ import { useGetPluginCredentialSchemaHook, useUpdatePluginCredentialHook, } from '../hooks/use-credential' -import { useRenderI18nObject } from '@/hooks/use-i18n' export type ApiKeyModalProps = { pluginPayload: PluginPayload @@ -75,8 +73,6 @@ const ApiKeyModal = ({ acc[schema.name] = schema.default return acc }, {} as Record) - const helpField = formSchemas.find(schema => schema.url && schema.help) - const renderI18nObject = useRenderI18nObject() const { mutateAsync: addPluginCredential } = useAddPluginCredentialHook(pluginPayload) const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload) const formRef = useRef(null) @@ -136,18 +132,7 @@ const ApiKeyModal = ({ onClose={onClose} onCancel={onClose} footerSlot={ - helpField && ( - - - {renderI18nObject(helpField?.help as any)} - - - - ) + (
) } bottomSlot={
diff --git a/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx b/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx index 14c7ed957f..c10b06166b 100644 --- a/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx @@ -4,7 +4,6 @@ import { useRef, useState, } from 'react' -import { RiExternalLinkLine } from '@remixicon/react' import { useForm, useStore, @@ -24,7 +23,6 @@ import type { } from '@/app/components/base/form/types' import { useToastContext } from '@/app/components/base/toast' import Button from '@/app/components/base/button' -import { useRenderI18nObject } from '@/hooks/use-i18n' type OAuthClientSettingsProps = { pluginPayload: PluginPayload @@ -129,8 +127,6 @@ const OAuthClientSettings = ({ defaultValues: editValues || defaultValues, }) const __oauth_client__ = useStore(form.store, s => s.values.__oauth_client__) - const helpField = schemas.find(schema => schema.url && schema.help) - const renderI18nObject = useRenderI18nObject() return ( - { - helpField && __oauth_client__ === 'custom' && ( - - - {renderI18nObject(helpField?.help as any)} - - - - )} ) diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx index 130773e0c2..a715237a43 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx @@ -47,7 +47,22 @@ const EndpointModal: FC = ({ return } } - onSaved(tempCredential) + + // Fix: Process boolean fields to ensure they are sent as proper boolean values + const processedCredential = { ...tempCredential } + formSchemas.forEach((field) => { + if (field.type === 'boolean' && processedCredential[field.name] !== undefined) { + const value = processedCredential[field.name] + if (typeof value === 'string') + processedCredential[field.name] = value === 'true' || value === '1' || value === 'True' + else if (typeof value === 'number') + processedCredential[field.name] = value === 1 + else if (typeof value === 'boolean') + processedCredential[field.name] = value + } + }) + + onSaved(processedCredential) } return ( diff --git a/web/app/components/tools/add-tool-modal/empty.tsx b/web/app/components/tools/add-tool-modal/empty.tsx index 7390249dc1..5759589c8e 100644 --- a/web/app/components/tools/add-tool-modal/empty.tsx +++ b/web/app/components/tools/add-tool-modal/empty.tsx @@ -5,6 +5,7 @@ import { RiArrowRightUpLine } from '@remixicon/react' import Link from 'next/link' import cn from '@/utils/classnames' import { NoToolPlaceholder } from '../../base/icons/src/vender/other' +import useTheme from '@/hooks/use-theme' type Props = { type?: ToolTypeEnum isAgent?: boolean @@ -25,6 +26,7 @@ const Empty = ({ isAgent, }: Props) => { const { t } = useTranslation() + const { theme } = useTheme() const hasLink = type && [ToolTypeEnum.Custom, ToolTypeEnum.MCP].includes(type) const Comp = (hasLink ? Link : 'div') as any @@ -34,7 +36,7 @@ const Empty = ({ return (
- +
{hasTitle ? t(`tools.addToolModal.${renderType}.title`) : 'No tools available'}
diff --git a/web/app/components/tools/utils/to-form-schema.ts b/web/app/components/tools/utils/to-form-schema.ts index ee7f3379ad..ae43e6f157 100644 --- a/web/app/components/tools/utils/to-form-schema.ts +++ b/web/app/components/tools/utils/to-form-schema.ts @@ -63,6 +63,16 @@ export const addDefaultValue = (value: Record, formSchemas: { varia const itemValue = value[formSchema.variable] if ((formSchema.default !== undefined) && (value === undefined || itemValue === null || itemValue === '' || itemValue === undefined)) newValues[formSchema.variable] = formSchema.default + + // Fix: Convert boolean field values to proper boolean type + if (formSchema.type === 'boolean' && itemValue !== undefined && itemValue !== null && itemValue !== '') { + if (typeof itemValue === 'string') + newValues[formSchema.variable] = itemValue === 'true' || itemValue === '1' || itemValue === 'True' + else if (typeof itemValue === 'number') + newValues[formSchema.variable] = itemValue === 1 + else if (typeof itemValue === 'boolean') + newValues[formSchema.variable] = itemValue + } }) return newValues } diff --git a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx index e84d4a07c4..968ee395ff 100644 --- a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx +++ b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx @@ -27,9 +27,12 @@ import { useInvalidateAppWorkflow, usePublishWorkflow, useResetWorkflowVersionHi import type { PublishWorkflowParams } from '@/types/workflow' import { fetchAppDetail } from '@/service/apps' import { useStore as useAppStore } from '@/app/components/app/store' +import useTheme from '@/hooks/use-theme' +import cn from '@/utils/classnames' const FeaturesTrigger = () => { const { t } = useTranslation() + const { theme } = useTheme() const workflowStore = useWorkflowStore() const appDetail = useAppStore(s => s.appDetail) const appID = appDetail?.id @@ -122,7 +125,13 @@ const FeaturesTrigger = () => { return ( <> - diff --git a/web/app/components/workflow/block-selector/tool/action-item.tsx b/web/app/components/workflow/block-selector/tool/action-item.tsx index e5e33614b0..4a2ed4a87b 100644 --- a/web/app/components/workflow/block-selector/tool/action-item.tsx +++ b/web/app/components/workflow/block-selector/tool/action-item.tsx @@ -34,6 +34,7 @@ const ToolItem: FC = ({ diff --git a/web/app/components/workflow/header/chat-variable-button.tsx b/web/app/components/workflow/header/chat-variable-button.tsx index b816e44d26..36c4a640c4 100644 --- a/web/app/components/workflow/header/chat-variable-button.tsx +++ b/web/app/components/workflow/header/chat-variable-button.tsx @@ -2,8 +2,11 @@ import { memo } from 'react' import Button from '@/app/components/base/button' import { BubbleX } from '@/app/components/base/icons/src/vender/line/others' import { useStore } from '@/app/components/workflow/store' +import useTheme from '@/hooks/use-theme' +import cn from '@/utils/classnames' const ChatVariableButton = ({ disabled }: { disabled: boolean }) => { + const { theme } = useTheme() const setShowChatVariablePanel = useStore(s => s.setShowChatVariablePanel) const setShowEnvPanel = useStore(s => s.setShowEnvPanel) const setShowDebugAndPreviewPanel = useStore(s => s.setShowDebugAndPreviewPanel) @@ -15,7 +18,14 @@ const ChatVariableButton = ({ disabled }: { disabled: boolean }) => { } return ( - ) diff --git a/web/app/components/workflow/header/env-button.tsx b/web/app/components/workflow/header/env-button.tsx index e3196ee66d..48bb1da4a4 100644 --- a/web/app/components/workflow/header/env-button.tsx +++ b/web/app/components/workflow/header/env-button.tsx @@ -2,8 +2,11 @@ import { memo } from 'react' import Button from '@/app/components/base/button' import { Env } from '@/app/components/base/icons/src/vender/line/others' import { useStore } from '@/app/components/workflow/store' +import useTheme from '@/hooks/use-theme' +import cn from '@/utils/classnames' const EnvButton = ({ disabled }: { disabled: boolean }) => { + const { theme } = useTheme() const setShowChatVariablePanel = useStore(s => s.setShowChatVariablePanel) const setShowEnvPanel = useStore(s => s.setShowEnvPanel) const setShowDebugAndPreviewPanel = useStore(s => s.setShowDebugAndPreviewPanel) @@ -15,7 +18,14 @@ const EnvButton = ({ disabled }: { disabled: boolean }) => { } return ( - ) diff --git a/web/app/components/workflow/header/header-in-restoring.tsx b/web/app/components/workflow/header/header-in-restoring.tsx index edf9c603ce..cce2b9131f 100644 --- a/web/app/components/workflow/header/header-in-restoring.tsx +++ b/web/app/components/workflow/header/header-in-restoring.tsx @@ -19,6 +19,8 @@ import RestoringTitle from './restoring-title' import Button from '@/app/components/base/button' import { useInvalidAllLastRun } from '@/service/use-workflow' import { useHooksStore } from '../hooks-store' +import useTheme from '@/hooks/use-theme' +import cn from '@/utils/classnames' export type HeaderInRestoringProps = { onRestoreSettled?: () => void @@ -27,6 +29,7 @@ const HeaderInRestoring = ({ onRestoreSettled, }: HeaderInRestoringProps) => { const { t } = useTranslation() + const { theme } = useTheme() const workflowStore = useWorkflowStore() const configsMap = useHooksStore(s => s.configsMap) const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId) @@ -77,21 +80,27 @@ const HeaderInRestoring = ({
-
- - +
diff --git a/web/app/components/workflow/header/version-history-button.tsx b/web/app/components/workflow/header/version-history-button.tsx index 9b608bd248..a70ca8f769 100644 --- a/web/app/components/workflow/header/version-history-button.tsx +++ b/web/app/components/workflow/header/version-history-button.tsx @@ -5,6 +5,8 @@ import { useKeyPress } from 'ahooks' import Button from '../../base/button' import Tooltip from '../../base/tooltip' import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '../utils' +import useTheme from '@/hooks/use-theme' +import cn from '@/utils/classnames' type VersionHistoryButtonProps = { onClick: () => Promise | unknown @@ -38,6 +40,7 @@ PopupContent.displayName = 'PopupContent' const VersionHistoryButton: FC = ({ onClick, }) => { + const { theme } = useTheme() const handleViewVersionHistory = useCallback(async () => { await onClick?.() }, [onClick]) @@ -53,12 +56,15 @@ const VersionHistoryButton: FC = ({ popupClassName='rounded-lg border-[0.5px] border-components-panel-border bg-components-tooltip-bg shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px] p-1.5' > - +
} diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx index c872952900..9e67debd58 100644 --- a/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx @@ -143,7 +143,6 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => category: PluginType.agent, }) } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [query]) const pluginRef = useRef(null) diff --git a/web/app/components/workflow/nodes/_base/components/readonly-input-with-select-var.tsx b/web/app/components/workflow/nodes/_base/components/readonly-input-with-select-var.tsx index 4a4ca454d3..c1927011dc 100644 --- a/web/app/components/workflow/nodes/_base/components/readonly-input-with-select-var.tsx +++ b/web/app/components/workflow/nodes/_base/components/readonly-input-with-select-var.tsx @@ -4,12 +4,10 @@ import React from 'react' import cn from 'classnames' import { useWorkflow } from '../../../hooks' import { BlockEnum } from '../../../types' -import { VarBlockIcon } from '../../../block-icon' -import { getNodeInfoById, isConversationVar, isENV, isSystemVar } from './variable/utils' -import { Line3 } from '@/app/components/base/icons/src/public/common' -import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' -import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others' -import { RiMoreLine } from '@remixicon/react' +import { getNodeInfoById, isSystemVar } from './variable/utils' +import { + VariableLabelInText, +} from '@/app/components/workflow/nodes/_base/components/variable/variable-label' type Props = { nodeId: string value: string @@ -42,40 +40,17 @@ const ReadonlyInputWithSelectVar: FC = ({ const value = vars[index].split('.') const isSystem = isSystemVar(value) - const isEnv = isENV(value) - const isChatVar = isConversationVar(value) const node = (isSystem ? startNode : getNodeInfoById(availableNodes, value[0]))?.data - const varName = `${isSystem ? 'sys.' : ''}${value[value.length - 1]}` const isShowAPart = value.length > 2 return ( {str} -
- {!isEnv && !isChatVar && ( -
-
- -
-
{node?.title}
- -
- )} - {isShowAPart && ( -
- - -
- )} -
- {!isEnv && !isChatVar && } - {isEnv && } - {isChatVar && } -
{varName}
-
-
+
) }) return html diff --git a/web/app/components/workflow/nodes/_base/components/variable-tag.tsx b/web/app/components/workflow/nodes/_base/components/variable-tag.tsx index 0815f85134..4896d485d7 100644 --- a/web/app/components/workflow/nodes/_base/components/variable-tag.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable-tag.tsx @@ -1,9 +1,6 @@ import { useCallback, useMemo } from 'react' import { useNodes, useReactFlow, useStoreApi } from 'reactflow' -import { capitalize } from 'lodash-es' import { useTranslation } from 'react-i18next' -import { RiErrorWarningFill } from '@remixicon/react' -import { VarBlockIcon } from '@/app/components/workflow/block-icon' import type { CommonNodeType, Node, @@ -11,13 +8,11 @@ import type { VarType, } from '@/app/components/workflow/types' import { BlockEnum } from '@/app/components/workflow/types' -import { Line3 } from '@/app/components/base/icons/src/public/common' -import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' -import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others' import { getNodeInfoById, isConversationVar, isENV, isRagVariableVar, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' -import Tooltip from '@/app/components/base/tooltip' -import cn from '@/utils/classnames' import { isExceptionVariable } from '@/app/components/workflow/utils' +import { + VariableLabelInSelect, +} from '@/app/components/workflow/nodes/_base/components/variable/variable-label' type VariableTagProps = { valueSelector: ValueSelector @@ -74,51 +69,20 @@ const VariableTag = ({ const { t } = useTranslation() return ( - -
{ - if (e.metaKey || e.ctrlKey) { - e.stopPropagation() - handleVariableJump() - } - }} - > - {(!isEnv && !isChatVar && <> - {node && ( - <> - -
- {node?.data.title} -
- - )} - - - )} - {isEnv && } - {isChatVar && } -
- {variableName} -
- { - !isShort && varType && ( -
{capitalize(varType)}
- ) + { + if (e.metaKey || e.ctrlKey) { + e.stopPropagation() + handleVariableJump() } - {!isValid && } -
-
+ }} + errorMsg={!isValid ? t('workflow.errorMsg.invalidVariable') : undefined} + isExceptionVariable={isException} + /> ) } diff --git a/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx b/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx index a7c9a9d172..7365e91c21 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import React, { useCallback } from 'react' +import React, { useCallback, useState } from 'react' import produce from 'immer' import { useTranslation } from 'react-i18next' import type { OutputVar } from '../../../code/types' @@ -8,8 +8,10 @@ import RemoveButton from '../remove-button' import VarTypePicker from './var-type-picker' import Input from '@/app/components/base/input' import type { VarType } from '@/app/components/workflow/types' -import { checkKeys, replaceSpaceWithUnderscreInVarNameInput } from '@/utils/var' +import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var' +import type { ToastHandle } from '@/app/components/base/toast' import Toast from '@/app/components/base/toast' +import { useDebounceFn } from 'ahooks' type Props = { readonly: boolean @@ -27,6 +29,7 @@ const OutputVarList: FC = ({ onRemove, }) => { const { t } = useTranslation() + const [toastHandler, setToastHandler] = useState() const list = outputKeyOrders.map((key) => { return { @@ -34,29 +37,36 @@ const OutputVarList: FC = ({ variable_type: outputs[key]?.type, } }) + + const { run: validateVarInput } = useDebounceFn((existingVariables: typeof list, newKey: string) => { + const { isValid, errorKey, errorMessageKey } = checkKeys([newKey], true) + if (!isValid) { + setToastHandler(Toast.notify({ + type: 'error', + message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }), + })) + return + } + if (existingVariables.some(key => key.variable?.trim() === newKey.trim())) { + setToastHandler(Toast.notify({ + type: 'error', + message: t('appDebug.varKeyError.keyAlreadyExists', { key: newKey }), + })) + } + else { + toastHandler?.clear?.() + } + }, { wait: 500 }) + const handleVarNameChange = useCallback((index: number) => { return (e: React.ChangeEvent) => { const oldKey = list[index].variable - replaceSpaceWithUnderscreInVarNameInput(e.target) + replaceSpaceWithUnderscoreInVarNameInput(e.target) const newKey = e.target.value - const { isValid, errorKey, errorMessageKey } = checkKeys([newKey], true) - if (!isValid) { - Toast.notify({ - type: 'error', - message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }), - }) - return - } - - if (list.map(item => item.variable?.trim()).includes(newKey.trim())) { - Toast.notify({ - type: 'error', - message: t('appDebug.varKeyError.keyAlreadyExists', { key: newKey }), - }) - return - } + toastHandler?.clear?.() + validateVarInput(list.toSpliced(index, 1), newKey) const newOutputs = produce(outputs, (draft) => { draft[newKey] = draft[oldKey] @@ -64,8 +74,7 @@ const OutputVarList: FC = ({ }) onChange(newOutputs, index, newKey) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [list, onChange, outputs, outputKeyOrders]) + }, [list, onChange, outputs, outputKeyOrders, validateVarInput]) const handleVarTypeChange = useCallback((index: number) => { return (value: string) => { @@ -75,7 +84,6 @@ const OutputVarList: FC = ({ }) onChange(newOutputs) } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [list, onChange, outputs, outputKeyOrders]) const handleVarRemove = useCallback((index: number) => { diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx index b1a8d52a05..e9e45d3aad 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import React, { useCallback, useMemo } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import produce from 'immer' import RemoveButton from '../remove-button' @@ -8,12 +8,14 @@ import VarReferencePicker from './var-reference-picker' import Input from '@/app/components/base/input' import type { ValueSelector, Var, Variable } from '@/app/components/workflow/types' import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' -import { checkKeys, replaceSpaceWithUnderscreInVarNameInput } from '@/utils/var' +import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var' +import type { ToastHandle } from '@/app/components/base/toast' import Toast from '@/app/components/base/toast' import { ReactSortable } from 'react-sortablejs' import { v4 as uuid4 } from 'uuid' import { RiDraggable } from '@remixicon/react' import cn from '@/utils/classnames' +import { useDebounceFn } from 'ahooks' type Props = { nodeId: string @@ -39,6 +41,7 @@ const VarList: FC = ({ isSupportFileVar = true, }) => { const { t } = useTranslation() + const [toastHandle, setToastHandle] = useState() const listWithIds = useMemo(() => list.map((item) => { const id = uuid4() @@ -48,27 +51,35 @@ const VarList: FC = ({ } }), [list]) - const handleVarNameChange = useCallback((index: number) => { - return (e: React.ChangeEvent) => { - replaceSpaceWithUnderscreInVarNameInput(e.target) - - const newKey = e.target.value - const { isValid, errorKey, errorMessageKey } = checkKeys([newKey], true) - if (!isValid) { - Toast.notify({ + const { run: validateVarInput } = useDebounceFn((list: Variable[], newKey: string) => { + const { isValid, errorKey, errorMessageKey } = checkKeys([newKey], true) + if (!isValid) { + setToastHandle(Toast.notify({ type: 'error', message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }), - }) + })) return } + if (list.some(item => item.variable?.trim() === newKey.trim())) { + console.log('new key', newKey.trim()) + setToastHandle(Toast.notify({ + type: 'error', + message: t('appDebug.varKeyError.keyAlreadyExists', { key: newKey }), + })) + } + else { + toastHandle?.clear?.() + } + }, { wait: 500 }) - if (list.map(item => item.variable?.trim()).includes(newKey.trim())) { - Toast.notify({ - type: 'error', - message: t('appDebug.varKeyError.keyAlreadyExists', { key: newKey }), - }) - return - } + const handleVarNameChange = useCallback((index: number) => { + return (e: React.ChangeEvent) => { + replaceSpaceWithUnderscoreInVarNameInput(e.target) + + const newKey = e.target.value + + toastHandle?.clear?.() + validateVarInput(list.toSpliced(index, 1), newKey) onVarNameChange?.(list[index].variable, newKey) const newList = produce(list, (draft) => { @@ -76,7 +87,7 @@ const VarList: FC = ({ }) onChange(newList) } - }, [list, onVarNameChange, onChange]) + }, [list, onVarNameChange, onChange, validateVarInput]) const handleVarReferenceChange = useCallback((index: number) => { return (value: ValueSelector | string, varKindType: VarKindType, varInfo?: Var) => { diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx index a2e2f80253..5210ff6a71 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx @@ -27,7 +27,6 @@ import { type CredentialFormSchema, type FormOption, FormTypeEnum } from '@/app/ import { BlockEnum } from '@/app/components/workflow/types' import { VarBlockIcon } from '@/app/components/workflow/block-icon' import { Line3 } from '@/app/components/base/icons/src/public/common' -import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others' import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' import { PortalToFollowElem, @@ -46,10 +45,10 @@ import Tooltip from '@/app/components/base/tooltip' import { isExceptionVariable } from '@/app/components/workflow/utils' import VarFullPathPanel from './var-full-path-panel' import { noop } from 'lodash-es' -import { InputField } from '@/app/components/base/icons/src/vender/pipeline' import { useStore as useWorkflowStore } from '@/app/components/workflow/store' import { useFetchDynamicOptions } from '@/service/use-plugins' import type { Tool } from '@/app/components/tools/types' +import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label' const TRIGGER_DEFAULT_WIDTH = 227 @@ -142,7 +141,6 @@ const VarReferencePicker: FC = ({ useEffect(() => { if (triggerRef.current) setTriggerWidth(triggerRef.current.clientWidth) - // eslint-disable-next-line react-hooks/exhaustive-deps }, [triggerRef.current]) const [varKindType, setVarKindType] = useState(defaultVarKindType) @@ -156,7 +154,6 @@ const VarReferencePicker: FC = ({ const [open, setOpen] = useState(false) useEffect(() => { onOpen() - // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]) const hasValue = !isConstant && value.length > 0 @@ -368,8 +365,33 @@ const VarReferencePicker: FC = ({ options: dynamicOptions, } } - return schema - }, [dynamicOptions]) + + // If we don't have dynamic options but we have a selected value, create a temporary option to preserve the selection during loading + if (isLoading && value && typeof value === 'string') { + const preservedOptions = [{ + value, + label: { en_US: value, zh_Hans: value }, + show_on: [], + }] + return { + ...schema, + options: preservedOptions, + } + } + + // Default case: return schema with empty options + return { + ...schema, + options: [], + } + }, [schema, dynamicOptions, isLoading, value]) + + const variableCategory = useMemo(() => { + if (isEnv) return 'environment' + if (isChatVar) return 'conversation' + if (isLoopVar) return 'loop' + return 'system' + }, [isEnv, isChatVar, isLoopVar]) return (
@@ -467,11 +489,11 @@ const VarReferencePicker: FC = ({
)}
- {!hasValue && } {isLoading && } - {isEnv && } - {isChatVar && } - {isRagVar && } +
{varName}
diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx index 152e6b7af1..4008bdb2e6 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx @@ -5,7 +5,6 @@ import { useHover } from 'ahooks' import { useTranslation } from 'react-i18next' import cn from '@/utils/classnames' import { type NodeOutPutVar, type ValueSelector, type Var, VarType } from '@/app/components/workflow/types' -import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows' import { PortalToFollowElem, @@ -13,7 +12,6 @@ import { PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' import Input from '@/app/components/base/input' -import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others' import { checkKeys } from '@/utils/var' import type { StructuredOutput } from '../../../llm/types' import { Type } from '../../../llm/types' @@ -21,10 +19,9 @@ import PickerStructurePanel from '@/app/components/workflow/nodes/_base/componen import { isSpecialVar, varTypeToStructType } from './utils' import type { Field } from '@/app/components/workflow/nodes/llm/types' import { FILE_STRUCT } from '@/app/components/workflow/constants' -import { Loop } from '@/app/components/base/icons/src/vender/workflow' import { noop } from 'lodash-es' -import { InputField } from '@/app/components/base/icons/src/vender/pipeline' import ManageInputField from './manage-input-field' +import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label' type ObjectChildrenProps = { nodeId: string @@ -121,7 +118,6 @@ const Item: FC = ({ const open = (isObj || isStructureOutput) && isHovering useEffect(() => { onHovering && onHovering(isHovering) - // eslint-disable-next-line react-hooks/exhaustive-deps }, [isHovering]) const handleChosen = (e: React.MouseEvent) => { e.stopPropagation() @@ -135,6 +131,12 @@ const Item: FC = ({ onChange([nodeId, ...objPath, itemData.variable], itemData) } } + const variableCategory = useMemo(() => { + if (isEnv) return 'environment' + if (isChatVar) return 'conversation' + if (isLoopVar) return 'loop' + return 'system' + }, [isEnv, isChatVar, isSys, isLoopVar]) return ( = ({ onMouseDown={e => e.preventDefault()} >
- {!isEnv && !isChatVar && !isLoopVar && !isRagVariable && } - {isEnv && } - {isChatVar && } - {isLoopVar && } - {isRagVariable && } - {!isEnv && !isChatVar && !isRagVariable && ( + + {!isEnv && !isChatVar && (
{itemData.variable}
)} {isEnv && ( @@ -226,11 +227,9 @@ const ObjectChildren: FC = ({ const isHovering = isItemHovering || isChildrenHovering useEffect(() => { onHovering && onHovering(isHovering) - // eslint-disable-next-line react-hooks/exhaustive-deps }, [isHovering]) useEffect(() => { onHovering && onHovering(isItemHovering) - // eslint-disable-next-line react-hooks/exhaustive-deps }, [isItemHovering]) // absolute top-[-2px] return ( diff --git a/web/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-icon.tsx b/web/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-icon.tsx new file mode 100644 index 0000000000..93f47f794a --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-icon.tsx @@ -0,0 +1,28 @@ +import { memo } from 'react' +import cn from '@/utils/classnames' +import { useVarIcon } from '../hooks' +import type { VarInInspectType } from '@/types/workflow' + +export type VariableIconProps = { + className?: string + variables?: string[] + variableCategory?: VarInInspectType | string +} +const VariableIcon = ({ + className, + variables = [], + variableCategory, +}: VariableIconProps) => { + const VarIcon = useVarIcon(variables, variableCategory) + + return VarIcon && ( + + ) +} + +export default memo(VariableIcon) diff --git a/web/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-label.tsx b/web/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-label.tsx new file mode 100644 index 0000000000..99f080f545 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-label.tsx @@ -0,0 +1,83 @@ +import { memo } from 'react' +import { capitalize } from 'lodash-es' +import { + RiErrorWarningFill, + RiMoreLine, +} from '@remixicon/react' +import type { VariablePayload } from '../types' +import { useVarColor } from '../hooks' +import VariableNodeLabel from './variable-node-label' +import VariableIcon from './variable-icon' +import VariableName from './variable-name' +import cn from '@/utils/classnames' +import Tooltip from '@/app/components/base/tooltip' + +const VariableLabel = ({ + nodeType, + nodeTitle, + variables, + variableType, + className, + errorMsg, + onClick, + isExceptionVariable, + ref, + notShowFullPath, + rightSlot, +}: VariablePayload) => { + const varColorClassName = useVarColor(variables, isExceptionVariable) + return ( +
+ + { + notShowFullPath && ( + <> + +
/
+ + ) + } + + + { + variableType && ( +
+ {capitalize(variableType)} +
+ ) + } + { + !!errorMsg && ( + + + + ) + } + { + rightSlot + } +
+ ) +} + +export default memo(VariableLabel) diff --git a/web/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-name.tsx b/web/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-name.tsx new file mode 100644 index 0000000000..f656b780a5 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-name.tsx @@ -0,0 +1,30 @@ +import { memo } from 'react' +import { useVarName } from '../hooks' +import cn from '@/utils/classnames' + +type VariableNameProps = { + variables: string[] + className?: string + notShowFullPath?: boolean +} +const VariableName = ({ + variables, + className, + notShowFullPath, +}: VariableNameProps) => { + const varName = useVarName(variables, notShowFullPath) + + return ( +
+ {varName} +
+ ) +} + +export default memo(VariableName) diff --git a/web/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-node-label.tsx b/web/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-node-label.tsx new file mode 100644 index 0000000000..35b539d97a --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-node-label.tsx @@ -0,0 +1,37 @@ +import { memo } from 'react' +import { VarBlockIcon } from '@/app/components/workflow/block-icon' +import type { BlockEnum } from '@/app/components/workflow/types' + +type VariableNodeLabelProps = { + nodeType?: BlockEnum + nodeTitle?: string +} +const VariableNodeLabel = ({ + nodeType, + nodeTitle, +}: VariableNodeLabelProps) => { + if (!nodeType) + return null + + return ( + <> + + { + nodeTitle && ( +
+ {nodeTitle} +
+ ) + } +
/
+ + ) +} + +export default memo(VariableNodeLabel) diff --git a/web/app/components/workflow/nodes/_base/components/variable/variable-label/hooks.ts b/web/app/components/workflow/nodes/_base/components/variable/variable-label/hooks.ts new file mode 100644 index 0000000000..14ca87903b --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/variable/variable-label/hooks.ts @@ -0,0 +1,89 @@ +import { useMemo } from 'react' +import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' +import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others' +import { Loop } from '@/app/components/base/icons/src/vender/workflow' +import { + isConversationVar, + isENV, + isSystemVar, +} from '../utils' +import { VarInInspectType } from '@/types/workflow' + +export const useVarIcon = (variables: string[], variableCategory?: VarInInspectType | string) => { + if (variableCategory === 'loop') + return Loop + + if (isENV(variables) || variableCategory === VarInInspectType.environment || variableCategory === 'environment') + return Env + + if (isConversationVar(variables) || variableCategory === VarInInspectType.conversation || variableCategory === 'conversation') + return BubbleX + + return Variable02 +} + +export const useVarColor = (variables: string[], isExceptionVariable?: boolean, variableCategory?: VarInInspectType | string) => { + return useMemo(() => { + if (isExceptionVariable) + return 'text-text-warning' + + if (variableCategory === 'loop') + return 'text-util-colors-cyan-cyan-500' + + if (isENV(variables) || variableCategory === VarInInspectType.environment || variableCategory === 'environment') + return 'text-util-colors-violet-violet-600' + + if (isConversationVar(variables) || variableCategory === VarInInspectType.conversation || variableCategory === 'conversation') + return 'text-util-colors-teal-teal-700' + + return 'text-text-accent' + }, [variables, isExceptionVariable]) +} + +export const useVarName = (variables: string[], notShowFullPath?: boolean) => { + const variableFullPathName = variables.slice(1).join('.') + const variablesLength = variables.length + const varName = useMemo(() => { + const isSystem = isSystemVar(variables) + const varName = notShowFullPath ? variables[variablesLength - 1] : variableFullPathName + return `${isSystem ? 'sys.' : ''}${varName}` + }, [variables, notShowFullPath]) + + return varName +} + +export const useVarBgColorInEditor = (variables: string[], hasError?: boolean) => { + if (hasError) { + return { + hoverBorderColor: 'hover:border-state-destructive-active', + hoverBgColor: 'hover:bg-state-destructive-hover', + selectedBorderColor: '!border-state-destructive-solid', + selectedBgColor: '!bg-state-destructive-hover', + } + } + + if (isENV(variables)) { + return { + hoverBorderColor: 'hover:border-util-colors-violet-violet-100', + hoverBgColor: 'hover:bg-util-colors-violet-violet-50', + selectedBorderColor: 'border-util-colors-violet-violet-600', + selectedBgColor: 'bg-util-colors-violet-violet-50', + } + } + + if (isConversationVar(variables)) { + return { + hoverBorderColor: 'hover:border-util-colors-teal-teal-100', + hoverBgColor: 'hover:bg-util-colors-teal-teal-50', + selectedBorderColor: 'border-util-colors-teal-teal-600', + selectedBgColor: 'bg-util-colors-teal-teal-50', + } + } + + return { + hoverBorderColor: 'hover:border-state-accent-alt', + hoverBgColor: 'hover:bg-state-accent-hover', + selectedBorderColor: 'border-state-accent-solid', + selectedBgColor: 'bg-state-accent-hover', + } +} diff --git a/web/app/components/workflow/nodes/_base/components/variable/variable-label/index.tsx b/web/app/components/workflow/nodes/_base/components/variable/variable-label/index.tsx new file mode 100644 index 0000000000..012522e0aa --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/variable/variable-label/index.tsx @@ -0,0 +1,5 @@ +export { default as VariableLabelInSelect } from './variable-label-in-select' +export { default as VariableLabelInEditor } from './variable-label-in-editor' +export { default as VariableLabelInNode } from './variable-label-in-node' +export { default as VariableLabelInText } from './variable-label-in-text' +export { default as VariableIconWithColor } from './variable-icon-with-color' diff --git a/web/app/components/workflow/nodes/_base/components/variable/variable-label/types.ts b/web/app/components/workflow/nodes/_base/components/variable/variable-label/types.ts new file mode 100644 index 0000000000..6f3b06f6ee --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/variable/variable-label/types.ts @@ -0,0 +1,19 @@ +import type { ReactNode } from 'react' +import type { + BlockEnum, + VarType, +} from '@/app/components/workflow/types' + +export type VariablePayload = { + className?: string + nodeType?: BlockEnum + nodeTitle?: string + variables: string[] + variableType?: VarType + onClick?: (e: React.MouseEvent) => void + errorMsg?: string + isExceptionVariable?: boolean + ref?: React.Ref + notShowFullPath?: boolean + rightSlot?: ReactNode +} diff --git a/web/app/components/workflow/nodes/_base/components/variable/variable-label/variable-icon-with-color.tsx b/web/app/components/workflow/nodes/_base/components/variable/variable-label/variable-icon-with-color.tsx new file mode 100644 index 0000000000..56d6c3738e --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/variable/variable-label/variable-icon-with-color.tsx @@ -0,0 +1,30 @@ +import { memo } from 'react' +import VariableIcon from './base/variable-icon' +import type { VariableIconProps } from './base/variable-icon' +import { useVarColor } from './hooks' +import cn from '@/utils/classnames' + +type VariableIconWithColorProps = { + isExceptionVariable?: boolean +} & VariableIconProps + +const VariableIconWithColor = ({ + isExceptionVariable, + variableCategory, + variables = [], + className, +}: VariableIconWithColorProps) => { + const varColorClassName = useVarColor(variables, isExceptionVariable, variableCategory) + return ( + + ) +} + +export default memo(VariableIconWithColor) diff --git a/web/app/components/workflow/nodes/_base/components/variable/variable-label/variable-label-in-editor.tsx b/web/app/components/workflow/nodes/_base/components/variable/variable-label/variable-label-in-editor.tsx new file mode 100644 index 0000000000..fa5ae57f91 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/variable/variable-label/variable-label-in-editor.tsx @@ -0,0 +1,40 @@ +import { memo } from 'react' +import type { VariablePayload } from './types' +import VariableLabel from './base/variable-label' +import { useVarBgColorInEditor } from './hooks' +import cn from '@/utils/classnames' + +type VariableLabelInEditorProps = { + isSelected?: boolean +} & VariablePayload +const VariableLabelInEditor = ({ + isSelected, + variables, + errorMsg, + ...rest +}: VariableLabelInEditorProps) => { + const { + hoverBorderColor, + hoverBgColor, + selectedBorderColor, + selectedBgColor, + } = useVarBgColorInEditor(variables, !!errorMsg) + + return ( + + ) +} + +export default memo(VariableLabelInEditor) diff --git a/web/app/components/workflow/nodes/_base/components/variable/variable-label/variable-label-in-node.tsx b/web/app/components/workflow/nodes/_base/components/variable/variable-label/variable-label-in-node.tsx new file mode 100644 index 0000000000..cebe140e26 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/variable/variable-label/variable-label-in-node.tsx @@ -0,0 +1,17 @@ +import { memo } from 'react' +import type { VariablePayload } from './types' +import VariableLabel from './base/variable-label' +import cn from '@/utils/classnames' + +const VariableLabelInNode = (variablePayload: VariablePayload) => { + return ( + + ) +} + +export default memo(VariableLabelInNode) diff --git a/web/app/components/workflow/nodes/_base/components/variable/variable-label/variable-label-in-select.tsx b/web/app/components/workflow/nodes/_base/components/variable/variable-label/variable-label-in-select.tsx new file mode 100644 index 0000000000..34e7b5f461 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/variable/variable-label/variable-label-in-select.tsx @@ -0,0 +1,13 @@ +import { memo } from 'react' +import type { VariablePayload } from './types' +import VariableLabel from './base/variable-label' + +const VariableLabelInSelect = (variablePayload: VariablePayload) => { + return ( + + ) +} + +export default memo(VariableLabelInSelect) diff --git a/web/app/components/workflow/nodes/_base/components/variable/variable-label/variable-label-in-text.tsx b/web/app/components/workflow/nodes/_base/components/variable/variable-label/variable-label-in-text.tsx new file mode 100644 index 0000000000..dd0d6fcf8b --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/variable/variable-label/variable-label-in-text.tsx @@ -0,0 +1,17 @@ +import { memo } from 'react' +import type { VariablePayload } from './types' +import VariableLabel from './base/variable-label' +import cn from '@/utils/classnames' + +const VariableLabelInText = (variablePayload: VariablePayload) => { + return ( + + ) +} + +export default memo(VariableLabelInText) diff --git a/web/app/components/workflow/nodes/agent/components/tool-icon.tsx b/web/app/components/workflow/nodes/agent/components/tool-icon.tsx index 4ff0cd780d..8e6993a78d 100644 --- a/web/app/components/workflow/nodes/agent/components/tool-icon.tsx +++ b/web/app/components/workflow/nodes/agent/components/tool-icon.tsx @@ -61,37 +61,39 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => { >
- {(() => { - if (iconFetchError || !icon) +
+ {(() => { + if (iconFetchError || !icon) + return + if (typeof icon === 'string') { + return tool icon setIconFetchError(true)} + /> + } + if (typeof icon === 'object') { + return + } return - if (typeof icon === 'string') { - return tool icon setIconFetchError(true)} - /> - } - if (typeof icon === 'object') { - return - } - return - })()} - {indicator && } + })()} +
+ {indicator && }
}) diff --git a/web/app/components/workflow/nodes/assigner/node.tsx b/web/app/components/workflow/nodes/assigner/node.tsx index ae42065e2d..5e5950d715 100644 --- a/web/app/components/workflow/nodes/assigner/node.tsx +++ b/web/app/components/workflow/nodes/assigner/node.tsx @@ -2,10 +2,13 @@ import type { FC } from 'react' import React from 'react' import { useNodes } from 'reactflow' import { useTranslation } from 'react-i18next' -import NodeVariableItem from '../variable-assigner/components/node-variable-item' import type { AssignerNodeType } from './types' import { isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' import { BlockEnum, type Node, type NodeProps } from '@/app/components/workflow/types' +import { + VariableLabelInNode, +} from '@/app/components/workflow/nodes/_base/components/variable/variable-label' +import Badge from '@/app/components/base/badge' const i18nPrefix = 'workflow.nodes.assigner' @@ -40,12 +43,14 @@ const NodeComponent: FC> = ({ const isSystem = isSystemVar(variable) const node = isSystem ? nodes.find(node => node.data.type === BlockEnum.Start) : nodes.find(node => node.id === variable[0]) return ( - + } /> ) })} @@ -62,11 +67,13 @@ const NodeComponent: FC> = ({ return (
- + } />
) diff --git a/web/app/components/workflow/nodes/document-extractor/node.tsx b/web/app/components/workflow/nodes/document-extractor/node.tsx index 660e2ebb18..ab7fe9a9a6 100644 --- a/web/app/components/workflow/nodes/document-extractor/node.tsx +++ b/web/app/components/workflow/nodes/document-extractor/node.tsx @@ -2,10 +2,12 @@ import type { FC } from 'react' import React from 'react' import { useNodes } from 'reactflow' import { useTranslation } from 'react-i18next' -import NodeVariableItem from '../variable-assigner/components/node-variable-item' import type { DocExtractorNodeType } from './types' import { isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' import { BlockEnum, type Node, type NodeProps } from '@/app/components/workflow/types' +import { + VariableLabelInNode, +} from '@/app/components/workflow/nodes/_base/components/variable/variable-label' const i18nPrefix = 'workflow.nodes.docExtractor' @@ -25,10 +27,10 @@ const NodeComponent: FC> = ({ return (
{t(`${i18nPrefix}.inputVar`)}
-
) diff --git a/web/app/components/workflow/nodes/end/node.tsx b/web/app/components/workflow/nodes/end/node.tsx index 6906e0f77c..2583e61b68 100644 --- a/web/app/components/workflow/nodes/end/node.tsx +++ b/web/app/components/workflow/nodes/end/node.tsx @@ -1,19 +1,16 @@ import type { FC } from 'react' import React from 'react' -import cn from 'classnames' import type { EndNodeType } from './types' import type { NodeProps, Variable } from '@/app/components/workflow/types' -import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' import { useIsChatMode, useWorkflow, useWorkflowVariables, } from '@/app/components/workflow/hooks' -import { VarBlockIcon } from '@/app/components/workflow/block-icon' -import { Line3 } from '@/app/components/base/icons/src/public/common' -import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' -import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others' import { BlockEnum } from '@/app/components/workflow/types' +import { + VariableLabelInNode, +} from '@/app/components/workflow/nodes/_base/components/variable/variable-label' const Node: FC> = ({ id, @@ -42,42 +39,20 @@ const Node: FC> = ({
{filteredOutputs.map(({ value_selector }, index) => { const node = getNode(value_selector[0]) - const isSystem = isSystemVar(value_selector) - const isEnv = isENV(value_selector) - const isChatVar = isConversationVar(value_selector) - const varName = isSystem ? `sys.${value_selector[value_selector.length - 1]}` : value_selector[value_selector.length - 1] const varType = getCurrentVariableType({ valueSelector: value_selector, availableNodes, isChatMode, }) - return ( -
-
- {!isEnv && !isChatVar && ( - <> -
- -
-
{node?.data.title}
- - - )} -
- {!isEnv && !isChatVar && } - {isEnv && } - {isChatVar && } -
{varName}
-
-
-
-
{varType}
-
-
+ return ( + ) })} diff --git a/web/app/components/workflow/nodes/http/node.tsx b/web/app/components/workflow/nodes/http/node.tsx index aa1912bd59..6002bf737d 100644 --- a/web/app/components/workflow/nodes/http/node.tsx +++ b/web/app/components/workflow/nodes/http/node.tsx @@ -15,7 +15,7 @@ const Node: FC> = ({
{method}
-
+
{ const notHasValue = comparisonOperatorNotRequireValue(c.comparison_operator) if (notHasValue) @@ -76,19 +72,11 @@ const ConditionValue = ({ return (
- {!isEnvVar && !isChatVar && } - {isEnvVar && } - {isChatVar && } - -
- {variableName} -
+
| undefined = nodes.find(n => n.id === variableSelector[0]) as Node const isException = isExceptionVariable(variableName, node?.data.type) const formatValue = useMemo(() => { @@ -76,20 +74,14 @@ const ConditionValue = ({ return (
- {!isEnvVar && !isChatVar && } - {isEnvVar && } - {isChatVar && } - -
- {variableName} -
+
> = ({ return (
{t(`${i18nPrefix}.inputVar`)}
-
) diff --git a/web/app/components/workflow/nodes/llm/use-config.ts b/web/app/components/workflow/nodes/llm/use-config.ts index b8516caed8..8c22068671 100644 --- a/web/app/components/workflow/nodes/llm/use-config.ts +++ b/web/app/components/workflow/nodes/llm/use-config.ts @@ -101,7 +101,6 @@ const useConfig = (id: string, payload: LLMNodeType) => { }) setInputs(newInputs) } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultConfig, isChatModel]) const [modelChanged, setModelChanged] = useState(false) @@ -161,7 +160,6 @@ const useConfig = (id: string, payload: LLMNodeType) => { return setModelChanged(false) handleVisionConfigAfterModelChanged() - // eslint-disable-next-line react-hooks/exhaustive-deps }, [isVisionModel, modelChanged]) // variables diff --git a/web/app/components/workflow/nodes/loop/components/condition-files-list-value.tsx b/web/app/components/workflow/nodes/loop/components/condition-files-list-value.tsx index 772b960953..00eec93de3 100644 --- a/web/app/components/workflow/nodes/loop/components/condition-files-list-value.tsx +++ b/web/app/components/workflow/nodes/loop/components/condition-files-list-value.tsx @@ -11,10 +11,10 @@ import { } from '../utils' import type { ValueSelector } from '../../../types' import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from './../default' -import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' -import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others' -import cn from '@/utils/classnames' -import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' +import { isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' +import { + VariableLabelInNode, +} from '@/app/components/workflow/nodes/_base/components/variable/variable-label' const i18nPrefix = 'workflow.nodes.ifElse' type ConditionValueProps = { @@ -32,11 +32,7 @@ const ConditionValue = ({ const variableSelector = variable_selector as ValueSelector - const variableName = (isSystemVar(variableSelector) ? variableSelector.slice(0).join('.') : variableSelector.slice(1).join('.')) const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}`) : operator - const notHasValue = comparisonOperatorNotRequireValue(operator) - const isEnvVar = isENV(variableSelector) - const isChatVar = isConversationVar(variableSelector) const formatValue = useCallback((c: Condition) => { const notHasValue = comparisonOperatorNotRequireValue(c.comparison_operator) if (notHasValue) @@ -76,19 +72,11 @@ const ConditionValue = ({ return (
- {!isEnvVar && !isChatVar && } - {isEnvVar && } - {isChatVar && } - -
- {variableName} -
+
{ const { t } = useTranslation() - const variableName = labelName || (isSystemVar(variableSelector) ? variableSelector.slice(0).join('.') : variableSelector.slice(1).join('.')) const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}`) : operator const notHasValue = comparisonOperatorNotRequireValue(operator) - const isEnvVar = isENV(variableSelector) - const isChatVar = isConversationVar(variableSelector) const formatValue = useMemo(() => { if (notHasValue) return '' @@ -67,19 +64,11 @@ const ConditionValue = ({ return (
- {!isEnvVar && !isChatVar && } - {isEnvVar && } - {isChatVar && } - -
- {variableName} -
+
{ - replaceSpaceWithUnderscreInVarNameInput(e.target) + replaceSpaceWithUnderscoreInVarNameInput(e.target) if (!!e.target.value && !checkVariableName(e.target.value)) return handleUpdateLoopVariable(item.id, { label: e.target.value }) diff --git a/web/app/components/workflow/nodes/variable-assigner/components/node-group-item.tsx b/web/app/components/workflow/nodes/variable-assigner/components/node-group-item.tsx index c5bc9399b0..e96475b953 100644 --- a/web/app/components/workflow/nodes/variable-assigner/components/node-group-item.tsx +++ b/web/app/components/workflow/nodes/variable-assigner/components/node-group-item.tsx @@ -18,10 +18,12 @@ import { } from '../hooks' import { filterVar } from '../utils' import AddVariable from './add-variable' -import NodeVariableItem from './node-variable-item' import { isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' import cn from '@/utils/classnames' import { isExceptionVariable } from '@/app/components/workflow/utils' +import { + VariableLabelInNode, +} from '@/app/components/workflow/nodes/_base/components/variable/variable-label' const i18nPrefix = 'workflow.nodes.variableAssigner' type GroupItem = { @@ -122,22 +124,29 @@ const NodeGroupItem = ({ ) } { - !!item.variables.length && item.variables.map((variable = [], index) => { - const isSystem = isSystemVar(variable) - const node = isSystem ? nodes.find(node => node.data.type === BlockEnum.Start) : nodes.find(node => node.id === variable[0]) - const varName = isSystem ? `sys.${variable[variable.length - 1]}` : variable.slice(1).join('.') - const isException = isExceptionVariable(varName, node?.data.type) + !!item.variables.length && ( +
+ { + item.variables.map((variable = [], index) => { + const isSystem = isSystemVar(variable) - return ( - - ) - }) + const node = isSystem ? nodes.find(node => node.data.type === BlockEnum.Start) : nodes.find(node => node.id === variable[0]) + const varName = isSystem ? `sys.${variable[variable.length - 1]}` : variable.slice(1).join('.') + const isException = isExceptionVariable(varName, node?.data.type) + + return ( + + ) + }) + } +
+ ) }
) diff --git a/web/app/components/workflow/nodes/variable-assigner/components/var-group-item.tsx b/web/app/components/workflow/nodes/variable-assigner/components/var-group-item.tsx index 60be8a0842..a6b02931d9 100644 --- a/web/app/components/workflow/nodes/variable-assigner/components/var-group-item.tsx +++ b/web/app/components/workflow/nodes/variable-assigner/components/var-group-item.tsx @@ -15,7 +15,7 @@ import { VarType } from '@/app/components/workflow/types' import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' import { Folder } from '@/app/components/base/icons/src/vender/line/files' -import { checkKeys, replaceSpaceWithUnderscreInVarNameInput } from '@/utils/var' +import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var' import Toast from '@/app/components/base/toast' const i18nPrefix = 'workflow.nodes.variableAssigner' @@ -89,7 +89,7 @@ const VarGroupItem: FC = ({ }] = useBoolean(false) const handleGroupNameChange = useCallback((e: ChangeEvent) => { - replaceSpaceWithUnderscreInVarNameInput(e.target) + replaceSpaceWithUnderscoreInVarNameInput(e.target) const value = e.target.value const { isValid, errorKey, errorMessageKey } = checkKeys([value], false) if (!isValid) { diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx index 869317ca6a..15292b928d 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx @@ -16,7 +16,7 @@ import type { ConversationVariable } from '@/app/components/workflow/types' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type' import cn from '@/utils/classnames' -import { checkKeys, replaceSpaceWithUnderscreInVarNameInput } from '@/utils/var' +import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var' export type ModalPropsType = { chatVar?: ConversationVariable @@ -144,7 +144,7 @@ const ChatVariableModal = ({ } const handleVarNameChange = (e: React.ChangeEvent) => { - replaceSpaceWithUnderscreInVarNameInput(e.target) + replaceSpaceWithUnderscoreInVarNameInput(e.target) if (!!e.target.value && !checkVariableName(e.target.value)) return setName(e.target.value || '') diff --git a/web/app/components/workflow/panel/env-panel/variable-modal.tsx b/web/app/components/workflow/panel/env-panel/variable-modal.tsx index 4877575d7e..1c780f7341 100644 --- a/web/app/components/workflow/panel/env-panel/variable-modal.tsx +++ b/web/app/components/workflow/panel/env-panel/variable-modal.tsx @@ -10,7 +10,7 @@ import { ToastContext } from '@/app/components/base/toast' import { useStore } from '@/app/components/workflow/store' import type { EnvironmentVariable } from '@/app/components/workflow/types' import cn from '@/utils/classnames' -import { checkKeys, replaceSpaceWithUnderscreInVarNameInput } from '@/utils/var' +import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var' export type ModalPropsType = { env?: EnvironmentVariable @@ -44,7 +44,7 @@ const VariableModal = ({ } const handleVarNameChange = (e: React.ChangeEvent) => { - replaceSpaceWithUnderscreInVarNameInput(e.target) + replaceSpaceWithUnderscoreInVarNameInput(e.target) if (!!e.target.value && !checkVariableName(e.target.value)) return setName(e.target.value || '') diff --git a/web/app/components/workflow/panel/version-history-panel/context-menu/use-context-menu.ts b/web/app/components/workflow/panel/version-history-panel/context-menu/use-context-menu.ts index 3b01db77b3..62043713e8 100644 --- a/web/app/components/workflow/panel/version-history-panel/context-menu/use-context-menu.ts +++ b/web/app/components/workflow/panel/version-history-panel/context-menu/use-context-menu.ts @@ -29,9 +29,12 @@ const useContextMenu = (props: ContextMenuProps) => { key: VersionHistoryContextMenuOptions.edit, name: t('workflow.versionHistory.nameThisVersion'), }, + { + key: VersionHistoryContextMenuOptions.copyId, + name: t('workflow.versionHistory.copyId'), + }, ] - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isNamedVersion]) + }, [isNamedVersion, t]) return { deleteOperation, diff --git a/web/app/components/workflow/panel/version-history-panel/index.tsx b/web/app/components/workflow/panel/version-history-panel/index.tsx index 662b7bb1ff..17710c9994 100644 --- a/web/app/components/workflow/panel/version-history-panel/index.tsx +++ b/web/app/components/workflow/panel/version-history-panel/index.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { RiArrowDownDoubleLine, RiCloseLine, RiLoader2Line } from '@remixicon/react' +import copy from 'copy-to-clipboard' import { useNodesSyncDraft, useWorkflowRun } from '../../hooks' import { useStore, useWorkflowStore } from '../../store' import { VersionHistoryContextMenuOptions, WorkflowVersionFilterOptions } from '../../types' @@ -110,8 +111,15 @@ export const VersionHistoryPanel = ({ case VersionHistoryContextMenuOptions.delete: setDeleteConfirmOpen(true) break + case VersionHistoryContextMenuOptions.copyId: + copy(item.id) + Toast.notify({ + type: 'success', + message: t('workflow.versionHistory.action.copyIdSuccess'), + }) + break } - }, []) + }, [t]) const handleCancel = useCallback((operation: VersionHistoryContextMenuOptions) => { switch (operation) { diff --git a/web/app/components/workflow/panel/version-history-panel/version-history-item.tsx b/web/app/components/workflow/panel/version-history-panel/version-history-item.tsx index 98fd7ce45c..797a3fbe4f 100644 --- a/web/app/components/workflow/panel/version-history-panel/version-history-item.tsx +++ b/web/app/components/workflow/panel/version-history-panel/version-history-item.tsx @@ -55,7 +55,6 @@ const VersionHistoryItem: React.FC = ({ useEffect(() => { if (isDraft) onClick(item) - // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const handleClickItem = () => { diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index 836760cbda..f7aa0702df 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -463,8 +463,9 @@ export enum VersionHistoryContextMenuOptions { restore = 'restore', edit = 'edit', delete = 'delete', + copyId = 'copyId', } -export interface ChildNodeTypeCount { +export type ChildNodeTypeCount = { [key: string]: number; } diff --git a/web/app/components/workflow/variable-inspect/group.tsx b/web/app/components/workflow/variable-inspect/group.tsx index 1b032c8992..29b6c3ca44 100644 --- a/web/app/components/workflow/variable-inspect/group.tsx +++ b/web/app/components/workflow/variable-inspect/group.tsx @@ -11,16 +11,12 @@ import { import ActionButton from '@/app/components/base/action-button' import Tooltip from '@/app/components/base/tooltip' import BlockIcon from '@/app/components/workflow/block-icon' -import { - BubbleX, - Env, -} from '@/app/components/base/icons/src/vender/line/others' -import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' import type { currentVarType } from './panel' import { VarInInspectType } from '@/types/workflow' import type { NodeWithVar, VarInInspect } from '@/types/workflow' import cn from '@/utils/classnames' import { useToolIcon } from '../hooks' +import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label' type Props = { nodeData?: NodeWithVar @@ -158,9 +154,11 @@ const Group = ({ )} onClick={() => handleSelectVar(varItem, varType)} > - {isEnv && } - {isChatVar && } - {(isSystem || nodeData) && } +
{varItem.name}
{varItem.value_type}
diff --git a/web/app/components/workflow/variable-inspect/right.tsx b/web/app/components/workflow/variable-inspect/right.tsx index 6ddd0d47d3..aa318cfe79 100644 --- a/web/app/components/workflow/variable-inspect/right.tsx +++ b/web/app/components/workflow/variable-inspect/right.tsx @@ -14,12 +14,11 @@ import Badge from '@/app/components/base/badge' import CopyFeedback from '@/app/components/base/copy-feedback' import Tooltip from '@/app/components/base/tooltip' import BlockIcon from '@/app/components/workflow/block-icon' -import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others' -import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' import Loading from '@/app/components/base/loading' import type { currentVarType } from './panel' import { VarInInspectType } from '@/types/workflow' import cn from '@/utils/classnames' +import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label' type Props = { currentNodeVar?: currentVarType @@ -86,15 +85,14 @@ const Right = ({
{currentNodeVar && ( <> - {currentNodeVar.nodeType === VarInInspectType.environment && ( - - )} - {currentNodeVar.nodeType === VarInInspectType.conversation && ( - - )} - {currentNodeVar.nodeType === VarInInspectType.system && ( - - )} + { + [VarInInspectType.environment, VarInInspectType.conversation, VarInInspectType.system].includes(currentNodeVar.nodeType as VarInInspectType) && ( + + ) + } {currentNodeVar.nodeType !== VarInInspectType.environment && currentNodeVar.nodeType !== VarInInspectType.conversation && currentNodeVar.nodeType !== VarInInspectType.system && ( <>

{t('login.checkCode.checkYourEmail')}

- + + {t('login.checkCode.tipsPrefix')} + {email} +
{t('login.checkCode.validTime')}

diff --git a/web/app/signin/check-code/page.tsx b/web/app/signin/check-code/page.tsx index 3072dbe1ee..5d87d7b47b 100644 --- a/web/app/signin/check-code/page.tsx +++ b/web/app/signin/check-code/page.tsx @@ -71,7 +71,10 @@ export default function CheckCode() {

{t('login.checkCode.checkYourEmail')}

- + + {t('login.checkCode.tipsPrefix')} + {email} +
{t('login.checkCode.validTime')}

diff --git a/web/context/web-app-context.tsx b/web/context/web-app-context.tsx index db1c5158dd..e78ef81bbc 100644 --- a/web/context/web-app-context.tsx +++ b/web/context/web-app-context.tsx @@ -61,22 +61,18 @@ const WebAppStoreProvider: FC = ({ children }) => { const pathname = usePathname() const searchParams = useSearchParams() const redirectUrlParam = searchParams.get('redirect_url') - const session = searchParams.get('session') - const sysUserId = searchParams.get('sys.user_id') - const [shareCode, setShareCode] = useState(null) - useEffect(() => { - const shareCodeFromRedirect = getShareCodeFromRedirectUrl(redirectUrlParam) - const shareCodeFromPathname = getShareCodeFromPathname(pathname) - const newShareCode = shareCodeFromRedirect || shareCodeFromPathname - setShareCode(newShareCode) - updateShareCode(newShareCode) - }, [pathname, redirectUrlParam, updateShareCode]) + + // Compute shareCode directly + const shareCode = getShareCodeFromRedirectUrl(redirectUrlParam) || getShareCodeFromPathname(pathname) + updateShareCode(shareCode) + const { isFetching, data: accessModeResult } = useGetWebAppAccessModeByCode(shareCode) - const [isFetchingAccessToken, setIsFetchingAccessToken] = useState(true) + const [isFetchingAccessToken, setIsFetchingAccessToken] = useState(false) + useEffect(() => { if (accessModeResult?.accessMode) { updateWebAppAccessMode(accessModeResult.accessMode) - if (accessModeResult?.accessMode === AccessMode.PUBLIC && session && sysUserId) { + if (accessModeResult.accessMode === AccessMode.PUBLIC) { setIsFetchingAccessToken(true) checkOrSetAccessToken(shareCode).finally(() => { setIsFetchingAccessToken(false) @@ -86,7 +82,8 @@ const WebAppStoreProvider: FC = ({ children }) => { setIsFetchingAccessToken(false) } } - }, [accessModeResult, updateWebAppAccessMode, setIsFetchingAccessToken, shareCode, session, sysUserId]) + }, [accessModeResult, updateWebAppAccessMode, shareCode]) + if (isFetching || isFetchingAccessToken) { return
diff --git a/web/i18n-config/auto-gen-i18n.js b/web/i18n-config/auto-gen-i18n.js index 45f5606393..9a8e741063 100644 --- a/web/i18n-config/auto-gen-i18n.js +++ b/web/i18n-config/auto-gen-i18n.js @@ -42,6 +42,13 @@ async function translateMissingKeyDeeply(sourceObj, targetObject, toLanguage) { return } + // Skip template literal placeholders + if (source === 'TEMPLATE_LITERAL_PLACEHOLDER') { + console.log(`⏭️ Skipping template literal key: "${key}"`) + skippedKeys.push(`${key}: ${source}`) + return + } + // Only skip obvious code patterns, not normal text with parentheses const codePatterns = [ /\{\{.*\}\}/, // Template variables like {{key}} @@ -102,6 +109,15 @@ async function autoGenTrans(fileName, toGenLanguage, isDryRun = false) { try { const content = fs.readFileSync(fullKeyFilePath, 'utf8') + // Temporarily replace template literals with regular strings for AST parsing + // This allows us to process other keys while skipping problematic ones + let processedContent = content + const templateLiteralPattern = /(resolutionTooltip):\s*`([^`]*)`/g + processedContent = processedContent.replace(templateLiteralPattern, (match, key, value) => { + console.log(`⏭️ Temporarily replacing template literal for key: ${key}`) + return `${key}: "TEMPLATE_LITERAL_PLACEHOLDER"` + }) + // Create a safer module environment for vm const moduleExports = {} const context = { @@ -114,7 +130,7 @@ async function autoGenTrans(fileName, toGenLanguage, isDryRun = false) { } // Use vm.runInNewContext instead of eval for better security - vm.runInNewContext(transpile(content), context) + vm.runInNewContext(transpile(processedContent), context) const fullKeyContent = moduleExports.default || moduleExports @@ -132,7 +148,14 @@ export default translation // To keep object format and format it for magicast to work: const translation = { ... } => export default {...} const readContent = await loadFile(toGenLanguageFilePath) const { code: toGenContent } = generateCode(readContent) - const mod = await parseModule(`export default ${toGenContent.replace('export default translation', '').replace('const translation = ', '')}`) + + // Also handle template literals in target file content + let processedToGenContent = toGenContent + processedToGenContent = processedToGenContent.replace(templateLiteralPattern, (match, key, value) => { + console.log(`⏭️ Temporarily replacing template literal in target file for key: ${key}`) + return `${key}: "TEMPLATE_LITERAL_PLACEHOLDER"` + }) + const mod = await parseModule(`export default ${processedToGenContent.replace('export default translation', '').replace('const translation = ', '')}`) const toGenOutPut = mod.exports.default console.log(`\n🌍 Processing ${fileName} for ${toGenLanguage}...`) @@ -151,11 +174,26 @@ export default translation } const { code } = generateCode(mod) - const res = `const translation =${code.replace('export default', '')} + let res = `const translation =${code.replace('export default', '')} export default translation `.replace(/,\n\n/g, ',\n').replace('};', '}') + // Restore original template literals by reading from the original target file if it exists + if (fs.existsSync(toGenLanguageFilePath)) { + const originalContent = fs.readFileSync(toGenLanguageFilePath, 'utf8') + // Extract original template literal content for resolutionTooltip + const originalMatch = originalContent.match(/(resolutionTooltip):\s*`([^`]*)`/s) + if (originalMatch) { + const [fullMatch, key, value] = originalMatch + res = res.replace( + `${key}: "TEMPLATE_LITERAL_PLACEHOLDER"`, + `${key}: \`${value}\``, + ) + console.log(`🔄 Restored original template literal for key: ${key}`) + } + } + if (!isDryRun) { fs.writeFileSync(toGenLanguageFilePath, res) console.log(`💾 Saved translations to ${toGenLanguageFilePath}`) @@ -190,7 +228,7 @@ async function main() { .readdirSync(path.resolve(__dirname, i18nFolder, targetLanguage)) .filter(file => /\.ts$/.test(file)) // Only process .ts files .map(file => file.replace(/\.ts$/, '')) - .filter(f => f !== 'app-debug') // ast parse error in app-debug + // Removed app-debug exclusion, now only skip specific problematic keys // Filter by target file if specified const filesToProcess = targetFile ? files.filter(f => f === targetFile) : files diff --git a/web/i18n-config/check-i18n.js b/web/i18n-config/check-i18n.js index edc2566a3c..cc55277613 100644 --- a/web/i18n-config/check-i18n.js +++ b/web/i18n-config/check-i18n.js @@ -129,10 +129,11 @@ async function removeExtraKeysFromFile(language, fileName, extraKeys) { let modified = false const linesToRemove = [] - // Find lines to remove for each key + // Find lines to remove for each key (including multiline values) for (const keyToRemove of fileSpecificKeys) { const keyParts = keyToRemove.split('.') let targetLineIndex = -1 + const linesToRemoveForKey = [] // Build regex pattern for the exact key path if (keyParts.length === 1) { @@ -183,8 +184,53 @@ async function removeExtraKeysFromFile(language, fileName, extraKeys) { } if (targetLineIndex !== -1) { - linesToRemove.push(targetLineIndex) - console.log(`🗑️ Found key to remove: ${keyToRemove} at line ${targetLineIndex + 1}`) + linesToRemoveForKey.push(targetLineIndex) + + // Check if this is a multiline key-value pair + const keyLine = lines[targetLineIndex] + const trimmedKeyLine = keyLine.trim() + + // If key line ends with ":" (not ":", "{ " or complete value), it's likely multiline + if (trimmedKeyLine.endsWith(':') && !trimmedKeyLine.includes('{') && !trimmedKeyLine.match(/:\s*['"`]/)) { + // Find the value lines that belong to this key + let currentLine = targetLineIndex + 1 + let foundValue = false + + while (currentLine < lines.length) { + const line = lines[currentLine] + const trimmed = line.trim() + + // Skip empty lines + if (trimmed === '') { + currentLine++ + continue + } + + // Check if this line starts a new key (indicates end of current value) + if (trimmed.match(/^\w+\s*:/)) + break + + // Check if this line is part of the value + if (trimmed.startsWith('\'') || trimmed.startsWith('"') || trimmed.startsWith('`') || foundValue) { + linesToRemoveForKey.push(currentLine) + foundValue = true + + // Check if this line ends the value (ends with quote and comma/no comma) + if ((trimmed.endsWith('\',') || trimmed.endsWith('",') || trimmed.endsWith('`,') + || trimmed.endsWith('\'') || trimmed.endsWith('"') || trimmed.endsWith('`')) + && !trimmed.startsWith('//')) + break + } + else { + break + } + + currentLine++ + } + } + + linesToRemove.push(...linesToRemoveForKey) + console.log(`🗑️ Found key to remove: ${keyToRemove} at line ${targetLineIndex + 1}${linesToRemoveForKey.length > 1 ? ` (multiline, ${linesToRemoveForKey.length} lines)` : ''}`) modified = true } else { @@ -193,10 +239,10 @@ async function removeExtraKeysFromFile(language, fileName, extraKeys) { } if (modified) { - // Remove lines in reverse order to maintain correct indices - linesToRemove.sort((a, b) => b - a) + // Remove duplicates and sort in reverse order to maintain correct indices + const uniqueLinesToRemove = [...new Set(linesToRemove)].sort((a, b) => b - a) - for (const lineIndex of linesToRemove) { + for (const lineIndex of uniqueLinesToRemove) { const line = lines[lineIndex] console.log(`🗑️ Removing line ${lineIndex + 1}: ${line.trim()}`) lines.splice(lineIndex, 1) @@ -237,7 +283,7 @@ async function main() { // Filter target keys by file if specified const targetKeys = targetFile - ? allTargetKeys.filter(key => key.startsWith(targetFile.replace(/[-_](.)/g, (_, c) => c.toUpperCase()))) + ? allTargetKeys.filter(key => key.startsWith(`${targetFile.replace(/[-_](.)/g, (_, c) => c.toUpperCase())}.`)) : allTargetKeys // Filter languages by target language if specified @@ -247,7 +293,7 @@ async function main() { // Filter language keys by file if specified const languagesKeys = targetFile - ? allLanguagesKeys.map(keys => keys.filter(key => key.startsWith(targetFile.replace(/[-_](.)/g, (_, c) => c.toUpperCase())))) + ? allLanguagesKeys.map(keys => keys.filter(key => key.startsWith(`${targetFile.replace(/[-_](.)/g, (_, c) => c.toUpperCase())}.`))) : allLanguagesKeys const keysCount = languagesKeys.map(keys => keys.length) diff --git a/web/i18n/de-DE/app-annotation.ts b/web/i18n/de-DE/app-annotation.ts index 2e141ed380..be6f1948a2 100644 --- a/web/i18n/de-DE/app-annotation.ts +++ b/web/i18n/de-DE/app-annotation.ts @@ -83,6 +83,16 @@ const translation = { configConfirmBtn: 'Speichern', }, embeddingModelSwitchTip: 'Anmerkungstext-Vektorisierungsmodell, das Wechseln von Modellen wird neu eingebettet, was zusätzliche Kosten verursacht.', + list: { + delete: { + title: 'Bist du sicher, dass du löschen möchtest?', + }, + }, + batchAction: { + cancel: 'Abbrechen', + selected: 'Ausgewählt', + delete: 'Löschen', + }, } export default translation diff --git a/web/i18n/de-DE/app-debug.ts b/web/i18n/de-DE/app-debug.ts index 93511faf55..68f674b76a 100644 --- a/web/i18n/de-DE/app-debug.ts +++ b/web/i18n/de-DE/app-debug.ts @@ -197,6 +197,7 @@ const translation = { after: '', }, }, + contentEnableLabel: 'Moderater Inhalt aktiviert', }, fileUpload: { title: 'Datei-Upload', @@ -241,6 +242,7 @@ const translation = { 'Bitte warten Sie auf die Antwort auf die Stapelaufgabe, um abzuschließen.', notSelectModel: 'Bitte wählen Sie ein Modell', waitForImgUpload: 'Bitte warten Sie, bis das Bild hochgeladen ist', + waitForFileUpload: 'Bitte warten Sie, bis die Datei(en) hochgeladen sind', }, chatSubTitle: 'Anweisungen', completionSubTitle: 'Vor-Prompt', @@ -276,20 +278,62 @@ const translation = { queryNoBeEmpty: 'Anfrage muss im Prompt gesetzt sein', }, variableConfig: { - description: 'Einstellung für Variable {{varName}}', - fieldType: 'Feldtyp', - string: 'Kurztext', - paragraph: 'Absatz', - select: 'Auswählen', - notSet: 'Nicht gesetzt, versuchen Sie, {{input}} im Vor-Prompt zu tippen', - stringTitle: 'Formular-Textfeldoptionen', - maxLength: 'Maximale Länge', - options: 'Optionen', - addOption: 'Option hinzufügen', - apiBasedVar: 'API-basierte Variable', - defaultValue: 'Standardwert', - noDefaultValue: 'Kein Standardwert', - selectDefaultValue: 'Standardwert auswählen', + 'description': 'Einstellung für Variable {{varName}}', + 'fieldType': 'Feldtyp', + 'string': 'Kurztext', + 'paragraph': 'Absatz', + 'select': 'Auswählen', + 'notSet': 'Nicht gesetzt, versuchen Sie, {{input}} im Vor-Prompt zu tippen', + 'stringTitle': 'Formular-Textfeldoptionen', + 'maxLength': 'Maximale Länge', + 'options': 'Optionen', + 'addOption': 'Option hinzufügen', + 'apiBasedVar': 'API-basierte Variable', + 'defaultValue': 'Standardwert', + 'noDefaultValue': 'Kein Standardwert', + 'selectDefaultValue': 'Standardwert auswählen', + 'file': { + image: { + name: 'Bild', + }, + audio: { + name: 'Audio', + }, + document: { + name: 'Dokument', + }, + video: { + name: 'Video', + }, + custom: { + description: 'Geben Sie andere Dateitypen an.', + createPlaceholder: ' Dateiendung, z.B. .doc', + name: 'Andere Dateitypen', + }, + supportFileTypes: 'Unterstützte Dateitypen', + }, + 'errorMsg': { + optionRepeat: 'Hat Wiederholungsoptionen', + atLeastOneOption: 'Mindestens eine Option ist erforderlich', + labelNameRequired: 'Labelname ist erforderlich', + varNameCanBeRepeat: 'Variablenname kann nicht wiederholt werden', + }, + 'multi-files': 'Dateiliste', + 'varName': 'Variablenname', + 'content': 'Inhalt', + 'single-file': 'Einzelne Datei', + 'labelName': 'Kennsatzname', + 'addModalTitle': 'Eingabefeld hinzufügen', + 'hide': 'Verstecken', + 'inputPlaceholder': 'Bitte geben Sie ein', + 'both': 'Beide', + 'uploadFileTypes': 'Dateitypen hochladen', + 'maxNumberOfUploads': 'Maximale Anzahl von Uploads', + 'number': 'Zahl', + 'editModalTitle': 'Eingabefeld bearbeiten', + 'required': 'Erforderlich', + 'text-input': 'Kurztext', + 'localUpload': 'Lokaler Upload', }, vision: { name: 'Vision', @@ -309,6 +353,7 @@ const translation = { url: 'URL', uploadLimit: 'Upload-Limit', }, + onlySupportVisionModelTip: 'Unterstützt nur Bildverarbeitungsmodelle', }, voice: { name: 'Stimme', @@ -320,6 +365,9 @@ const translation = { language: 'Sprache', resolutionTooltip: 'Text-zu-Sprache unterstützte Sprache.', voice: 'Stimme', + autoPlay: 'Automatische Wiedergabe', + autoPlayEnabled: 'Auf', + autoPlayDisabled: 'Aus', }, }, openingStatement: { @@ -376,6 +424,8 @@ const translation = { score_threshold: 'Schwellenwert', score_thresholdTip: 'Wird verwendet, um den Ähnlichkeitsschwellenwert für die Abschnittsfilterung einzustellen.', retrieveChangeTip: 'Das Ändern des Indexmodus und des Abfragemodus kann Anwendungen beeinflussen, die mit diesem Wissen verbunden sind.', + embeddingModelRequired: 'Ein konfiguriertes Einbettungsmodell ist erforderlich', + knowledgeTip: 'Klicken Sie auf die Schaltfläche " ", um Wissen hinzuzufügen', }, debugAsSingleModel: 'Als Einzelmodell debuggen', debugAsMultipleModel: 'Als Mehrfachmodelle debuggen', @@ -417,6 +467,79 @@ const translation = { enabled: 'Aktiviert', }, }, + codegen: { + applyChanges: 'Änderungen übernehmen', + generatedCodeTitle: 'Generierter Code', + instructionPlaceholder: 'Geben Sie eine detaillierte Beschreibung des Codes ein, den Sie generieren möchten.', + overwriteConfirmMessage: 'Durch diese Aktion wird der vorhandene Code überschrieben. Möchten Sie fortfahren?', + title: 'Codegenerator', + noDataLine1: 'Beschreiben Sie links Ihren Anwendungsfall,', + loading: 'Code wird generiert...', + resTitle: 'Generierter Code', + description: 'Der Code-Generator verwendet konfigurierte Modelle, um qualitativ hochwertigen Code basierend auf Ihren Anweisungen zu generieren. Bitte geben Sie klare und detaillierte Anweisungen.', + instruction: 'Anweisungen', + apply: 'Anwenden', + generate: 'Erzeugen', + overwriteConfirmTitle: 'Vorhandenen Code überschreiben?', + noDataLine2: 'Die Codevorschau wird hier angezeigt.', + }, + generate: { + template: { + pythonDebugger: { + instruction: 'Ein Bot, der Ihren Code basierend auf Ihren Anweisungen generieren und debuggen kann', + name: 'Python-Debugger', + }, + translation: { + instruction: 'Ein Übersetzer, der mehrere Sprachen übersetzen kann', + name: 'Übersetzung', + }, + professionalAnalyst: { + name: 'Professioneller Analyst', + instruction: 'Extrahieren Sie Erkenntnisse, identifizieren Sie Risiken und destillieren Sie wichtige Informationen aus langen Berichten in einem einzigen Memo', + }, + excelFormulaExpert: { + instruction: 'Ein Chatbot, der Anfängern helfen kann, Excel-Formeln basierend auf Benutzeranweisungen zu verstehen, zu verwenden und zu erstellen', + name: 'Excel-Formel-Experte', + }, + travelPlanning: { + instruction: 'Der Reiseplanungsassistent ist ein intelligentes Tool, mit dem Benutzer ihre Reisen mühelos planen können', + name: 'Reiseplanung', + }, + SQLSorcerer: { + name: 'SQL-Zauberer', + instruction: 'Verwandeln Sie alltägliche Sprache in SQL-Abfragen', + }, + GitGud: { + name: 'Git gud', + instruction: 'Generieren geeigneter Git-Befehle basierend auf vom Benutzer beschriebenen Aktionen zur Versionskontrolle', + }, + meetingTakeaways: { + instruction: 'Fassen Sie Meetings in prägnante Zusammenfassungen zusammen, die Diskussionsthemen, wichtige Erkenntnisse und Aktionspunkte enthalten', + name: 'Takeaways für Meetings', + }, + writingsPolisher: { + instruction: 'Verwenden Sie fortgeschrittene Lektoratstechniken, um Ihre Texte zu verbessern', + name: 'Polierer für Schreibstil', + }, + }, + title: 'Eingabeaufforderungs-Generator', + apply: 'Anwenden', + overwriteTitle: 'Vorhandene Konfiguration überschreiben?', + instructionPlaceHolder: 'Schreiben Sie klare und spezifische Anweisungen.', + noDataLine1: 'Beschreiben Sie links Ihren Anwendungsfall,', + noDataLine2: 'Die Orchestrierungsvorschau wird hier angezeigt.', + instruction: 'Anweisungen', + tryIt: 'Versuch es', + generate: 'Erzeugen', + overwriteMessage: 'Durch Anwenden dieser Eingabeaufforderung wird die vorhandene Konfiguration überschrieben.', + loading: 'Orchestrieren Sie die Anwendung für Sie...', + resTitle: 'Generierte Eingabeaufforderung', + description: 'Der Eingabeaufforderungsgenerator verwendet das konfigurierte Modell, um Eingabeaufforderungen für eine höhere Qualität und bessere Struktur zu optimieren. Bitte schreiben Sie klare und detaillierte Anweisungen.', + }, + warningMessage: { + timeoutExceeded: 'Die Ergebnisse werden aufgrund einer Zeitüberschreitung nicht angezeigt. Bitte beziehen Sie sich auf die Protokolle, um die vollständigen Ergebnisse zu erhalten.', + }, + noResult: 'Hier wird die Ausgabe angezeigt.', } export default translation diff --git a/web/i18n/de-DE/dataset-documents.ts b/web/i18n/de-DE/dataset-documents.ts index 438bcb708d..b17230354b 100644 --- a/web/i18n/de-DE/dataset-documents.ts +++ b/web/i18n/de-DE/dataset-documents.ts @@ -30,6 +30,7 @@ const translation = { sync: 'Synchronisieren', resume: 'Fortsetzen', pause: 'Pause', + download: 'Datei herunterladen', }, index: { enable: 'Aktivieren', diff --git a/web/i18n/de-DE/login.ts b/web/i18n/de-DE/login.ts index 7ef0e2420a..ef87f05f79 100644 --- a/web/i18n/de-DE/login.ts +++ b/web/i18n/de-DE/login.ts @@ -79,9 +79,9 @@ const translation = { useAnotherMethod: 'Verwenden Sie eine andere Methode', validTime: 'Beachten Sie, dass der Code 5 Minuten lang gültig ist', emptyCode: 'Code ist erforderlich', - tips: 'Wir senden einen Verifizierungscode an {{email}}', invalidCode: 'Ungültiger Code', resend: 'Wieder senden', + tipsPrefix: 'Wir senden einen Bestätigungscode an', }, or: 'ODER', back: 'Zurück', diff --git a/web/i18n/de-DE/workflow.ts b/web/i18n/de-DE/workflow.ts index 72f9642995..1bd965c731 100644 --- a/web/i18n/de-DE/workflow.ts +++ b/web/i18n/de-DE/workflow.ts @@ -927,6 +927,7 @@ const translation = { deleteFailure: 'Version löschen fehlgeschlagen', restoreSuccess: 'Version wiederhergestellt', updateFailure: 'Aktualisierung der Version fehlgeschlagen', + copyIdSuccess: 'ID in die Zwischenablage kopiert', }, latest: 'Neueste', nameThisVersion: 'Nennen Sie diese Version', @@ -937,6 +938,7 @@ const translation = { editVersionInfo: 'Versionsinformationen bearbeiten', deletionTip: 'Die Löschung ist unumkehrbar, bitte bestätigen Sie.', restorationTip: 'Nach der Wiederherstellung der Version wird der aktuelle Entwurf überschrieben.', + copyId: 'ID kopieren', }, debug: { noData: { diff --git a/web/i18n/en-US/dataset-documents.ts b/web/i18n/en-US/dataset-documents.ts index c8d3c64567..84cb7d7d05 100644 --- a/web/i18n/en-US/dataset-documents.ts +++ b/web/i18n/en-US/dataset-documents.ts @@ -32,6 +32,7 @@ const translation = { sync: 'Sync', pause: 'Pause', resume: 'Resume', + download: 'Download File', }, index: { enable: 'Enable', diff --git a/web/i18n/en-US/login.ts b/web/i18n/en-US/login.ts index d47eb7c079..a00e73b901 100644 --- a/web/i18n/en-US/login.ts +++ b/web/i18n/en-US/login.ts @@ -79,7 +79,7 @@ const translation = { validate: 'Validate', checkCode: { checkYourEmail: 'Check your email', - tips: 'We send a verification code to {{email}}', + tipsPrefix: 'We send a verification code to ', validTime: 'Bear in mind that the code is valid for 5 minutes', verificationCode: 'Verification code', verificationCodePlaceholder: 'Enter 6-digit code', diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 1a963396bf..ac87041956 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -942,6 +942,7 @@ const translation = { defaultName: 'Untitled Version', nameThisVersion: 'Name this version', editVersionInfo: 'Edit version info', + copyId: 'Copy ID', editField: { title: 'Title', releaseNotes: 'Release Notes', @@ -958,7 +959,8 @@ const translation = { deleteFailure: 'Failed to delete version', updateSuccess: 'Version updated', updateFailure: 'Failed to update version', - }, + copyIdSuccess: 'ID copied to clipboard', + }, }, debug: { settingsTab: 'Settings', diff --git a/web/i18n/es-ES/app-annotation.ts b/web/i18n/es-ES/app-annotation.ts index 2a797edcc3..9a25037e18 100644 --- a/web/i18n/es-ES/app-annotation.ts +++ b/web/i18n/es-ES/app-annotation.ts @@ -83,6 +83,16 @@ const translation = { configConfirmBtn: 'Guardar', }, embeddingModelSwitchTip: 'Modelo de vectorización de texto de anotación, cambiar de modelo volverá a incrustar, lo que resultará en costos adicionales.', + list: { + delete: { + title: '¿Estás seguro de que deseas eliminar?', + }, + }, + batchAction: { + delete: 'Eliminar', + selected: 'Seleccionado', + cancel: 'Cancelar', + }, } export default translation diff --git a/web/i18n/es-ES/app-debug.ts b/web/i18n/es-ES/app-debug.ts index dbdc32c36b..dd670fab04 100644 --- a/web/i18n/es-ES/app-debug.ts +++ b/web/i18n/es-ES/app-debug.ts @@ -197,6 +197,7 @@ const translation = { after: '', }, }, + contentEnableLabel: 'Contenido moderado habilitado', }, fileUpload: { title: 'Subida de archivos', @@ -240,6 +241,7 @@ const translation = { waitForBatchResponse: 'Por favor espera la respuesta a la tarea por lotes para completar.', notSelectModel: 'Por favor elige un modelo', waitForImgUpload: 'Por favor espera a que la imagen se cargue', + waitForFileUpload: 'Espere a que se cargue el archivo o los archivos', }, chatSubTitle: 'Instrucciones', completionSubTitle: 'Prefijo de la Indicación', @@ -302,6 +304,32 @@ const translation = { 'defaultValue': 'Valor predeterminado', 'noDefaultValue': 'Sin valor predeterminado', 'selectDefaultValue': 'Seleccionar valor predeterminado', + 'file': { + image: { + name: 'Imagen', + }, + audio: { + name: 'Audio', + }, + document: { + name: 'Documento', + }, + video: { + name: 'Vídeo', + }, + custom: { + name: 'Otros tipos de archivos', + description: 'Especifique otros tipos de archivo.', + createPlaceholder: ' Extensión de archivo, por ejemplo, .doc', + }, + supportFileTypes: 'Tipos de archivos de soporte', + }, + 'uploadFileTypes': 'Cargar tipos de archivos', + 'localUpload': 'Carga local', + 'both': 'ambos', + 'single-file': 'En fila india', + 'maxNumberOfUploads': 'Número máximo de cargas', + 'multi-files': 'Lista de archivos', }, vision: { name: 'Visión', @@ -321,6 +349,7 @@ const translation = { url: 'URL', uploadLimit: 'Límite de carga', }, + onlySupportVisionModelTip: 'Solo admite modelos de visión', }, voice: { name: 'Voz', @@ -389,6 +418,7 @@ const translation = { score_threshold: 'Umbral de Puntuación', score_thresholdTip: 'Usado para establecer el umbral de similitud para la filtración de fragmentos.', retrieveChangeTip: 'Modificar el modo de índice y el modo de recuperación puede afectar las aplicaciones asociadas con este Conocimiento.', + embeddingModelRequired: 'Se requiere un modelo de incrustación configurado', }, debugAsSingleModel: 'Depurar como Modelo Único', debugAsMultipleModel: 'Depurar como Múltiples Modelos', @@ -430,6 +460,79 @@ const translation = { enabled: 'Habilitado', }, }, + codegen: { + apply: 'Aplicar', + overwriteConfirmMessage: 'Esta acción sobrescribirá el código existente. ¿Quieres continuar?', + instruction: 'Instrucciones', + loading: 'Generando código...', + title: 'Generador de código', + resTitle: 'Código generado', + description: 'El Generador de código utiliza modelos configurados para generar código de alta calidad basado en sus instrucciones. Proporcione instrucciones claras y detalladas.', + noDataLine1: 'Describa su caso de uso a la izquierda,', + generate: 'Generar', + generatedCodeTitle: 'Código generado', + noDataLine2: 'La vista previa del código se mostrará aquí.', + overwriteConfirmTitle: '¿Sobrescribir el código existente?', + instructionPlaceholder: 'Introduzca una descripción detallada del código que desea generar.', + applyChanges: 'Aplicar cambios', + }, + generate: { + template: { + pythonDebugger: { + name: 'Depurador de Python', + instruction: 'Un bot que puede generar y depurar el código en función de las instrucciones', + }, + translation: { + instruction: 'Un traductor que puede traducir varios idiomas', + name: 'Traducción', + }, + professionalAnalyst: { + instruction: 'Extraiga información, identifique riesgos y extraiga información clave de informes largos en un solo memorándum', + name: 'Analista profesional', + }, + excelFormulaExpert: { + name: 'Experto en fórmulas de Excel', + instruction: 'Un chatbot que puede ayudar a los usuarios novatos a comprender, usar y crear fórmulas de Excel basadas en las instrucciones del usuario', + }, + travelPlanning: { + instruction: 'El Asistente de planificación de viajes es una herramienta inteligente diseñada para ayudar a los usuarios a planificar sus viajes sin esfuerzo', + name: 'Planificación de viajes', + }, + SQLSorcerer: { + instruction: 'Transforme el lenguaje cotidiano en consultas SQL', + name: 'Hechicero SQL', + }, + GitGud: { + name: 'Git gud', + instruction: 'Generar comandos de Git adecuados basados en acciones de control de versiones descritas por el usuario', + }, + meetingTakeaways: { + name: 'Conclusiones de la reunión', + instruction: 'Destilar las reuniones en resúmenes concisos que incluyan temas de discusión, conclusiones clave y elementos de acción', + }, + writingsPolisher: { + name: 'Pulidora de escritura', + instruction: 'Utiliza técnicas avanzadas de corrección de textos para mejorar tus escritos', + }, + }, + apply: 'Aplicar', + instruction: 'Instrucciones', + noDataLine2: 'La vista previa de orquestación se mostrará aquí.', + description: 'El generador de mensajes utiliza el modelo configurado para optimizar los mensajes para una mayor calidad y una mejor estructura. Escriba instrucciones claras y detalladas.', + generate: 'Generar', + title: 'Generador de avisos', + tryIt: 'Pruébalo', + overwriteMessage: 'La aplicación de este mensaje anulará la configuración existente.', + resTitle: 'Mensaje generado', + noDataLine1: 'Describa su caso de uso a la izquierda,', + overwriteTitle: '¿Anular la configuración existente?', + loading: 'Orquestando la aplicación para usted...', + instructionPlaceHolder: 'Escriba instrucciones claras y específicas.', + }, + warningMessage: { + timeoutExceeded: 'Los resultados no se muestran debido al tiempo de espera. Consulte los registros para obtener resultados completos.', + }, + noResult: 'La salida se mostrará aquí.', } export default translation diff --git a/web/i18n/es-ES/dataset-documents.ts b/web/i18n/es-ES/dataset-documents.ts index 3775873b40..408c4bd0e0 100644 --- a/web/i18n/es-ES/dataset-documents.ts +++ b/web/i18n/es-ES/dataset-documents.ts @@ -31,6 +31,7 @@ const translation = { sync: 'Sincronizar', resume: 'Reanudar', pause: 'Pausa', + download: 'Descargar archivo', }, index: { enable: 'Habilitar', diff --git a/web/i18n/es-ES/login.ts b/web/i18n/es-ES/login.ts index fda14f3708..8fd82ecb85 100644 --- a/web/i18n/es-ES/login.ts +++ b/web/i18n/es-ES/login.ts @@ -78,10 +78,10 @@ const translation = { emptyCode: 'Se requiere código', useAnotherMethod: 'Usar otro método', resend: 'Reenviar', - tips: 'Enviamos un código de verificación a {{email}}', verificationCode: 'Código de verificación', validTime: 'Ten en cuenta que el código es válido durante 5 minutos', invalidCode: 'Código no válido', + tipsPrefix: 'Enviamos un código de verificación a', }, or: 'O', back: 'Atrás', diff --git a/web/i18n/es-ES/workflow.ts b/web/i18n/es-ES/workflow.ts index 459121a168..d4958c3c0d 100644 --- a/web/i18n/es-ES/workflow.ts +++ b/web/i18n/es-ES/workflow.ts @@ -927,6 +927,7 @@ const translation = { deleteFailure: 'Error al eliminar la versión', updateFailure: 'Error al actualizar la versión', restoreSuccess: 'Versión restaurada', + copyIdSuccess: 'ID copiado en el portapapeles', }, releaseNotesPlaceholder: 'Describe lo que cambió', restorationTip: 'Después de la restauración de la versión, el borrador actual será sobrescrito.', @@ -937,6 +938,7 @@ const translation = { currentDraft: 'Borrador Actual', editVersionInfo: 'Editar información de la versión', latest: 'Último', + copyId: 'Copiar ID', }, debug: { noData: { diff --git a/web/i18n/fa-IR/app-annotation.ts b/web/i18n/fa-IR/app-annotation.ts index d66c2eb0e5..7bedf8371f 100644 --- a/web/i18n/fa-IR/app-annotation.ts +++ b/web/i18n/fa-IR/app-annotation.ts @@ -83,6 +83,16 @@ const translation = { configConfirmBtn: 'ذخیره', }, embeddingModelSwitchTip: 'مدل برداری‌سازی متن یادداشت، تغییر مدل‌ها باعث جاسازی مجدد خواهد شد و هزینه‌های اضافی به همراه خواهد داشت.', + list: { + delete: { + title: 'آیا مطمئن هستید که می‌خواهید حذف کنید؟', + }, + }, + batchAction: { + cancel: 'لغو', + selected: 'انتخاب شده', + delete: 'حذف کنید', + }, } export default translation diff --git a/web/i18n/fa-IR/app-debug.ts b/web/i18n/fa-IR/app-debug.ts index 5efbb9421b..333fb68620 100644 --- a/web/i18n/fa-IR/app-debug.ts +++ b/web/i18n/fa-IR/app-debug.ts @@ -197,6 +197,7 @@ const translation = { after: '', }, }, + contentEnableLabel: 'محتوای متوسط فعال شده است', }, generate: { title: 'تولید کننده دستورالعمل', @@ -478,6 +479,291 @@ const translation = { description: 'فعال‌سازی صوت به مدل اجازه می‌دهد فایل‌های صوتی را برای رونویسی و تجزیه و تحلیل پردازش کند.', }, }, + codegen: { + apply: 'درخواست', + resTitle: 'کد تولید شده', + generate: 'تولید', + loading: 'تولید کد...', + applyChanges: 'اعمال تغییرات', + generatedCodeTitle: 'کد تولید شده', + title: 'ژنراتور کد', + instruction: 'دستورالعمل', + instructionPlaceholder: 'توضیحات دقیق کدی را که می خواهید تولید کنید وارد کنید.', + overwriteConfirmMessage: 'این عمل کد موجود را بازنویسی می کند. آیا می خواهید ادامه دهید؟', + overwriteConfirmTitle: 'کد موجود را بازنویسی کنید؟', + noDataLine2: 'پیش نمایش کد در اینجا نشان داده می شود.', + noDataLine1: 'مورد استفاده خود را در سمت چپ شرح دهید،', + description: 'Code Generator از مدل های پیکربندی شده برای تولید کد با کیفیت بالا بر اساس دستورالعمل های شما استفاده می کند. لطفا دستورالعمل های واضح و دقیق ارائه دهید.', + }, + generate: { + template: { + pythonDebugger: { + name: 'اشکال زدایی پایتون', + instruction: 'رباتی که می تواند کد شما را بر اساس دستورالعمل شما تولید و اشکال زدایی کند', + }, + translation: { + name: 'ترجمه', + instruction: 'مترجمی که می تواند چندین زبان را ترجمه کند', + }, + professionalAnalyst: { + name: 'تحلیلگر حرفه ای', + instruction: 'استخراج بینش، شناسایی ریسک و تقطیر اطلاعات کلیدی از گزارش های طولانی در یک یادداشت', + }, + excelFormulaExpert: { + name: 'کارشناس فرمول اکسل', + instruction: 'یک ربات چت که می تواند به کاربران تازه کار کمک کند تا فرمول های اکسل را بر اساس دستورالعمل های کاربر درک، استفاده و ایجاد کنند', + }, + travelPlanning: { + name: 'برنامه ریزی سفر', + instruction: 'دستیار برنامه ریزی سفر ابزاری هوشمند است که برای کمک به کاربران در برنامه ریزی بدون زحمت سفرهای خود طراحی شده است', + }, + SQLSorcerer: { + name: 'جادوگر SQL', + instruction: 'تبدیل زبان روزمره به کوئری های SQL', + }, + GitGud: { + name: 'گیت گود', + instruction: 'فرمان های Git مناسب را بر اساس اکشن های کنترل نسخه توصیف شده توسط کاربر ایجاد کنید', + }, + meetingTakeaways: { + name: 'نکات مهم جلسه', + instruction: 'جلسات را به خلاصه های مختصر از جمله موضوعات بحث، نکات کلیدی و موارد اقدام تقطیر کنید', + }, + writingsPolisher: { + name: 'پولیش نوشتن', + instruction: 'از تکنیک های پیشرفته ویرایش کپی برای بهبود نوشته های خود استفاده کنید', + }, + }, + title: 'ژنراتور سریع', + resTitle: 'اعلان تولید شده', + overwriteTitle: 'پیکربندی موجود را لغو کنید؟', + generate: 'تولید', + noDataLine1: 'مورد استفاده خود را در سمت چپ شرح دهید،', + apply: 'درخواست', + instruction: 'دستورالعمل', + overwriteMessage: 'اعمال این اعلان پیکربندی موجود را لغو می کند.', + instructionPlaceHolder: 'دستورالعمل های واضح و مشخص بنویسید.', + tryIt: 'آن را امتحان کنید', + noDataLine2: 'پیش نمایش ارکستراسیون در اینجا نشان داده می شود.', + loading: 'هماهنگ کردن برنامه برای شما...', + description: 'Prompt Generator از مدل پیکربندی شده برای بهینه سازی درخواست ها برای کیفیت بالاتر و ساختار بهتر استفاده می کند. لطفا دستورالعمل های واضح و دقیق بنویسید.', + }, + resetConfig: { + title: 'بازنشانی را تأیید کنید؟', + message: 'بازنشانی تغییرات را دور می اندازد و آخرین پیکربندی منتشر شده را بازیابی می کند.', + }, + errorMessage: { + notSelectModel: 'لطفا یک مدل را انتخاب کنید', + waitForResponse: 'لطفا منتظر بمانید تا پاسخ به پیام قبلی کامل شود.', + queryRequired: 'درخواست متن الزامی است.', + waitForFileUpload: 'لطفا منتظر بمانید تا فایل/فایل ها آپلود شوند', + waitForImgUpload: 'لطفا منتظر بمانید تا تصویر آپلود شود', + waitForBatchResponse: 'لطفا منتظر بمانید تا پاسخ به کار دسته ای تکمیل شود.', + }, + warningMessage: { + timeoutExceeded: 'نتایج به دلیل مهلت زمانی نمایش داده نمی شوند. لطفا برای جمع آوری نتایج کامل به گزارش ها مراجعه کنید.', + }, + variableTable: { + key: 'کلید متغیر', + typeSelect: 'انتخاب', + action: 'اقدامات', + typeString: 'رشته', + name: 'نام فیلد ورودی کاربر', + type: 'نوع ورودی', + optional: 'اختیاری', + }, + varKeyError: {}, + otherError: { + promptNoBeEmpty: 'اعلان نمی تواند خالی باشد', + historyNoBeEmpty: 'سابقه مکالمه باید در اعلان تنظیم شود', + queryNoBeEmpty: 'پرس و جو باید در اعلان تنظیم شود', + }, + variableConfig: { + 'file': { + image: { + name: 'تصویر', + }, + audio: { + name: 'صوتی', + }, + document: { + name: 'سند', + }, + video: { + name: 'ویدئو', + }, + custom: { + name: 'انواع فایل های دیگر', + createPlaceholder: ' پسوند فایل، به عنوان مثال .doc', + description: 'انواع فایل های دیگر را مشخص کنید.', + }, + supportFileTypes: 'انواع فایل های پشتیبانی', + }, + 'errorMsg': { + optionRepeat: 'دارای گزینه های تکرار', + varNameCanBeRepeat: 'نام متغیر را نمی توان تکرار کرد', + labelNameRequired: 'نام برچسب الزامی است', + atLeastOneOption: 'حداقل یک گزینه مورد نیاز است', + }, + 'number': 'شماره', + 'hide': 'مخفی کردن', + 'both': 'هر دو', + 'single-file': 'تک فایل', + 'select': 'انتخاب', + 'inputPlaceholder': 'لطفا وارد کنید', + 'editModalTitle': 'ویرایش فیلد ورودی', + 'paragraph': 'پاراگراف', + 'fieldType': 'نوع فیلد', + 'maxLength': 'حداکثر طول', + 'varName': 'نام متغیر', + 'maxNumberOfUploads': 'حداکثر تعداد آپلود', + 'noDefaultValue': 'بدون مقدار پیش فرض', + 'addOption': 'افزودن گزینه', + 'stringTitle': 'گزینه های جعبه متن فرم', + 'options': 'گزینه', + 'selectDefaultValue': 'مقدار پیش فرض را انتخاب کنید', + 'content': 'محتوای', + 'multi-files': 'لیست فایل ها', + 'labelName': 'نام برچسب', + 'defaultValue': 'مقدار پیش فرض', + 'required': 'مورد نیاز', + 'uploadFileTypes': 'آپلود انواع فایل', + 'apiBasedVar': 'متغیر مبتنی بر API', + 'addModalTitle': 'افزودن فیلد ورودی', + 'string': 'متن کوتاه', + 'text-input': 'متن کوتاه', + 'localUpload': 'آپلود محلی', + }, + vision: { + visionSettings: { + url: 'آدرس', + high: 'بالا', + low: 'کم', + resolution: 'وضوح', + uploadLimit: 'محدودیت آپلود', + uploadMethod: 'روش آپلود', + localUpload: 'آپلود محلی', + title: 'تنظیمات بینایی', + both: 'هر دو', + }, + settings: 'تنظیمات', + name: 'چشم انداز', + onlySupportVisionModelTip: 'فقط از مدل های بینایی پشتیبانی می کند', + description: 'Enable Vision به مدل اجازه می دهد تا تصاویر را بگیرد و به سؤالات مربوط به آنها پاسخ دهد.', + }, + voice: { + voiceSettings: { + voice: 'صوتی', + autoPlayEnabled: 'در', + autoPlayDisabled: 'خاموش', + language: 'زبان', + title: 'تنظیمات صدا', + resolutionTooltip: 'زبان پشتیبانی از صدای متن به گفتار。', + autoPlay: 'پخش خودکار', + }, + settings: 'تنظیمات', + name: 'صوتی', + description: 'متن به گفتار به گفتار تنظیمات', + defaultDisplay: 'صدای پیش فرض', + }, + openingStatement: { + tooShort: 'حداقل 20 کلمه درخواست اولیه برای ایجاد یک سخنرانی آغازین برای مکالمه مورد نیاز است.', + writeOpener: 'ویرایش بازکن', + add: 'اضافه کردن', + title: 'افتتاحیه مکالمه', + openingQuestion: 'سوالات آغازین', + noDataPlaceHolder: 'شروع مکالمه با کاربر می تواند به هوش مصنوعی کمک کند تا در برنامه های مکالمه ارتباط نزدیک تری با آنها برقرار کند.', + }, + modelConfig: { + modeType: { + chat: 'چت', + completion: 'کامل', + }, + model: 'مدل', + title: 'مدل و پارامترها', + setTone: 'لحن پاسخ ها را تنظیم کنید', + }, + inputs: { + run: 'اجرا', + queryTitle: 'محتوای پرس و جو', + userInputField: 'فیلد ورودی کاربر', + previewTitle: 'پیش نمایش سریع', + title: 'اشکال زدایی و پیش نمایش', + queryPlaceholder: 'لطفا متن درخواست را وارد کنید.', + noPrompt: 'سعی کنید مقداری اعلان در ورودی پیش از اعلان بنویسید', + completionVarTip: 'مقدار متغیر را پر کنید، که هر بار که سوالی ارسال می شود، به طور خودکار در کلمات سریع جایگزین می شود.', + chatVarTip: 'مقدار متغیر را پر کنید، که هر بار که یک جلسه جدید شروع می شود، به طور خودکار در کلمه prompt جایگزین می شود', + noVar: 'مقدار متغیر را پر کنید، که هر بار که یک جلسه جدید شروع می شود، به طور خودکار در کلمه prompt جایگزین می شود.', + }, + datasetConfig: { + retrieveOneWay: { + title: 'بازیابی N-to-1', + description: 'بر اساس هدف کاربر و توضیحات دانش، عامل به طور مستقل بهترین دانش را برای پرس و جو انتخاب می کند. بهترین برای برنامه های کاربردی با دانش متمایز و محدود.', + }, + retrieveMultiWay: { + title: 'بازیابی چند مسیری', + description: 'بر اساس هدف کاربر، پرس و جوها را در تمام دانش انجام می دهد، متن مربوطه را از چند منبع بازیابی می کند و پس از رتبه بندی مجدد، بهترین نتایج را مطابق با پرس و جو کاربر انتخاب می کند.', + }, + top_k: 'K بالا', + knowledgeTip: 'برای افزودن دانش روی دکمه " کلیک کنید', + score_threshold: 'آستانه امتیاز', + settingTitle: 'تنظیمات بازیابی', + params: 'پارام ها', + embeddingModelRequired: 'یک مدل تعبیه پیکربندی شده مورد نیاز است', + rerankModelRequired: 'یک مدل Rerank پیکربندی شده مورد نیاز است', + score_thresholdTip: 'برای تنظیم آستانه شباهت برای فیلتر کردن تکه ها استفاده می شود.', + top_kTip: 'برای فیلتر کردن تکه هایی که بیشتر شبیه به سؤالات کاربر هستند استفاده می شود. این سیستم همچنین با توجه به max_tokens مدل انتخاب شده، مقدار Top K را به صورت پویا تنظیم می کند.', + retrieveChangeTip: 'اصلاح حالت نمایه و حالت بازیابی ممکن است بر کاربردهای مرتبط با این دانش تأثیر بگذارد.', + }, + assistantType: { + chatAssistant: { + description: 'ساخت یک دستیار مبتنی بر چت با استفاده از یک مدل زبان بزرگ', + name: 'دستیار پایه', + }, + agentAssistant: { + description: 'یک عامل هوشمند بسازید که بتواند به طور مستقل ابزارهایی را برای تکمیل تسک ها انتخاب کند.', + name: 'دستیار نماینده', + }, + name: 'نوع دستیار', + }, + agent: { + agentModeType: { + functionCall: 'فراخوانی تابع', + ReACT: 'واکنش', + }, + setting: { + maximumIterations: { + name: 'حداکثر تکرارها', + description: 'تعداد تکرارهایی را که یک دستیار عامل می تواند اجرا کند محدود کنید', + }, + name: 'تنظیمات نماینده', + description: 'تنظیمات Agent Assistant امکان تنظیم حالت عامل و ویژگی های پیشرفته مانند اعلان های داخلی را فراهم می کند که فقط در نوع Agent موجود است.', + }, + tools: { + enabled: 'فعال', + name: 'ابزار', + description: 'استفاده از ابزارها می تواند قابلیت های LLM مانند جستجو در اینترنت یا انجام محاسبات علمی را گسترش دهد', + }, + agentMode: 'حالت عامل', + nextIteration: 'تکرار بعدی', + promptPlaceholder: 'درخواست خود را اینجا بنویسید', + agentModeDes: 'نوع حالت استنتاج را برای عامل تنظیم کنید', + buildInPrompt: 'اعلان داخلی', + firstPrompt: 'اولین اعلان', + }, + result: 'متن خروجی', + completionSubTitle: 'اعلان پیشوند', + variableTitle: 'متغیرهای', + formattingChangedTitle: 'قالب بندی تغییر کرد', + chatSubTitle: 'دستورالعمل', + debugAsSingleModel: 'اشکال زدایی به عنوان مدل واحد', + publishAs: 'انتشار به عنوان', + duplicateModel: 'تکراری', + noResult: 'خروجی در اینجا نمایش داده می شود.', + debugAsMultipleModel: 'اشکال زدایی به عنوان چندین مدل', + formattingChangedText: 'با تغییر قالب بندی، ناحیه اشکال زدایی بازنشانی می شود، مطمئن هستید؟', + variableTip: 'کاربران متغیرها را در یک فرم پر می کنند و به طور خودکار متغیرها را در اعلان جایگزین می کنند.', + autoAddVar: 'متغیرهای تعریف نشده که در پیش اعلان ارجاع داده شده اند، آیا می خواهید آنها را به صورت ورودی کاربر اضافه کنید؟', } export default translation diff --git a/web/i18n/fa-IR/app.ts b/web/i18n/fa-IR/app.ts index b2cde413d9..e28aa1946c 100644 --- a/web/i18n/fa-IR/app.ts +++ b/web/i18n/fa-IR/app.ts @@ -31,21 +31,7 @@ const translation = { newApp: { startFromBlank: 'ایجاد از خالی', startFromTemplate: 'ایجاد از قالب', - captionAppType: 'چه نوع برنامه‌ای می‌خواهید ایجاد کنید؟', - chatbotDescription: 'ساخت برنامه‌ای مبتنی بر چت. این برنامه از قالب پرسش و پاسخ استفاده می‌کند و امکان چندین دور مکالمه مداوم را فراهم می‌کند.', - completionDescription: 'ساخت برنامه‌ای که متن با کیفیت بالا بر اساس درخواست‌ها تولید می‌کند، مانند تولید مقالات، خلاصه‌ها، ترجمه‌ها و بیشتر.', - completionWarning: 'این نوع برنامه دیگر پشتیبانی نمی‌شود.', - agentDescription: 'ساخت نماینده هوشمند که می‌تواند ابزارها را برای انجام وظایف به طور خودمختار انتخاب کند', - workflowDescription: 'ساخت برنامه‌ای که متن با کیفیت بالا بر اساس گردش کار با درجه بالای سفارشی‌سازی تولید می‌کند. مناسب برای کاربران با تجربه.', workflowWarning: 'در حال حاضر در نسخه بتا', - chatbotType: 'روش سازماندهی چت‌بات', - basic: 'اساسی', - basicTip: 'برای مبتدیان، می‌توان بعداً به Chatflow تغییر داد', - basicFor: 'برای مبتدیان', - basicDescription: 'سازماندهی اساسی به شما اجازه می‌دهد تا یک برنامه چت‌بات را با تنظیمات ساده و بدون امکان تغییر درخواست‌های داخلی سازماندهی کنید. مناسب برای مبتدیان است.', - advanced: 'Chatflow', - advancedFor: 'برای کاربران پیشرفته', - advancedDescription: 'سازماندهی گردش کار، چت‌بات‌ها را به صورت گردش کار سازماندهی می‌کند و درجه بالایی از سفارشی‌سازی، از جمله امکان ویرایش درخواست‌های داخلی را فراهم می‌کند. مناسب برای کاربران با تجربه است.', captionName: 'آیکون و نام برنامه', appNamePlaceholder: 'به برنامه خود یک نام بدهید', captionDescription: 'توضیحات', diff --git a/web/i18n/fa-IR/dataset-documents.ts b/web/i18n/fa-IR/dataset-documents.ts index cdd14d3881..b9d76e5828 100644 --- a/web/i18n/fa-IR/dataset-documents.ts +++ b/web/i18n/fa-IR/dataset-documents.ts @@ -31,6 +31,7 @@ const translation = { sync: 'همگام‌سازی', resume: 'ادامه', pause: 'مکث', + download: 'دانلود فایل', }, index: { enable: 'فعال کردن', diff --git a/web/i18n/fa-IR/login.ts b/web/i18n/fa-IR/login.ts index da2e5197eb..2b6098b95e 100644 --- a/web/i18n/fa-IR/login.ts +++ b/web/i18n/fa-IR/login.ts @@ -9,7 +9,6 @@ const translation = { namePlaceholder: 'نام کاربری شما', forget: 'رمز عبور خود را فراموش کرده‌اید؟', signBtn: 'ورود', - sso: 'ادامه با SSO', installBtn: 'راه‌اندازی', setAdminAccount: 'راه‌اندازی حساب مدیر', setAdminAccountDesc: 'بیشترین امتیازات برای حساب مدیر، که می‌تواند برای ایجاد برنامه‌ها و مدیریت ارائه‌دهندگان LLM و غیره استفاده شود.', @@ -81,8 +80,8 @@ const translation = { useAnotherMethod: 'از روش دیگری استفاده کنید', checkYourEmail: 'ایمیل خود را بررسی کنید', validTime: 'به خاطر داشته باشید که کد 5 دقیقه اعتبار دارد', - tips: 'کد درستی سنجی را به {{email}} ارسال می کنیم', resend: 'ارسال مجدد', + tipsPrefix: 'ما یک کد تأیید می‌فرستیم به ', }, or: 'یا', back: 'بازگشت', diff --git a/web/i18n/fa-IR/workflow.ts b/web/i18n/fa-IR/workflow.ts index 2f08183151..b3de497c2f 100644 --- a/web/i18n/fa-IR/workflow.ts +++ b/web/i18n/fa-IR/workflow.ts @@ -930,6 +930,7 @@ const translation = { deleteFailure: 'حذف نسخه موفق نبود', restoreFailure: 'بازگرداندن نسخه ناموفق بود', updateFailure: 'به‌روزرسانی نسخه ناموفق بود', + copyIdSuccess: 'شناسه در کلیپ بورد کپی شده است', }, latest: 'آخرین', editVersionInfo: 'ویرایش اطلاعات نسخه', @@ -940,6 +941,7 @@ const translation = { releaseNotesPlaceholder: 'شرح دهید چه چیزی تغییر کرده است', restorationTip: 'پس از بازیابی نسخه، پیش‌نویس فعلی بازنویسی خواهد شد.', deletionTip: 'حذف غیرقابل برگشت است، لطفا تأیید کنید.', + copyId: 'شناسه کپی', }, debug: { noData: { diff --git a/web/i18n/fr-FR/app-annotation.ts b/web/i18n/fr-FR/app-annotation.ts index 3a34e326f4..648a1b93cc 100644 --- a/web/i18n/fr-FR/app-annotation.ts +++ b/web/i18n/fr-FR/app-annotation.ts @@ -83,6 +83,16 @@ const translation = { configConfirmBtn: 'Enregistrer', }, embeddingModelSwitchTip: 'Modèle de vectorisation de texte d\'annotation, changer de modèles entraînera une ré-intégration, ce qui entraînera des coûts supplémentaires.', + list: { + delete: { + title: 'Êtes-vous sûr de vouloir supprimer ?', + }, + }, + batchAction: { + cancel: 'Annuler', + delete: 'Supprimer', + selected: 'sélectionné', + }, } export default translation diff --git a/web/i18n/fr-FR/app-debug.ts b/web/i18n/fr-FR/app-debug.ts index 7619db0569..9bd08087df 100644 --- a/web/i18n/fr-FR/app-debug.ts +++ b/web/i18n/fr-FR/app-debug.ts @@ -197,6 +197,7 @@ const translation = { after: 'Sorry, but you didn\'t provide a text to translate. Could you please provide the text?', }, }, + contentEnableLabel: 'Activation du contenu modéré', }, fileUpload: { title: 'Téléchargement de fichier', @@ -241,6 +242,7 @@ const translation = { 'Veuillez attendre que la réponse à la tâche en lot soit terminée.', notSelectModel: 'Veuillez choisir un modèle', waitForImgUpload: 'Veuillez attendre que l\'image soit téléchargée', + waitForFileUpload: 'Veuillez patienter jusqu’à ce que le(s) fichier(s) soit/les fichiers à télécharger', }, chatSubTitle: 'Instructions', completionSubTitle: 'Indicatif de Prompt', @@ -305,6 +307,33 @@ const translation = { 'defaultValue': 'Valeur par défaut', 'noDefaultValue': 'Aucune valeur par défaut', 'selectDefaultValue': 'Sélectionner la valeur par défaut', + 'file': { + image: { + name: 'Image', + }, + audio: { + name: 'Audio', + }, + document: { + name: 'Document', + }, + video: { + name: 'Vidéo', + }, + custom: { + description: 'Spécifiez d’autres types de fichiers.', + name: 'Autres types de fichiers', + createPlaceholder: ' Extension de fichier, par exemple .doc', + }, + supportFileTypes: 'Types de fichiers de support', + }, + 'content': 'Contenu', + 'uploadFileTypes': 'Types de fichiers de téléchargement', + 'multi-files': 'Liste des fichiers', + 'both': 'Les deux', + 'maxNumberOfUploads': 'Nombre maximal de téléchargements', + 'localUpload': 'Téléchargement local', + 'single-file': 'En file indienne', }, vision: { name: 'Vision', @@ -324,6 +353,7 @@ const translation = { url: 'URL', uploadLimit: 'Limite de téléchargement', }, + onlySupportVisionModelTip: 'Ne prend en charge que les modèles de vision', }, voice: { name: 'Voix', @@ -395,6 +425,7 @@ const translation = { score_threshold: 'Seuil de Score', score_thresholdTip: 'Utilisé pour définir le seuil de similarité pour le filtrage des morceaux.', retrieveChangeTip: 'La modification du mode d\'indexation et du mode de récupération peut affecter les applications associées à cette Connaissance.', + embeddingModelRequired: 'Un modèle d’incorporation configuré est requis', }, debugAsSingleModel: 'Déboguer comme Modèle Unique', debugAsMultipleModel: 'Déboguer en tant que Modèles Multiples', @@ -436,6 +467,79 @@ const translation = { enabled: 'Activé', }, }, + codegen: { + noDataLine1: 'Décrivez votre cas d’utilisation sur la gauche,', + instruction: 'Instructions', + generate: 'Générer', + noDataLine2: 'L’aperçu du code s’affichera ici.', + resTitle: 'Code généré', + applyChanges: 'Appliquer les modifications', + overwriteConfirmTitle: 'Écraser le code existant ?', + description: 'Le générateur de code utilise des modèles configurés pour générer un code de haute qualité basé sur vos instructions. Veuillez fournir des instructions claires et détaillées.', + loading: 'Génération de code...', + overwriteConfirmMessage: 'Cette action remplacera le code existant. Voulez-vous continuer ?', + generatedCodeTitle: 'Code généré', + apply: 'Appliquer', + title: 'Générateur de code', + instructionPlaceholder: 'Entrez une description détaillée du code que vous souhaitez générer.', + }, + generate: { + template: { + pythonDebugger: { + name: 'Débogueur Python', + instruction: 'Un bot capable de générer et de déboguer votre code en fonction de vos instructions', + }, + translation: { + name: 'Traduction', + instruction: 'Un traducteur capable de traduire en plusieurs langues', + }, + professionalAnalyst: { + instruction: 'Extrayez des informations, identifiez les risques et distillez les informations clés des rapports longs dans un seul mémo', + name: 'Analyste professionnel', + }, + excelFormulaExpert: { + name: 'Expert en formules Excel', + instruction: 'Un chatbot qui peut aider les utilisateurs novices à comprendre, utiliser et créer des formules Excel basées sur les instructions de l’utilisateur', + }, + travelPlanning: { + instruction: 'L’assistant de planification de voyage est un outil intelligent conçu pour aider les utilisateurs à planifier sans effort leurs voyages', + name: 'Planification de voyage', + }, + SQLSorcerer: { + instruction: 'Transformez le langage quotidien en requêtes SQL', + name: 'Sorcier SQL', + }, + GitGud: { + name: 'Git gud', + instruction: 'Générer des commandes Git appropriées en fonction des actions de contrôle de version décrites par l’utilisateur', + }, + meetingTakeaways: { + name: 'Points à retenir de la réunion', + instruction: 'Distillez les réunions en résumés concis comprenant les sujets de discussion, les points clés à retenir et les actions à prendre', + }, + writingsPolisher: { + name: 'Polisseuse d’écriture', + instruction: 'Utilisez des techniques de révision avancées pour améliorer vos écrits', + }, + }, + instruction: 'Instructions', + generate: 'Générer', + tryIt: 'Essaie', + overwriteTitle: 'Remplacer la configuration existante ?', + noDataLine2: 'L’aperçu de l’orchestration s’affichera ici.', + overwriteMessage: 'L’application de cette invite remplacera la configuration existante.', + noDataLine1: 'Décrivez votre cas d’utilisation sur la gauche,', + instructionPlaceHolder: 'Rédigez des instructions claires et précises.', + title: 'Générateur d’invites', + apply: 'Appliquer', + resTitle: 'Invite générée', + loading: 'Orchestrer l’application pour vous...', + description: 'Le générateur d’invites utilise le modèle configuré pour optimiser les invites afin d’obtenir une meilleure qualité et une meilleure structure. Veuillez rédiger des instructions claires et détaillées.', + }, + warningMessage: { + timeoutExceeded: 'Les résultats ne s’affichent pas en raison d’un délai d’expiration. Veuillez vous référer aux journaux pour rassembler les résultats complets.', + }, + noResult: 'La sortie sera affichée ici.', } export default translation diff --git a/web/i18n/fr-FR/app.ts b/web/i18n/fr-FR/app.ts index f572658d12..a34d6a31da 100644 --- a/web/i18n/fr-FR/app.ts +++ b/web/i18n/fr-FR/app.ts @@ -27,21 +27,7 @@ const translation = { newApp: { startFromBlank: 'Créer à partir de zéro', startFromTemplate: 'Créer à partir d\'un modèle', - captionAppType: 'Quel type d\'application souhaitez-vous créer ?', - chatbotDescription: 'Construisez une application basée sur le chat. Cette application utilise un format question-réponse, permettant ainsi plusieurs tours de conversation continue.', - completionDescription: 'Construisez une application qui génère du texte de haute qualité en fonction des invites, telles que la génération d\'articles, de résumés, de traductions, et plus encore.', - completionWarning: 'Ce type d\'application ne sera plus pris en charge.', - agentDescription: 'Construisez un agent intelligent capable de choisir automatiquement les outils pour accomplir les tâches', - workflowDescription: 'Construisez une application qui génère du texte de haute qualité en fonction d\'un flux de travail avec un haut degré de personnalisation. Il convient aux utilisateurs expérimentés.', workflowWarning: 'Actuellement en version bêta', - chatbotType: 'Méthode d\'orchestration du chatbot', - basic: 'Basique', - basicTip: 'Pour les débutants, peut passer à Chatflow plus tard', - basicFor: 'POUR LES DÉBUTANTS', - basicDescription: 'L\'orchestration de base permet d\'orchestrer une application Chatbot à l\'aide de paramètres simples, sans possibilité de modifier les invites intégrées. Il convient aux débutants.', - advanced: 'Chatflow', - advancedFor: 'Pour les utilisateurs avancés', - advancedDescription: 'L\'orchestration de flux de travail orchestre les Chatbots sous forme de workflows, offrant un haut degré de personnalisation, y compris la possibilité de modifier les invites intégrées. Il convient aux utilisateurs expérimentés.', captionName: 'Icône et nom de l\'application', appNamePlaceholder: 'Donnez un nom à votre application', captionDescription: 'Description', diff --git a/web/i18n/fr-FR/dataset-documents.ts b/web/i18n/fr-FR/dataset-documents.ts index debb03a379..6a844129f6 100644 --- a/web/i18n/fr-FR/dataset-documents.ts +++ b/web/i18n/fr-FR/dataset-documents.ts @@ -30,6 +30,7 @@ const translation = { sync: 'Synchroniser', pause: 'Pause', resume: 'Reprendre', + download: 'Télécharger le fichier', }, index: { enable: 'Activer', diff --git a/web/i18n/fr-FR/login.ts b/web/i18n/fr-FR/login.ts index 9e718cad2d..38b8159158 100644 --- a/web/i18n/fr-FR/login.ts +++ b/web/i18n/fr-FR/login.ts @@ -70,7 +70,6 @@ const translation = { activated: 'Connectez-vous maintenant', adminInitPassword: 'Mot de passe d\'initialisation de l\'administrateur', validate: 'Valider', - sso: 'Poursuivre avec l’authentification unique', checkCode: { verificationCode: 'Code de vérification', useAnotherMethod: 'Utiliser une autre méthode', @@ -82,7 +81,7 @@ const translation = { invalidCode: 'Code non valide', checkYourEmail: 'Vérifiez vos e-mails', validTime: 'Gardez à l’esprit que le code est valable 5 minutes', - tips: 'Nous envoyons un code de vérification à {{email}}', + tipsPrefix: 'Nous envoyons un code de vérification à', }, sendVerificationCode: 'Envoyer le code de vérification', or: 'OU', diff --git a/web/i18n/fr-FR/workflow.ts b/web/i18n/fr-FR/workflow.ts index 5e53a8b4ae..adc3eb125c 100644 --- a/web/i18n/fr-FR/workflow.ts +++ b/web/i18n/fr-FR/workflow.ts @@ -930,6 +930,7 @@ const translation = { deleteSuccess: 'Version supprimée', updateFailure: 'Échec de la mise à jour de la version', restoreFailure: 'Échec de la restauration de la version', + copyIdSuccess: 'ID copié dans le presse-papiers', }, title: 'Versions', releaseNotesPlaceholder: 'Décrivez ce qui a changé', @@ -940,6 +941,7 @@ const translation = { restorationTip: 'Après la restauration de la version, le brouillon actuel sera écrasé.', deletionTip: 'La suppression est irreversible, veuillez confirmer.', latest: 'Dernier', + copyId: 'Copier l’ID', }, debug: { noData: { diff --git a/web/i18n/hi-IN/app-annotation.ts b/web/i18n/hi-IN/app-annotation.ts index b89f33c438..51eb14cc55 100644 --- a/web/i18n/hi-IN/app-annotation.ts +++ b/web/i18n/hi-IN/app-annotation.ts @@ -83,6 +83,16 @@ const translation = { configConfirmBtn: 'सहेजें', }, embeddingModelSwitchTip: 'एनोटेशन टेक्स्ट वेक्टराइजेशन मॉडल, मॉडल बदलने से पुनः एम्बेड किया जाएगा, जिससे अतिरिक्त लागतें उत्पन्न होंगी।', + list: { + delete: { + title: 'क्या आप सुनिश्चित हैं कि हटाएं?', + }, + }, + batchAction: { + selected: 'चुना हुआ', + delete: 'हटाएँ', + cancel: 'रद्द करें', + }, } export default translation diff --git a/web/i18n/hi-IN/app-debug.ts b/web/i18n/hi-IN/app-debug.ts index ea9b20c500..04b50da9ed 100644 --- a/web/i18n/hi-IN/app-debug.ts +++ b/web/i18n/hi-IN/app-debug.ts @@ -213,6 +213,34 @@ const translation = { after: 'में कॉन्फ़िगर किए गए ओपनएआई एपीआई कुंजी की आवश्यकता होती है।', }, }, + contentEnableLabel: 'मध्य स्तर की सामग्री सक्षम की गई', + }, + fileUpload: { + numberLimit: 'मैक्स अपलोड करता है', + title: 'फ़ाइल अपलोड', + modalTitle: 'फ़ाइल अपलोड सेटिंग', + description: 'चैट इनपुट बॉक्स छवियों, दस्तावेजों और अन्य फ़ाइलों को अपलोड करने की अनुमति देता है।', + supportedTypes: 'फ़ाइल प्रकारों का समर्थन करें', + }, + imageUpload: { + supportedTypes: 'फ़ाइल प्रकारों का समर्थन करें', + modalTitle: 'छवि अपलोड सेटिंग', + title: 'छवि अपलोड', + description: 'छवियों को अपलोड करने की अनुमति दें।', + numberLimit: 'मैक्स अपलोड करता है', + }, + bar: { + manage: 'प्रबंधित करें', + enableText: 'विशेषताएँ सक्षम हैं', + empty: 'वेब ऐप उपयोगकर्ता अनुभव को बढ़ाने के लिए फ़ीचर सक्षम करें', + }, + documentUpload: { + title: 'दस्तावेज़', + description: 'डॉक्यूमेंट सक्षम करने से मॉडल को दस्तावेज़ प्राप्त करने और उनके बारे में प्रश्नों का उत्तर देने की अनुमति मिलेगी।', + }, + audioUpload: { + title: 'ऑडियो', + description: 'ऑडियो सक्षम करने से मॉडल को ऑडियो फ़ाइलों के ट्रांसक्रिप्शन और विश्लेषण के लिए प्रोसेस करने की अनुमति मिलेगी।', }, }, automatic: { @@ -251,6 +279,7 @@ const translation = { 'कृपया बैच कार्य की प्रतिक्रिया पूरी होने तक प्रतीक्षा करें।', notSelectModel: 'कृपया एक मॉडल चुनें', waitForImgUpload: 'कृपया छवि अपलोड होने तक प्रतीक्षा करें', + waitForFileUpload: 'कृपया फ़ाइल/फ़ाइलें अपलोड होने का इंतज़ार करें', }, chatSubTitle: 'निर्देश', completionSubTitle: 'प्रारंभिक प्रॉम्प्ट', @@ -322,6 +351,30 @@ const translation = { 'defaultValue': 'डिफ़ॉल्ट मान', 'noDefaultValue': 'कोई डिफ़ॉल्ट मान नहीं', 'selectDefaultValue': 'डिफ़ॉल्ट मान चुनें', + 'file': { + image: { + name: 'छवि', + }, + audio: { + name: 'ऑडियो', + }, + document: {}, + video: { + name: 'वीडियो', + }, + custom: { + description: 'अन्य फ़ाइल प्रकार निर्दिष्ट करें।', + name: 'अन्य फ़ाइल प्रकार', + createPlaceholder: 'फ़ाइल एक्सटेंशन, जैसे .doc', + }, + supportFileTypes: 'फ़ाइल प्रकारों का समर्थन करें', + }, + 'both': 'दोनों', + 'multi-files': 'फ़ाइल सूची', + 'single-file': 'एकल फ़ाइल', + 'maxNumberOfUploads': 'अधिकतम अपलोड संख्या', + 'uploadFileTypes': 'फ़ाइल प्रकार अपलोड करें', + 'localUpload': 'स्थानीय अपलोड', }, vision: { name: 'विजन', @@ -341,6 +394,7 @@ const translation = { url: 'यूआरएल', uploadLimit: 'अपलोड सीमा', }, + onlySupportVisionModelTip: 'केवल दृष्टि मॉडल का समर्थन करता है', }, voice: { name: 'वॉयस', @@ -423,6 +477,7 @@ const translation = { 'खंडों को फ़िल्टर करने के लिए समानता थ्रेशोल्ड सेट करने के लिए उपयोग किया जाता है।', retrieveChangeTip: 'सूचकांक मोड और पुनःप्राप्ति मोड को संशोधित करने से इस ज्ञान से जुड़े अनुप्रयोग प्रभावित हो सकते हैं।', + embeddingModelRequired: 'एक कॉन्फ़िगर किया गया एंबेडिंग मॉडल आवश्यक है', }, debugAsSingleModel: 'एकल मॉडल के रूप में डिबग करें', debugAsMultipleModel: 'एकाधिक मॉडलों के रूप में डिबग करें', @@ -495,6 +550,79 @@ const translation = { description: 'ऑडियो सक्षम करने से मॉडल ट्रांसक्रिप्शन और विश्लेषण के लिए ऑडियो फ़ाइलों को प्रोसेस कर सकेगा।', }, }, + codegen: { + title: 'कोड जनरेटर', + loading: 'कोड उत्पन्न कर रहा हूँ...', + noDataLine1: 'बाईं ओर अपने उपयोग के मामले का वर्णन करें,', + apply: 'अनुप्रयोग करें', + generate: 'जनरेट करें', + instruction: 'अनुदेश', + overwriteConfirmTitle: 'मौजूदा कोड को ओवरराइट करें?', + resTitle: 'जनरेटेड कोड', + applyChanges: 'परिवर्तन लागू करें', + noDataLine2: 'कोड पूर्वावलोकन यहाँ दिखाई देगा।', + generatedCodeTitle: 'जनरेटेड कोड', + overwriteConfirmMessage: 'यह क्रिया मौजूदा कोड को ओवरराइट कर देगी। क्या आप जारी रखना चाहते हैं?', + instructionPlaceholder: 'आप जिस कोड का निर्माण करना चाहते हैं उसका विस्तृत विवरण प्रदान करें।', + description: 'कोड जनरेटर आपके निर्देशों के आधार पर उच्च गुणवत्ता वाली कोड उत्पन्न करने के लिए कॉन्फ़िगर किए गए मॉडलों का उपयोग करता है। कृपया स्पष्ट और विस्तृत निर्देश प्रदान करें।', + }, + generate: { + template: { + pythonDebugger: { + name: 'पाइथन डिबगर', + instruction: 'एक बॉट जो आपके निर्देशों के आधार पर आपका कोड उत्पन्न और डिबग कर सकता है।', + }, + translation: { + name: 'अनुवाद', + instruction: 'एक अनुवादक जो कई भाषाओं का अनुवाद कर सकता है', + }, + professionalAnalyst: { + name: 'पेशेवर विश्लेषक', + instruction: 'दीर्घ रिपोर्ट से अंतर्दृष्टियाँ निकालें, जोखिम की पहचान करें और प्रमुख जानकारी को एकल ज्ञापन में संक्षेपित करें।', + }, + excelFormulaExpert: { + name: 'एक्सेल फॉर्मूला विशेषज्ञ', + instruction: 'एक चैटबॉट जो नए उपयोगकर्ताओं की मदद कर सकता है कि वे कैसे समझें, उपयोग करें और उपयोगकर्ता के निर्देशों के आधार पर Excel फॉर्मूले बनाएं।', + }, + travelPlanning: { + name: 'यात्रा की योजना बनाना', + instruction: 'यात्रा नियोजन सहायक एक बुद्धिमान उपकरण है जिसे उपयोगकर्ताओं को बिना किसी परेशानी के अपने यात्रा की योजना बनाने में मदद करने के लिए डिज़ाइन किया गया है', + }, + SQLSorcerer: { + instruction: 'प्रति दिन की भाषा को SQL क्वेरियों में बदलें', + name: 'SQL जादूगर', + }, + GitGud: { + name: 'अच्छा खेलो', + instruction: 'उपयोगकर्ता द्वारा वर्णित संस्करण नियंत्रण क्रियाओं के आधार पर उचित Git कमांड उत्पन्न करें', + }, + meetingTakeaways: { + name: 'बैठक के निष्कर्ष', + instruction: 'बैठकों को संक्षिप्त सारांशों में डिस्टिल करें, जिसमें चर्चा के विषय, मुख्य निष्कर्ष और कार्य के बिंदु शामिल हों।', + }, + writingsPolisher: { + name: 'लेखन पालिशर', + instruction: 'अपनी लेखन को सुधारने के लिए उन्नत संपादन तकनीकों का उपयोग करें', + }, + }, + tryIt: 'इसे आजमाओ', + generate: 'जनरेट करें', + instructionPlaceHolder: 'स्पष्ट और विशेष निर्देश लिखें।', + title: 'प्रॉम्प्ट जनरेटर', + apply: 'अनुप्रयोग करें', + noDataLine1: 'बाईं ओर अपने उपयोग केस का वर्णन करें,', + instruction: 'अनुदेश', + loading: 'आपके लिए एप्लिकेशन का आयोजन कर रहे हैं...', + overwriteTitle: 'मौजूदा कॉन्फ़िगरेशन को अधिलेखित करें?', + noDataLine2: 'यहाँ सम्प्रेषण पूर्वावलोकन दिखाया जाएगा।', + resTitle: 'जनित प्रॉम्प्ट', + overwriteMessage: 'इस प्रॉम्प्ट को लागू करने से मौजूदा कॉन्फ़िगरेशन को ओवरराइड कर दिया जाएगा।', + description: 'प्रॉम्प्ट जेनरेटर उच्च गुणवत्ता और बेहतर संरचना के लिए प्रॉम्प्ट्स को ऑप्टिमाइज़ करने के लिए कॉन्फ़िगर किए गए मॉडल का उपयोग करता है। कृपया स्पष्ट और विस्तृत निर्देश लिखें।', + }, + warningMessage: { + timeoutExceeded: 'परिणाम टाइमआउट के कारण प्रदर्शित नहीं किए गए हैं। कृपया संपूर्ण परिणामों को इकट्ठा करने के लिए लॉग्स का संदर्भ लें।', + }, + noResult: 'प्रदर्शन यहाँ होगा।', } export default translation diff --git a/web/i18n/hi-IN/app-log.ts b/web/i18n/hi-IN/app-log.ts index 746d558fb9..90bb30e621 100644 --- a/web/i18n/hi-IN/app-log.ts +++ b/web/i18n/hi-IN/app-log.ts @@ -90,12 +90,6 @@ const translation = { viewLog: 'व्यू लॉग', agentLogDetail: { agentMode: 'एजेंट मोड', - startTime: 'शुरू करने का समय', - endTime: 'समाप्ति समय', - duration: 'अवधि', - promptTemplate: 'प्रॉम्प्ट टेम्पलेट', - promptInput: 'प्रॉम्प्ट इनपुट', - response: 'प्रतिक्रिया', iterations: 'पुनरूक्तियाँ', toolUsed: 'प्रयुक्त उपकरण', finalProcessing: 'अंतिम प्रसंस्करण', diff --git a/web/i18n/hi-IN/app.ts b/web/i18n/hi-IN/app.ts index 9b13fdc392..fc60901452 100644 --- a/web/i18n/hi-IN/app.ts +++ b/web/i18n/hi-IN/app.ts @@ -27,21 +27,7 @@ const translation = { newApp: { startFromBlank: 'रिक्त से बनाएँ', startFromTemplate: 'टेम्पलेट से बनाएँ', - captionAppType: 'आप किस प्रकार का ऐप बनाना चाहते हैं?', - chatbotDescription: 'एक चैट-आधारित एप्लिकेशन बनाएं। यह ऐप प्रश्न-उत्तर प्रारूप का उपयोग करता है, जिससे निरंतर बातचीत के कई राउंड संभव होते हैं।', - completionDescription: 'ऐसा एप्लिकेशन बनाएं जो प्रॉम्प्ट्स के आधार पर उच्च गुणवत्ता वाला टेक्स्ट उत्पन्न करता है, जैसे लेख, सारांश, अनुवाद आदि उत्पन्न करना।', - completionWarning: 'इस प्रकार के ऐप का समर्थन नहीं किया जाएगा।', - agentDescription: 'एक बुद्धिमान एजेंट बनाएं जो स्वायत्त रूप से टूल्स का चयन करके कार्य पूरा कर सके।', - workflowDescription: 'एक एप्लिकेशन बनाएं जो वर्कफ़्लो ऑर्केस्ट्रेट्स के साथ उच्च डिग्री के कस्टमाइज़ेशन के साथ उच्च गुणवत्ता वाला टेक्स्ट उत्पन्न करता है। यह अनुभवी उपयोगकर्ताओं के लिए उपयुक्त है।', workflowWarning: 'वर्तमान में बीटा में', - chatbotType: 'चैटबॉट ऑर्केस्ट्रेट विधि', - basic: 'बेसिक', - basicTip: 'शुरुआती लोगों के लिए, बाद में चैटफ़्लो में स्विच कर सकते हैं', - basicFor: 'शुरुआती लोगों के लिए', - basicDescription: 'बेसिक ऑर्केस्ट्रेट चैटबॉट ऐप को सरल सेटिंग्स का उपयोग करके ऑर्केस्ट्रेट करने की अनुमति देता है, बिना अंतर्निहित प्रॉम्प्ट्स को संशोधित करने की क्षमता के। यह शुरुआती लोगों के लिए उपयुक्त है।', - advanced: 'चैटफ्लो', - advancedFor: 'अनुभवी उपयोगकर्ताओं के लिए', - advancedDescription: 'वर्कफ़्लो ऑर्केस्ट्रेट वर्कफ़्लोज़ के रूप में चैटबॉट्स को ऑर्केस्ट्रेट करता है, जिसमें अंतर्निहित प्रॉम्प्ट्स को संपादित करने की क्षमता सहित उच्च डिग्री का कस्टमाइज़ेशन होता है। यह अनुभवी उपयोगकर्ताओं के लिए उपयुक्त है।', captionName: 'ऐप आइकन और नाम', appNamePlaceholder: 'अपने ऐप को नाम दें', captionDescription: 'विवरण', diff --git a/web/i18n/hi-IN/dataset-documents.ts b/web/i18n/hi-IN/dataset-documents.ts index 3a4930e04b..15a42b1b50 100644 --- a/web/i18n/hi-IN/dataset-documents.ts +++ b/web/i18n/hi-IN/dataset-documents.ts @@ -31,6 +31,7 @@ const translation = { sync: 'सिंक्रोनाइज़ करें', resume: 'रिज़्यूमे', pause: 'रोकें', + download: 'फ़ाइल डाउनलोड करें', }, index: { enable: 'सक्रिय करें', diff --git a/web/i18n/hi-IN/login.ts b/web/i18n/hi-IN/login.ts index 06019042b5..e89cea327a 100644 --- a/web/i18n/hi-IN/login.ts +++ b/web/i18n/hi-IN/login.ts @@ -9,7 +9,6 @@ const translation = { namePlaceholder: 'आपका उपयोगकर्ता नाम', forget: 'क्या आप पासवर्ड भूल गए?', signBtn: 'साइन इन करें', - sso: 'SSO के साथ जारी रखें', installBtn: 'सेट अप करें', setAdminAccount: 'एडमिन खाता सेट कर रहे हैं', setAdminAccountDesc: @@ -86,8 +85,8 @@ const translation = { resend: 'भेजें', checkYourEmail: 'अपना ईमेल जांचें', validTime: 'ध्यान रखें कि कोड 5 मिनट के लिए वैध है', - tips: 'हम {{email}} को एक सत्यापन कोड भेजते हैं', verificationCodePlaceholder: '6-अंक कोड दर्ज करें', + tipsPrefix: 'हम एक सत्यापन कोड भेजते हैं', }, sendVerificationCode: 'पुष्टि कोड भेजें', or: 'नहीं तो', diff --git a/web/i18n/hi-IN/workflow.ts b/web/i18n/hi-IN/workflow.ts index 95ccead15f..923abfaeb5 100644 --- a/web/i18n/hi-IN/workflow.ts +++ b/web/i18n/hi-IN/workflow.ts @@ -950,6 +950,7 @@ const translation = { updateSuccess: 'संस्करण अपडेट किया गया', updateFailure: 'संस्करण अपडेट करने में विफल', restoreFailure: 'संस्करण को पुनर्स्थापित करने में विफल', + copyIdSuccess: 'आईडी क्लिपबोर्ड पर कॉपी हो गई', }, latest: 'लेटेस्ट', editVersionInfo: 'संस्करण की जानकारी संपादित करें', @@ -960,6 +961,7 @@ const translation = { restorationTip: 'संस्करण पुनर्स्थापन के बाद, वर्तमान ड्राफ्ट अधिलेखित किया जाएगा।', defaultName: 'अविभाजित संस्करण', deletionTip: 'हटाना अप्रतिबंधी है, कृपया पुष्टि करें।', + copyId: 'आईडी कॉपी करें', }, debug: { noData: { diff --git a/web/i18n/it-IT/app-annotation.ts b/web/i18n/it-IT/app-annotation.ts index bba10ba84e..288068c78e 100644 --- a/web/i18n/it-IT/app-annotation.ts +++ b/web/i18n/it-IT/app-annotation.ts @@ -85,6 +85,16 @@ const translation = { }, embeddingModelSwitchTip: 'Modello di vettorizzazione del testo di annotazione, il cambio di modello comporterà una nuova integrazione, comportando costi aggiuntivi.', + list: { + delete: { + title: 'Sei sicuro di voler eliminare?', + }, + }, + batchAction: { + delete: 'Elimina', + cancel: 'Annulla', + selected: 'selezionato', + }, } export default translation diff --git a/web/i18n/it-IT/app-debug.ts b/web/i18n/it-IT/app-debug.ts index f79cccf6e7..9344c88976 100644 --- a/web/i18n/it-IT/app-debug.ts +++ b/web/i18n/it-IT/app-debug.ts @@ -215,6 +215,7 @@ const translation = { after: '', }, }, + contentEnableLabel: 'Abilitato il contenuto moderato', }, fileUpload: { title: 'Caricamento File', @@ -280,6 +281,7 @@ const translation = { 'Per favore attendi che la risposta all\'attività batch sia completata.', notSelectModel: 'Per favore scegli un modello', waitForImgUpload: 'Per favore attendi il caricamento dell\'immagine', + waitForFileUpload: 'Attendi il caricamento del file o dei file', }, chatSubTitle: 'Istruzioni', completionSubTitle: 'Prompt di prefisso', @@ -351,6 +353,32 @@ const translation = { 'defaultValue': 'Valore predefinito', 'noDefaultValue': 'Nessun valore predefinito', 'selectDefaultValue': 'Seleziona valore predefinito', + 'file': { + image: { + name: 'Immagine', + }, + audio: { + name: 'Audio', + }, + document: { + name: 'Documento', + }, + video: { + name: 'Video', + }, + custom: { + createPlaceholder: ' Estensione del file, ad esempio .doc', + description: 'Specificare altri tipi di file.', + name: 'Altri tipi di file', + }, + supportFileTypes: 'Tipi di file di supporto', + }, + 'single-file': 'File singolo', + 'uploadFileTypes': 'Caricare i tipi di file', + 'maxNumberOfUploads': 'Numero massimo di caricamenti', + 'multi-files': 'Elenco file', + 'both': 'Ambedue', + 'localUpload': 'Caricamento locale', }, vision: { name: 'Visione', @@ -371,6 +399,7 @@ const translation = { url: 'URL', uploadLimit: 'Limite di caricamento', }, + onlySupportVisionModelTip: 'Supporta solo i modelli di visione', }, voice: { name: 'Voce', @@ -451,6 +480,7 @@ const translation = { 'Usato per impostare la soglia di somiglianza per il filtraggio dei chunk.', retrieveChangeTip: 'Modificare la modalità di indicizzazione e la modalità di recupero può influenzare le applicazioni associate a questa Conoscenza.', + embeddingModelRequired: 'È necessario un modello di incorporamento configurato', }, debugAsSingleModel: 'Debug come modello singolo', debugAsMultipleModel: 'Debug come modelli multipli', @@ -497,6 +527,79 @@ const translation = { enabled: 'Abilitato', }, }, + codegen: { + noDataLine1: 'Descrivi il tuo caso d\'uso a sinistra,', + noDataLine2: 'L\'anteprima del codice verrà mostrata qui.', + generate: 'Generare', + resTitle: 'Codice generato', + overwriteConfirmTitle: 'Sovrascrivere il codice esistente?', + applyChanges: 'Applica modifiche', + title: 'Generatore di codice', + overwriteConfirmMessage: 'Questa azione sovrascriverà il codice esistente. Vuoi continuare?', + description: 'Il generatore di codice utilizza modelli configurati per generare codice di alta qualità in base alle istruzioni dell\'utente. Si prega di fornire istruzioni chiare e dettagliate.', + instruction: 'Disposizioni', + instructionPlaceholder: 'Inserisci una descrizione dettagliata del codice che desideri generare.', + generatedCodeTitle: 'Codice generato', + loading: 'Generazione del codice...', + apply: 'Applicare', + }, + generate: { + template: { + pythonDebugger: { + instruction: 'Un bot in grado di generare ed eseguire il debug del codice in base alle istruzioni', + name: 'Debugger Python', + }, + translation: { + instruction: 'Un traduttore in grado di tradurre in più lingue', + name: 'Traduzione', + }, + professionalAnalyst: { + name: 'Analista professionista', + instruction: 'Estrai informazioni, identifica i rischi e distilla le informazioni chiave da report lunghi in un unico memo', + }, + excelFormulaExpert: { + name: 'Esperto di formule per Excel', + instruction: 'Un chatbot che può aiutare gli utenti inesperti a comprendere, utilizzare e creare formule Excel basate sulle istruzioni dell\'utente', + }, + travelPlanning: { + name: 'Pianificazione del viaggio', + instruction: 'Il Travel Planning Assistant è uno strumento intelligente progettato per aiutare gli utenti a pianificare facilmente i loro viaggi', + }, + SQLSorcerer: { + name: 'Stregone SQL', + instruction: 'Trasforma il linguaggio di tutti i giorni in query SQL', + }, + GitGud: { + instruction: 'Generare comandi Git appropriati in base alle azioni di controllo della versione descritte dall\'utente', + name: 'Git gud', + }, + meetingTakeaways: { + name: 'Conclusioni sulle riunioni', + instruction: 'Distilla le riunioni in riassunti concisi che includono argomenti di discussione, punti chiave e punti d\'azione', + }, + writingsPolisher: { + name: 'Lucidatrice per scrittura', + instruction: 'Usa tecniche avanzate di copyediting per migliorare i tuoi scritti', + }, + }, + instruction: 'Disposizioni', + noDataLine1: 'Descrivi il tuo caso d\'uso a sinistra,', + title: 'Generatore di prompt', + instructionPlaceHolder: 'Scrivi istruzioni chiare e specifiche.', + loading: 'Orchestrare l\'applicazione per te...', + apply: 'Applicare', + overwriteMessage: 'L\'applicazione di questo prompt sovrascriverà la configurazione esistente.', + description: 'Il generatore di prompt utilizza il modello configurato per ottimizzare i prompt per una qualità superiore e una struttura migliore. Si prega di scrivere istruzioni chiare e dettagliate.', + overwriteTitle: 'Sovrascrivere la configurazione esistente?', + resTitle: 'Prompt generato', + generate: 'Generare', + noDataLine2: 'L\'anteprima dell\'orchestrazione verrà visualizzata qui.', + tryIt: 'Provalo', + }, + warningMessage: { + timeoutExceeded: 'I risultati non vengono visualizzati a causa del timeout. Si prega di fare riferimento ai registri per raccogliere risultati completi.', + }, + noResult: 'L\'output verrà visualizzato qui.', } export default translation diff --git a/web/i18n/it-IT/app.ts b/web/i18n/it-IT/app.ts index 66cb50b2a0..01ee29423e 100644 --- a/web/i18n/it-IT/app.ts +++ b/web/i18n/it-IT/app.ts @@ -27,27 +27,7 @@ const translation = { newApp: { startFromBlank: 'Crea da zero', startFromTemplate: 'Crea da modello', - captionAppType: 'Che tipo di app vuoi creare?', - chatbotDescription: - 'Crea un\'applicazione basata sulla chat. Questa app utilizza un formato domanda-e-risposta, consentendo più round di conversazione continua.', - completionDescription: - 'Crea un\'applicazione che genera testo di alta qualità basato sui prompt, come articoli, riassunti, traduzioni e altro.', - completionWarning: 'Questo tipo di app non sarà più supportato.', - agentDescription: - 'Crea un Agente intelligente che può scegliere autonomamente gli strumenti per completare i compiti', - workflowDescription: - 'Crea un\'applicazione che genera testo di alta qualità basato su flussi di lavoro orchestrati con un alto grado di personalizzazione. È adatto per utenti esperti.', workflowWarning: 'Attualmente in beta', - chatbotType: 'Metodo di orchestrazione Chatbot', - basic: 'Base', - basicTip: 'Per principianti, può passare a Chatflow in seguito', - basicFor: 'PER PRINCIPIANTI', - basicDescription: - 'L\'Orchestrazione di base consente l\'orchestrazione di un\'app Chatbot utilizzando impostazioni semplici, senza la possibilità di modificare i prompt integrati. È adatta per principianti.', - advanced: 'Chatflow', - advancedFor: 'Per utenti avanzati', - advancedDescription: - 'L\'Orchestrazione del flusso di lavoro orchestra i Chatbot sotto forma di flussi di lavoro, offrendo un alto grado di personalizzazione, inclusa la possibilità di modificare i prompt integrati. È adatta per utenti esperti.', captionName: 'Icona e nome dell\'app', appNamePlaceholder: 'Dai un nome alla tua app', captionDescription: 'Descrizione', diff --git a/web/i18n/it-IT/dataset-documents.ts b/web/i18n/it-IT/dataset-documents.ts index 66eb00aafd..404fb67bf7 100644 --- a/web/i18n/it-IT/dataset-documents.ts +++ b/web/i18n/it-IT/dataset-documents.ts @@ -29,8 +29,9 @@ const translation = { delete: 'Elimina', enableWarning: 'Il file archiviato non può essere abilitato', sync: 'Sincronizza', - resume: 'Riassumere', + resume: 'Riprendi', pause: 'Pausa', + download: 'Scarica file', }, index: { enable: 'Abilita', diff --git a/web/i18n/it-IT/login.ts b/web/i18n/it-IT/login.ts index 47ae79bdd9..5009f99519 100644 --- a/web/i18n/it-IT/login.ts +++ b/web/i18n/it-IT/login.ts @@ -9,7 +9,6 @@ const translation = { namePlaceholder: 'Il tuo nome utente', forget: 'Hai dimenticato la password?', signBtn: 'Accedi', - sso: 'Continua con SSO', installBtn: 'Configura', setAdminAccount: 'Impostazione di un account amministratore', setAdminAccountDesc: @@ -91,8 +90,8 @@ const translation = { validTime: 'Tieni presente che il codice è valido per 5 minuti', didNotReceiveCode: 'Non hai ricevuto il codice?', checkYourEmail: 'Controlla la tua email', - tips: 'Inviamo un codice di verifica a {{email}}', useAnotherMethod: 'Usa un altro metodo', + tipsPrefix: 'Inviamo un codice di verifica a', }, or: 'O', back: 'Indietro', diff --git a/web/i18n/it-IT/workflow.ts b/web/i18n/it-IT/workflow.ts index ca934428a6..49b3a11e38 100644 --- a/web/i18n/it-IT/workflow.ts +++ b/web/i18n/it-IT/workflow.ts @@ -956,6 +956,7 @@ const translation = { updateSuccess: 'Versione aggiornata', deleteFailure: 'Impossibile eliminare la versione', updateFailure: 'Impossibile aggiornare la versione', + copyIdSuccess: 'ID copiato negli appunti', }, latest: 'Ultimo', defaultName: 'Versione senza titolo', @@ -966,6 +967,7 @@ const translation = { currentDraft: 'Bozza attuale', restorationTip: 'Dopo il ripristino della versione, la bozza attuale verrà sovrascritta.', title: 'Versioni', + copyId: 'Copia ID', }, debug: { noData: { diff --git a/web/i18n/ja-JP/dataset-documents.ts b/web/i18n/ja-JP/dataset-documents.ts index b2638f1b56..d22e3018ed 100644 --- a/web/i18n/ja-JP/dataset-documents.ts +++ b/web/i18n/ja-JP/dataset-documents.ts @@ -32,6 +32,7 @@ const translation = { sync: '同期', pause: '一時停止', resume: '再開', + download: 'ファイルをダウンロード', }, index: { enable: '有効にする', diff --git a/web/i18n/ja-JP/login.ts b/web/i18n/ja-JP/login.ts index 833bedf719..7c116c4c18 100644 --- a/web/i18n/ja-JP/login.ts +++ b/web/i18n/ja-JP/login.ts @@ -78,10 +78,10 @@ const translation = { didNotReceiveCode: 'コードが届きませんか?', resend: '再送', verificationCode: '認証コード', - tips: '確認コードを{{email}}に送信します。', validTime: 'コードは 5 分間有効であることに注意してください', emptyCode: 'コードが必要です', checkYourEmail: 'メールをチェックしてください', + tipsPrefix: '私たちは確認コードを送信します', }, useVerificationCode: '確認コードを使用する', or: '又は', diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index 483adb402c..59791c5c7e 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -936,7 +936,9 @@ const translation = { deleteFailure: '削除に失敗しました', updateSuccess: '更新が完了しました', updateFailure: '更新に失敗しました', + copyIdSuccess: 'IDがクリップボードにコピーされました', }, + copyId: 'IDをコピー', }, debug: { noData: { diff --git a/web/i18n/ko-KR/app-annotation.ts b/web/i18n/ko-KR/app-annotation.ts index 662dc3f083..7e0cee020b 100644 --- a/web/i18n/ko-KR/app-annotation.ts +++ b/web/i18n/ko-KR/app-annotation.ts @@ -83,6 +83,16 @@ const translation = { configConfirmBtn: '저장', }, embeddingModelSwitchTip: '어노테이션 텍스트의 임베딩 모델입니다. 모델을 변경하면 다시 임베딩되며 추가 비용이 발생합니다.', + list: { + delete: { + title: '삭제할 것인지 확실합니까?', + }, + }, + batchAction: { + cancel: '취소', + delete: '삭제', + selected: '선택됨', + }, } export default translation diff --git a/web/i18n/ko-KR/app-debug.ts b/web/i18n/ko-KR/app-debug.ts index aade904a6b..8bf509ae12 100644 --- a/web/i18n/ko-KR/app-debug.ts +++ b/web/i18n/ko-KR/app-debug.ts @@ -197,6 +197,7 @@ const translation = { after: '에 OpenAI API 키가 설정되어 있어야 합니다.', }, }, + contentEnableLabel: '중간 콘텐츠 사용', }, fileUpload: { title: '파일 업로드', @@ -240,6 +241,7 @@ const translation = { waitForBatchResponse: '배치 작업에 대한 응답이 완료될 때까지 기다려 주세요.', notSelectModel: '모델을 선택해 주세요', waitForImgUpload: '이미지 업로드가 완료될 때까지 기다려 주세요', + waitForFileUpload: '파일이 업로드될 때까지 기다리십시오.', }, chatSubTitle: '단계', completionSubTitle: '접두사 프롬프트', @@ -301,6 +303,33 @@ const translation = { 'defaultValue': '기본값', 'noDefaultValue': '기본값 없음', 'selectDefaultValue': '기본값 선택', + 'file': { + image: { + name: '이미지', + }, + audio: { + name: '오디오', + }, + document: { + name: '문서', + }, + video: { + name: '비디오', + }, + custom: { + description: '다른 파일 형식을 지정합니다.', + name: '다른 파일 형식', + createPlaceholder: ' 파일 확장자(예: .doc', + }, + supportFileTypes: '지원 파일 형식', + }, + 'content': '콘텐츠', + 'single-file': '단일 파일', + 'both': '둘다', + 'multi-files': '파일 목록', + 'uploadFileTypes': '파일 형식 업로드', + 'maxNumberOfUploads': '최대 업로드 수', + 'localUpload': '로컬 업로드', }, vision: { name: '비전', @@ -320,6 +349,7 @@ const translation = { url: 'URL', uploadLimit: '업로드 제한', }, + onlySupportVisionModelTip: '비전 모델만 지원', }, voice: { name: '음성', @@ -388,6 +418,7 @@ const translation = { score_threshold: '점수 임계값', score_thresholdTip: '청크 필터링의 유사성 임계값을 설정하는 데 사용됩니다.', retrieveChangeTip: '인덱스 모드 및 리트리벌 모드를 변경하면 이 지식과 관련된 애플리케이션에 영향을 줄 수 있습니다.', + embeddingModelRequired: '구성된 임베딩 모델이 필요합니다.', }, debugAsSingleModel: '단일 모델로 디버그', debugAsMultipleModel: '다중 모델로 디버그', @@ -429,6 +460,79 @@ const translation = { enabled: '활성화됨', }, }, + codegen: { + instruction: '지시', + apply: '적용하다', + generatedCodeTitle: '생성된 코드', + title: '코드 생성기', + applyChanges: '변경 사항 적용', + resTitle: '생성된 코드', + noDataLine1: '왼쪽에 사용 사례를 설명하십시오.', + overwriteConfirmTitle: '기존 코드를 덮어쓰시겠습니까?', + generate: '창조하다', + loading: '코드 생성 중...', + overwriteConfirmMessage: '이 작업은 기존 코드를 덮어씁니다. 계속하시겠습니까?', + noDataLine2: '코드 미리 보기가 여기에 표시됩니다.', + instructionPlaceholder: '생성하려는 코드에 대한 자세한 설명을 입력합니다.', + description: '코드 생성기는 구성된 모델을 사용하여 지시에 따라 고품질 코드를 생성합니다. 명확하고 자세한 지침을 제공하십시오.', + }, + generate: { + template: { + pythonDebugger: { + name: '파이썬 디버거', + instruction: '지시에 따라 코드를 생성하고 디버깅할 수 있는 봇', + }, + translation: { + name: '번역', + instruction: '여러 언어를 번역할 수 있는 번역기', + }, + professionalAnalyst: { + name: '전문 분석가', + instruction: '인사이트를 추출하고, 위험을 식별하고, 긴 보고서에서 주요 정보를 단일 메모로 추출합니다.', + }, + excelFormulaExpert: { + name: 'Excel 수식 전문가', + instruction: '초보 사용자가 사용자 지시에 따라 Excel 수식을 이해, 사용 및 생성할 수 있도록 도와주는 챗봇', + }, + travelPlanning: { + name: '여행 계획', + instruction: '여행 계획 도우미는 사용자가 쉽게 여행을 계획할 수 있도록 설계된 지능형 도구입니다', + }, + SQLSorcerer: { + name: 'SQL 마법사', + instruction: '일상적인 언어를 SQL 쿼리로 변환', + }, + GitGud: { + name: '깃구드', + instruction: '사용자가 설명한 버전 제어 작업을 기반으로 적절한 Git 명령 생성', + }, + meetingTakeaways: { + name: '회의 요점', + instruction: '회의를 토론 주제, 핵심 내용 및 실행 항목을 포함한 간결한 요약으로 추출합니다.', + }, + writingsPolisher: { + name: '글쓰기 폴리셔', + instruction: '고급 카피에디팅 기술을 사용하여 글쓰기 향상', + }, + }, + apply: '적용하다', + instruction: '지시', + resTitle: '생성된 프롬프트', + generate: '창조하다', + tryIt: '사용해 보기', + title: '프롬프트 생성기', + overwriteTitle: '기존 구성을 재정의하시겠습니까?', + loading: '응용 프로그램 오케스트레이션...', + instructionPlaceHolder: '명확하고 구체적인 지침을 작성하십시오.', + noDataLine2: '오케스트레이션 미리 보기가 여기에 표시됩니다.', + overwriteMessage: '이 프롬프트를 적용하면 기존 구성이 재정의됩니다.', + noDataLine1: '왼쪽에 사용 사례를 설명하십시오.', + description: '프롬프트 생성기는 구성된 모델을 사용하여 더 높은 품질과 더 나은 구조를 위해 프롬프트를 최적화합니다. 명확하고 상세한 지침을 작성하십시오.', + }, + warningMessage: { + timeoutExceeded: '시간 초과로 인해 결과가 표시되지 않습니다. 전체 결과를 수집하려면 로그를 참조하십시오.', + }, + noResult: '출력이 여기에 표시됩니다.', } export default translation diff --git a/web/i18n/ko-KR/dataset-documents.ts b/web/i18n/ko-KR/dataset-documents.ts index e026144f17..3aa3e9239f 100644 --- a/web/i18n/ko-KR/dataset-documents.ts +++ b/web/i18n/ko-KR/dataset-documents.ts @@ -28,8 +28,9 @@ const translation = { delete: '삭제', enableWarning: '아카이브된 파일은 활성화할 수 없습니다.', sync: '동기화', - resume: '이력서', + resume: '재개', pause: '일시 중지', + download: '파일 다운로드', }, index: { enable: '활성화', diff --git a/web/i18n/ko-KR/login.ts b/web/i18n/ko-KR/login.ts index 51b68967c2..b050d4b9f5 100644 --- a/web/i18n/ko-KR/login.ts +++ b/web/i18n/ko-KR/login.ts @@ -73,7 +73,6 @@ const translation = { checkCode: { verify: '확인', verificationCode: '인증 코드', - tips: '{{email}}로 인증 코드를 보내드립니다.', validTime: '코드는 5 분 동안 유효합니다', checkYourEmail: '이메일 주소 확인', invalidCode: '유효하지 않은 코드', @@ -82,6 +81,7 @@ const translation = { useAnotherMethod: '다른 방법 사용', didNotReceiveCode: '코드를 받지 못하셨나요?', resend: '재전송', + tipsPrefix: '우리는 확인 코드를 보냅니다', }, back: '뒤로', or: '또는', diff --git a/web/i18n/ko-KR/workflow.ts b/web/i18n/ko-KR/workflow.ts index 7a7902dfdc..9b1ec69603 100644 --- a/web/i18n/ko-KR/workflow.ts +++ b/web/i18n/ko-KR/workflow.ts @@ -977,6 +977,7 @@ const translation = { restoreFailure: '버전을 복원하지 못했습니다.', deleteFailure: '버전을 삭제하지 못했습니다.', updateSuccess: '버전이 업데이트되었습니다.', + copyIdSuccess: '클립보드에 복사된 ID', }, editVersionInfo: '버전 정보 편집', latest: '최신', @@ -987,6 +988,7 @@ const translation = { title: '버전 기록', deletionTip: '삭제는 되돌릴 수 없으니, 확인해 주시기 바랍니다.', restorationTip: '버전 복원 후 현재 초안이 덮어쓰여질 것입니다.', + copyId: 'ID 복사', }, debug: { noData: { diff --git a/web/i18n/pl-PL/app-annotation.ts b/web/i18n/pl-PL/app-annotation.ts index 32efc76e66..c0f96a146e 100644 --- a/web/i18n/pl-PL/app-annotation.ts +++ b/web/i18n/pl-PL/app-annotation.ts @@ -85,6 +85,16 @@ const translation = { }, embeddingModelSwitchTip: 'Model wektoryzacji tekstu adnotacji, przełączanie modeli spowoduje ponowne osadzenie, co wiąże się z dodatkowymi kosztami.', + list: { + delete: { + title: 'Czy na pewno chcesz usunąć?', + }, + }, + batchAction: { + selected: 'Wybrany', + delete: 'Usuń', + cancel: 'Anuluj', + }, } export default translation diff --git a/web/i18n/pl-PL/app-debug.ts b/web/i18n/pl-PL/app-debug.ts index 06e271fbbb..7e20b2d7e3 100644 --- a/web/i18n/pl-PL/app-debug.ts +++ b/web/i18n/pl-PL/app-debug.ts @@ -213,6 +213,7 @@ const translation = { after: '', }, }, + contentEnableLabel: 'Włączono moderowanie treści', }, fileUpload: { title: 'Przesyłanie plików', @@ -277,6 +278,7 @@ const translation = { waitForBatchResponse: 'Proszę czekać na odpowiedź na zadanie wsadowe.', notSelectModel: 'Proszę wybrać model', waitForImgUpload: 'Proszę czekać na przesłanie obrazu', + waitForFileUpload: 'Poczekaj na przesłanie pliku/plików', }, chatSubTitle: 'Instrukcje', completionSubTitle: 'Prefix Monitu', @@ -346,6 +348,33 @@ const translation = { 'defaultValue': 'Wartość domyślna', 'noDefaultValue': 'Brak wartości domyślnej', 'selectDefaultValue': 'Wybierz wartość domyślną', + 'file': { + image: { + name: 'Obraz', + }, + audio: { + name: 'Dźwięk', + }, + document: { + name: 'Dokument', + }, + video: { + name: 'Wideo', + }, + custom: { + description: 'Określ inne typy plików.', + createPlaceholder: ' Rozszerzenie pliku, np. .doc', + name: 'Inne typy plików', + }, + supportFileTypes: 'Obsługa typów plików', + }, + 'both': 'Obie', + 'localUpload': 'Przesyłanie lokalne', + 'uploadFileTypes': 'Typy przesyłanych plików', + 'maxNumberOfUploads': 'Maksymalna liczba przesyłanych plików', + 'single-file': 'Pojedynczy plik', + 'content': 'Zawartość', + 'multi-files': 'Lista plików', }, vision: { name: 'Wizja', @@ -366,6 +395,7 @@ const translation = { url: 'URL', uploadLimit: 'Limit przesyłania', }, + onlySupportVisionModelTip: 'Obsługuje tylko modele wizyjne', }, voice: { name: 'Głos', @@ -446,6 +476,7 @@ const translation = { 'Używany do ustawienia progu podobieństwa dla filtrowania fragmentów.', retrieveChangeTip: 'Modyfikacja trybu indeksowania i odzyskiwania może wpłynąć na aplikacje powiązane z tą Wiedzą.', + embeddingModelRequired: 'Wymagany jest skonfigurowany model osadzania', }, debugAsSingleModel: 'Debuguj jako pojedynczy model', debugAsMultipleModel: 'Debuguj jako wiele modeli', @@ -492,6 +523,79 @@ const translation = { enabled: 'Włączone', }, }, + codegen: { + generate: 'Stworzyć', + applyChanges: 'Stosowanie zmian', + loading: 'Generowanie kodu...', + generatedCodeTitle: 'Wygenerowany kod', + description: 'Generator kodów używa skonfigurowanych modeli do generowania wysokiej jakości kodu na podstawie Twoich instrukcji. Podaj jasne i szczegółowe instrukcje.', + resTitle: 'Wygenerowany kod', + title: 'Generator kodów', + overwriteConfirmMessage: 'Ta akcja spowoduje zastąpienie istniejącego kodu. Czy chcesz kontynuować?', + instruction: 'Instrukcje', + apply: 'Zastosować', + instructionPlaceholder: 'Wprowadź szczegółowy opis kodu, który chcesz wygenerować.', + noDataLine2: 'W tym miejscu zostanie wyświetlony podgląd kodu.', + noDataLine1: 'Opisz swój przypadek użycia po lewej stronie,', + overwriteConfirmTitle: 'Nadpisać istniejący kod?', + }, + generate: { + template: { + pythonDebugger: { + name: 'Debuger języka Python', + instruction: 'Bot, który może generować i debugować kod na podstawie instrukcji', + }, + translation: { + name: 'Tłumaczenie', + instruction: 'Tłumacz, który może tłumaczyć wiele języków', + }, + professionalAnalyst: { + instruction: 'Wyodrębniaj szczegółowe informacje, identyfikuj ryzyko i destyluj kluczowe informacje z długich raportów w jednej notatce', + name: 'Zawodowy analityk', + }, + excelFormulaExpert: { + name: 'Ekspert ds. formuł programu Excel', + instruction: 'Chatbot, który może pomóc początkującym użytkownikom zrozumieć, używać i tworzyć formuły Excela na podstawie instrukcji użytkownika', + }, + travelPlanning: { + name: 'Planowanie podróży', + instruction: 'Asystent planowania podróży to inteligentne narzędzie zaprojektowane, aby pomóc użytkownikom w łatwym planowaniu podróży', + }, + SQLSorcerer: { + instruction: 'Przekształć język potoczny w zapytania SQL', + name: 'Czarownik SQL', + }, + GitGud: { + instruction: 'Generowanie odpowiednich poleceń usługi Git na podstawie opisanych przez użytkownika akcji kontroli wersji', + name: 'Git gud', + }, + meetingTakeaways: { + name: 'Wnioski ze spotkania', + instruction: 'Podziel spotkania na zwięzłe podsumowania, w tym tematy dyskusji, kluczowe wnioski i działania', + }, + writingsPolisher: { + instruction: 'Korzystaj z zaawansowanych technik redakcyjnych, aby ulepszyć swoje teksty', + name: 'Polerka do pisania', + }, + }, + instructionPlaceHolder: 'Napisz jasne i konkretne instrukcje.', + instruction: 'Instrukcje', + generate: 'Stworzyć', + tryIt: 'Spróbuj', + overwriteMessage: 'Zastosowanie tego monitu spowoduje zastąpienie istniejącej konfiguracji.', + resTitle: 'Wygenerowany monit', + noDataLine1: 'Opisz swój przypadek użycia po lewej stronie,', + title: 'Generator podpowiedzi', + apply: 'Zastosować', + overwriteTitle: 'Nadpisać istniejącą konfigurację?', + loading: 'Orkiestracja aplikacji dla Ciebie...', + description: 'Generator podpowiedzi używa skonfigurowanego modelu do optymalizacji podpowiedzi w celu uzyskania wyższej jakości i lepszej struktury. Napisz jasne i szczegółowe instrukcje.', + noDataLine2: 'W tym miejscu zostanie wyświetlony podgląd orkiestracji.', + }, + warningMessage: { + timeoutExceeded: 'Wyniki nie są wyświetlane z powodu przekroczenia limitu czasu. Zapoznaj się z dziennikami, aby zebrać pełne wyniki.', + }, + noResult: 'W tym miejscu zostaną wyświetlone dane wyjściowe.', } export default translation diff --git a/web/i18n/pl-PL/app.ts b/web/i18n/pl-PL/app.ts index 9a42b702e7..8751dedc99 100644 --- a/web/i18n/pl-PL/app.ts +++ b/web/i18n/pl-PL/app.ts @@ -27,27 +27,7 @@ const translation = { newApp: { startFromBlank: 'Utwórz od podstaw', startFromTemplate: 'Utwórz z szablonu', - captionAppType: 'Jaki typ aplikacji chcesz stworzyć?', - chatbotDescription: - 'Zbuduj aplikację opartą na czacie. Ta aplikacja używa formatu pytań i odpowiedzi, umożliwiając wielokrotne rundy ciągłej konwersacji.', - completionDescription: - 'Zbuduj aplikację generującą teksty wysokiej jakości na podstawie monitów, takich jak generowanie artykułów, streszczeń, tłumaczeń i innych.', - completionWarning: 'Ten typ aplikacji nie będzie już obsługiwany.', - agentDescription: - 'Zbuduj inteligentnego agenta, który może autonomicznie wybierać narzędzia do wykonywania zadań', - workflowDescription: - 'Zbuduj aplikację, która w oparciu o przepływ pracy generuje teksty wysokiej jakości z dużą możliwością dostosowania. Jest odpowiednia dla doświadczonych użytkowników.', workflowWarning: 'Obecnie w fazie beta', - chatbotType: 'Metoda orkiestracji chatbota', - basic: 'Podstawowy', - basicTip: 'Dla początkujących, można przełączyć się później na Chatflow', - basicFor: 'Dla początkujących', - basicDescription: - 'Podstawowa orkiestracja pozwala na skonfigurowanie aplikacji Chatbot za pomocą prostych ustawień, bez możliwości modyfikacji wbudowanych monitów. Jest odpowiednia dla początkujących.', - advanced: 'Chatflow', - advancedFor: 'Dla zaawansowanych użytkowników', - advancedDescription: - 'Orkiestracja przepływu pracy organizuje Chatboty w formie przepływów pracy, oferując wysoki stopień dostosowania, w tym możliwość edycji wbudowanych monitów. Jest odpowiednia dla doświadczonych użytkowników.', captionName: 'Ikona i nazwa aplikacji', appNamePlaceholder: 'Podaj nazwę swojej aplikacji', captionDescription: 'Opis', diff --git a/web/i18n/pl-PL/dataset-documents.ts b/web/i18n/pl-PL/dataset-documents.ts index da543d299a..c0b801ccf5 100644 --- a/web/i18n/pl-PL/dataset-documents.ts +++ b/web/i18n/pl-PL/dataset-documents.ts @@ -28,8 +28,9 @@ const translation = { delete: 'Usuń', enableWarning: 'Zarchiwizowany plik nie może zostać włączony', sync: 'Synchronizuj', - resume: 'Wznawiać', + resume: 'Wznów', pause: 'Pauza', + download: 'Pobierz plik', }, index: { enable: 'Włącz', diff --git a/web/i18n/pl-PL/login.ts b/web/i18n/pl-PL/login.ts index 8b63fec502..909d1a431f 100644 --- a/web/i18n/pl-PL/login.ts +++ b/web/i18n/pl-PL/login.ts @@ -9,7 +9,6 @@ const translation = { namePlaceholder: 'Twoja nazwa użytkownika', forget: 'Zapomniałeś hasła?', signBtn: 'Zaloguj się', - sso: 'Kontynuuj za pomocą SSO', installBtn: 'Ustaw', setAdminAccount: 'Ustawianie konta administratora', setAdminAccountDesc: @@ -86,8 +85,8 @@ const translation = { useAnotherMethod: 'Użyj innej metody', didNotReceiveCode: 'Nie otrzymałeś kodu?', verificationCode: 'Kod weryfikacyjny', - tips: 'Wysyłamy kod weryfikacyjny na adres {{email}}', emptyCode: 'Kod jest wymagany', + tipsPrefix: 'Wysyłamy kod weryfikacyjny do', }, continueWithCode: 'Kontynuuj z kodem', setYourAccount: 'Ustaw swoje konto', diff --git a/web/i18n/pl-PL/workflow.ts b/web/i18n/pl-PL/workflow.ts index 56b7536879..8c17ba0ff6 100644 --- a/web/i18n/pl-PL/workflow.ts +++ b/web/i18n/pl-PL/workflow.ts @@ -930,6 +930,7 @@ const translation = { deleteSuccess: 'Wersja usunięta', restoreSuccess: 'Wersja przywrócona', restoreFailure: 'Nie udało się przywrócić wersji', + copyIdSuccess: 'Identyfikator skopiowany do schowka', }, currentDraft: 'Aktualny szkic', nameThisVersion: 'Nazwij tę wersję', @@ -940,6 +941,7 @@ const translation = { editVersionInfo: 'Edytuj informacje o wersji', deletionTip: 'Usunięcie jest nieodwracalne, proszę potwierdzić.', restorationTip: 'Po przywróceniu wersji bieżący szkic zostanie nadpisany.', + copyId: 'Kopiuj ID', }, debug: { noData: { diff --git a/web/i18n/pt-BR/app-annotation.ts b/web/i18n/pt-BR/app-annotation.ts index 9e2760bf24..8c1d511f8d 100644 --- a/web/i18n/pt-BR/app-annotation.ts +++ b/web/i18n/pt-BR/app-annotation.ts @@ -83,6 +83,16 @@ const translation = { configConfirmBtn: 'Salvar', }, embeddingModelSwitchTip: 'Modelo de vetorização de texto de anotação, a troca de modelos será refeita, resultando em custos adicionais.', + list: { + delete: { + title: 'Você tem certeza que deseja excluir?', + }, + }, + batchAction: { + cancel: 'Cancelar', + selected: 'Selecionado', + delete: 'Excluir', + }, } export default translation diff --git a/web/i18n/pt-BR/app-debug.ts b/web/i18n/pt-BR/app-debug.ts index 5f8aabec65..fd158acdc8 100644 --- a/web/i18n/pt-BR/app-debug.ts +++ b/web/i18n/pt-BR/app-debug.ts @@ -197,6 +197,7 @@ const translation = { after: '', }, }, + contentEnableLabel: 'Conteúdo moderado habilitado', }, fileUpload: { title: 'Upload de Arquivo', @@ -258,6 +259,7 @@ const translation = { 'Aguarde a resposta à tarefa em lote ser concluída.', notSelectModel: 'Por favor, escolha um modelo', waitForImgUpload: 'Aguarde o upload da imagem', + waitForFileUpload: 'Aguarde o upload do arquivo / arquivos', }, chatSubTitle: 'Instruções', completionSubTitle: 'Prefixo da Solicitação', @@ -322,6 +324,33 @@ const translation = { 'defaultValue': 'Valor padrão', 'noDefaultValue': 'Nenhum valor padrão', 'selectDefaultValue': 'Selecionar valor padrão', + 'file': { + image: { + name: 'Imagem', + }, + audio: { + name: 'Áudio', + }, + document: { + name: 'Documento', + }, + video: { + name: 'Vídeo', + }, + custom: { + description: 'Especifique outros tipos de arquivo.', + name: 'Outros tipos de arquivo', + createPlaceholder: ' Extensão de arquivo, por exemplo, .doc', + }, + supportFileTypes: 'Tipos de arquivo de suporte', + }, + 'content': 'Conteúdo', + 'multi-files': 'Lista de arquivos', + 'single-file': 'Fila indiana', + 'maxNumberOfUploads': 'Número máximo de uploads', + 'uploadFileTypes': 'Carregar tipos de arquivo', + 'both': 'Ambos', + 'localUpload': 'Local Upload', }, vision: { name: 'Visão', @@ -341,6 +370,7 @@ const translation = { url: 'URL', uploadLimit: 'Limite de Upload', }, + onlySupportVisionModelTip: 'Suporta apenas modelos de visão', }, voice: { name: 'voz', @@ -412,6 +442,7 @@ const translation = { score_threshold: 'Limiar de Pontuação', score_thresholdTip: 'Usado para definir o limiar de similaridade para filtragem de trechos.', retrieveChangeTip: 'Modificar o modo de índice e o modo de recuperação pode afetar os aplicativos associados a este Conhecimento.', + embeddingModelRequired: 'É necessário um modelo de incorporação configurado', }, assistantType: { name: 'Tipo de Assistente', @@ -449,6 +480,83 @@ const translation = { enabled: 'Habilitado', }, }, + codegen: { + instruction: 'Instruções', + generatedCodeTitle: 'Código gerado', + noDataLine1: 'Descreva seu caso de uso à esquerda,', + loading: 'Gerando código...', + description: 'O Gerador de código usa modelos configurados para gerar código de alta qualidade com base em suas instruções. Por favor, forneça instruções claras e detalhadas.', + generate: 'Gerar', + resTitle: 'Código gerado', + title: 'Gerador de código', + overwriteConfirmTitle: 'Substituir o código existente?', + overwriteConfirmMessage: 'Essa ação substituirá o código existente. Você quer continuar?', + apply: 'Aplicar', + applyChanges: 'Aplicar alterações', + instructionPlaceholder: 'Insira uma descrição detalhada do código que você deseja gerar.', + noDataLine2: 'A visualização do código será exibida aqui.', + }, + generate: { + template: { + pythonDebugger: { + instruction: 'Um bot que pode gerar e depurar seu código com base em suas instruções', + name: 'Depurador Python', + }, + translation: { + name: 'Tradução', + instruction: 'Um tradutor que pode traduzir vários idiomas', + }, + professionalAnalyst: { + name: 'Analista profissional', + instruction: 'Extraia insights, identifique riscos e destile informações importantes de relatórios longos em um único memorando', + }, + excelFormulaExpert: { + name: 'Especialista em fórmulas do Excel', + instruction: 'Um chatbot que pode ajudar usuários iniciantes a entender, usar e criar fórmulas do Excel com base nas instruções do usuário', + }, + travelPlanning: { + name: 'Planejamento de viagens', + instruction: 'O Assistente de Planejamento de Viagens é uma ferramenta inteligente projetada para ajudar os usuários a planejar suas viagens sem esforço', + }, + SQLSorcerer: { + instruction: 'Transforme a linguagem cotidiana em consultas SQL', + name: 'Feiticeiro SQL', + }, + GitGud: { + instruction: 'Gerar comandos Git apropriados com base nas ações de controle de versão descritas pelo usuário', + name: 'Bom jogo', + }, + meetingTakeaways: { + name: 'Conclusões da reunião', + instruction: 'Destilar reuniões em resumos concisos, incluindo tópicos de discussão, principais conclusões e itens de ação', + }, + writingsPolisher: { + instruction: 'Use técnicas avançadas de edição de texto para melhorar seus escritos', + name: 'Polidor de escrita', + }, + }, + generate: 'Gerar', + overwriteMessage: 'A aplicação desse prompt substituirá a configuração existente.', + apply: 'Aplicar', + title: 'Gerador de Prompt', + description: 'O Gerador de Prompts usa o modelo configurado para otimizar prompts para maior qualidade e melhor estrutura. Por favor, escreva instruções claras e detalhadas.', + instructionPlaceHolder: 'Escreva instruções claras e específicas.', + noDataLine2: 'A visualização da orquestração será exibida aqui.', + tryIt: 'Experimente', + loading: 'Orquestrando o aplicativo para você...', + instruction: 'Instruções', + resTitle: 'Prompt gerado', + noDataLine1: 'Descreva seu caso de uso à esquerda,', + overwriteTitle: 'Substituir a configuração existente?', + }, + warningMessage: { + timeoutExceeded: 'Os resultados não são exibidos devido ao tempo limite. Consulte os logs para obter os resultados completos.', + }, + debugAsSingleModel: 'Depurar como modelo único', + noResult: 'A saída será exibida aqui.', + debugAsMultipleModel: 'Depurar como vários modelos', + publishAs: 'Publicar como', + duplicateModel: 'Duplicar', } export default translation diff --git a/web/i18n/pt-BR/app-log.ts b/web/i18n/pt-BR/app-log.ts index 0b4cbc81e1..ef97f6abff 100644 --- a/web/i18n/pt-BR/app-log.ts +++ b/web/i18n/pt-BR/app-log.ts @@ -87,11 +87,6 @@ const translation = { agentLog: 'Registro do agente', viewLog: 'Ver Registro', agenteLogDetail: { - agentMode: 'Modo Agente', - toolUsed: 'Ferramenta usada', - iterations: 'Iterações', - iteration: 'Iteração', - finalProcessing: 'Processamento Final', }, agentLogDetail: { iterations: 'Iterações', diff --git a/web/i18n/pt-BR/app.ts b/web/i18n/pt-BR/app.ts index 6122a75a97..1f44ae9e5a 100644 --- a/web/i18n/pt-BR/app.ts +++ b/web/i18n/pt-BR/app.ts @@ -27,21 +27,7 @@ const translation = { newApp: { startFromBlank: 'Criar do zero', startFromTemplate: 'Criar do modelo', - captionAppType: 'Que tipo de aplicativo você deseja criar?', - chatbotDescription: 'Construa um aplicativo baseado em chat. Este aplicativo usa um formato de pergunta e resposta, permitindo várias rodadas de conversa contínua.', - completionDescription: 'Construa um aplicativo que gera texto de alta qualidade com base em prompts, como geração de artigos, resumos, traduções e muito mais.', - completionWarning: 'Este tipo de aplicativo não será mais suportado.', - agentDescription: 'Construa um Agente inteligente que pode escolher ferramentas para completar as tarefas autonomamente', - workflowDescription: 'Construa um aplicativo que gera texto de alta qualidade com base em fluxo de trabalho com alto grau de personalização. É adequado para usuários experientes.', workflowWarning: 'Atualmente em beta', - chatbotType: 'Método de orquestração do Chatbot', - basic: 'Básico', - basicTip: 'Para iniciantes, pode mudar para o Chatflow mais tarde', - basicFor: 'PARA INICIANTES', - basicDescription: 'A Orquestração Básica permite orquestrar um aplicativo Chatbot usando configurações simples, sem a capacidade de modificar prompts integrados. É adequado para iniciantes.', - advanced: 'Chatflow', - advancedFor: 'Para usuários avançados', - advancedDescription: 'A Orquestração de Fluxo de Trabalho orquestra Chatbots na forma de fluxos de trabalho, oferecendo um alto grau de personalização, incluindo a capacidade de editar prompts integrados. É adequado para usuários experientes.', captionName: 'Ícone e nome do aplicativo', appNamePlaceholder: 'Dê um nome para o seu aplicativo', captionDescription: 'Descrição', diff --git a/web/i18n/pt-BR/dataset-documents.ts b/web/i18n/pt-BR/dataset-documents.ts index 30fa87f82f..ca4ad21530 100644 --- a/web/i18n/pt-BR/dataset-documents.ts +++ b/web/i18n/pt-BR/dataset-documents.ts @@ -30,6 +30,7 @@ const translation = { sync: 'Sincronizar', resume: 'Retomar', pause: 'Pausa', + download: 'Baixar arquivo', }, index: { enable: 'Habilitar', diff --git a/web/i18n/pt-BR/login.ts b/web/i18n/pt-BR/login.ts index 290cd3c8b4..150df678d9 100644 --- a/web/i18n/pt-BR/login.ts +++ b/web/i18n/pt-BR/login.ts @@ -70,19 +70,18 @@ const translation = { activated: 'Entrar agora', adminInitPassword: 'Senha de inicialização do administrador', validate: 'Validar', - sso: 'Continuar com SSO', checkCode: { useAnotherMethod: 'Use outro método', invalidCode: 'Código inválido', verificationCodePlaceholder: 'Digite o código de 6 dígitos', checkYourEmail: 'Verifique seu e-mail', - tips: 'Enviamos um código de verificação para {{email}}', emptyCode: 'O código é necessário', verify: 'Verificar', verificationCode: 'Código de verificação', resend: 'Reenviar', didNotReceiveCode: 'Não recebeu o código?', validTime: 'Lembre-se de que o código é válido por 5 minutos', + tipsPrefix: 'Enviamos um código de verificação para', }, resetPassword: 'Redefinir senha', or: 'OU', diff --git a/web/i18n/pt-BR/workflow.ts b/web/i18n/pt-BR/workflow.ts index d5820bd611..4d933994db 100644 --- a/web/i18n/pt-BR/workflow.ts +++ b/web/i18n/pt-BR/workflow.ts @@ -930,6 +930,7 @@ const translation = { restoreFailure: 'Falha ao restaurar versão', restoreSuccess: 'Versão restaurada', deleteFailure: 'Falha ao deletar versão', + copyIdSuccess: 'ID copiado para a área de transferência', }, title: 'Versões', latest: 'Último', @@ -940,6 +941,7 @@ const translation = { restorationTip: 'Após a restauração da versão, o rascunho atual será substituído.', currentDraft: 'Rascunho Atual', deletionTip: 'A exclusão é irreversível, por favor confirme.', + copyId: 'Copiar ID', }, debug: { noData: { diff --git a/web/i18n/ro-RO/app-annotation.ts b/web/i18n/ro-RO/app-annotation.ts index 67feb9db1f..66c1c3aa29 100644 --- a/web/i18n/ro-RO/app-annotation.ts +++ b/web/i18n/ro-RO/app-annotation.ts @@ -83,6 +83,16 @@ const translation = { configConfirmBtn: 'Salvează', }, embeddingModelSwitchTip: 'Model de vectorizare a textului anotației, schimbarea modelelor va fi reîncorporată, rezultând costuri suplimentare.', + list: { + delete: { + title: 'Ești sigur că vrei să ștergi?', + }, + }, + batchAction: { + cancel: 'Anulează', + delete: 'Șterge', + selected: 'Selectat', + }, } export default translation diff --git a/web/i18n/ro-RO/app-debug.ts b/web/i18n/ro-RO/app-debug.ts index f6a10df1d2..d8b455e4e0 100644 --- a/web/i18n/ro-RO/app-debug.ts +++ b/web/i18n/ro-RO/app-debug.ts @@ -197,6 +197,7 @@ const translation = { after: '', }, }, + contentEnableLabel: 'Conținut moderat activat', }, fileUpload: { title: 'Încărcare fișier', @@ -258,6 +259,7 @@ const translation = { 'Vă rugăm să așteptați finalizarea sarcinii în lot.', notSelectModel: 'Vă rugăm să alegeți un model', waitForImgUpload: 'Vă rugăm să așteptați încărcarea imaginii', + waitForFileUpload: 'Vă rugăm să așteptați încărcarea fișierului / fișierelor', }, chatSubTitle: 'Instrucțiuni', completionSubTitle: 'Prefix prompt', @@ -322,6 +324,33 @@ const translation = { 'defaultValue': 'Valoare implicită', 'noDefaultValue': 'Fără valoare implicită', 'selectDefaultValue': 'Selectați valoarea implicită', + 'file': { + image: { + name: 'Imagine', + }, + audio: { + name: 'Audio', + }, + document: { + name: 'Document', + }, + video: { + name: 'Video', + }, + custom: { + createPlaceholder: ' Extensia fișierului, de exemplu .doc', + name: 'Alte tipuri de fișiere', + description: 'Specificați alte tipuri de fișiere.', + }, + supportFileTypes: 'Tipuri de fișiere de asistență', + }, + 'content': 'Conținut', + 'single-file': 'Un singur fișier', + 'multi-files': 'Lista de fișiere', + 'uploadFileTypes': 'Încărcați tipuri de fișiere', + 'localUpload': 'Încărcare locală', + 'maxNumberOfUploads': 'Numărul maxim de încărcări', + 'both': 'Ambii', }, vision: { name: 'Viziune', @@ -341,6 +370,7 @@ const translation = { url: 'URL', uploadLimit: 'Limită de încărcare', }, + onlySupportVisionModelTip: 'Acceptă doar modele vizuale', }, voice: { name: 'Voce', @@ -411,6 +441,7 @@ const translation = { score_threshold: 'Prag scor', score_thresholdTip: 'Utilizat pentru a seta pragul de similitudine pentru filtrarea bucăților.', retrieveChangeTip: 'Modificarea modului de indexare și a modului de recuperare poate afecta aplicațiile asociate cu aceste Cunoștințe.', + embeddingModelRequired: 'Este necesar un model de încorporare configurat', }, debugAsSingleModel: 'Depanare ca Model Unic', debugAsMultipleModel: 'Depanare ca Modele Multiple', @@ -452,6 +483,79 @@ const translation = { enabled: 'Activat', }, }, + codegen: { + overwriteConfirmTitle: 'Suprascrierea codului existent?', + resTitle: 'Cod generat', + instruction: 'Instrucţiuni', + description: 'Generatorul de cod utilizează modele configurate pentru a genera cod de înaltă calitate pe baza instrucțiunilor dvs. Vă rugăm să oferiți instrucțiuni clare și detaliate.', + generatedCodeTitle: 'Cod generat', + apply: 'Aplica', + noDataLine2: 'Previzualizarea codului va fi afișată aici.', + noDataLine1: 'Descrieți cazul de utilizare din stânga,', + instructionPlaceholder: 'Introduceți descrierea detaliată a codului pe care doriți să îl generați.', + generate: 'Genera', + title: 'Generator de coduri', + applyChanges: 'Aplicarea modificărilor', + overwriteConfirmMessage: 'Această acțiune va suprascrie codul existent. Vrei să continui?', + loading: 'Generarea codului...', + }, + generate: { + template: { + pythonDebugger: { + name: 'Depanator Python', + instruction: 'Un bot care vă poate genera și depana codul pe baza instrucțiunilor dvs.', + }, + translation: { + instruction: 'Un traducător care poate traduce mai multe limbi', + name: 'Traducere', + }, + professionalAnalyst: { + name: 'Analist profesionist', + instruction: 'Extrageți informații, identificați riscurile și distilați informațiile cheie din rapoartele lungi într-o singură notă', + }, + excelFormulaExpert: { + name: 'Expert în formule Excel', + instruction: 'Un chatbot care poate ajuta utilizatorii începători să înțeleagă, să utilizeze și să creeze formule Excel pe baza instrucțiunilor utilizatorului', + }, + travelPlanning: { + name: 'Planificarea călătoriei', + instruction: 'Asistentul de planificare a călătoriilor este un instrument inteligent conceput pentru a ajuta utilizatorii să-și planifice călătoriile fără efort', + }, + SQLSorcerer: { + name: 'Vrăjitor SQL', + instruction: 'Transformați limbajul de zi cu zi în interogări SQL', + }, + GitGud: { + instruction: 'Generați comenzi Git adecvate pe baza acțiunilor de control al versiunii descrise de utilizator', + name: 'Git gud', + }, + meetingTakeaways: { + instruction: 'Distilați întâlnirile în rezumate concise, inclusiv subiecte de discuție, concluzii cheie și elemente de acțiune', + name: 'Concluzii ale întâlnirilor', + }, + writingsPolisher: { + name: 'Șlefuitor de scris', + instruction: 'Utilizați tehnici avansate de editare pentru a vă îmbunătăți scrierile', + }, + }, + apply: 'Aplica', + generate: 'Genera', + resTitle: 'Solicitare generată', + tryIt: 'Încearcă-l', + overwriteTitle: 'Înlocuiți configurația existentă?', + description: 'Generatorul de solicitări utilizează modelul configurat pentru a optimiza solicitările pentru o calitate superioară și o structură mai bună. Vă rugăm să scrieți instrucțiuni clare și detaliate.', + instruction: 'Instrucţiuni', + loading: 'Orchestrarea aplicației pentru dvs....', + noDataLine1: 'Descrieți cazul de utilizare din stânga,', + title: 'Generator de solicitări', + instructionPlaceHolder: 'Scrieți instrucțiuni clare și specifice.', + noDataLine2: 'Previzualizarea orchestrației va fi afișată aici.', + overwriteMessage: 'Aplicarea acestei solicitări va înlocui configurația existentă.', + }, + warningMessage: { + timeoutExceeded: 'Rezultatele nu sunt afișate din cauza expirării. Vă rugăm să consultați jurnalele pentru a colecta rezultatele complete.', + }, + noResult: 'Ieșirea va fi afișată aici.', } export default translation diff --git a/web/i18n/ro-RO/app.ts b/web/i18n/ro-RO/app.ts index d674b4ca82..2559eea20f 100644 --- a/web/i18n/ro-RO/app.ts +++ b/web/i18n/ro-RO/app.ts @@ -27,21 +27,7 @@ const translation = { newApp: { startFromBlank: 'Creează din Nou', startFromTemplate: 'Creează din Șablon', - captionAppType: 'Ce tip de aplicație vrei să creezi?', - chatbotDescription: 'Construiește o aplicație bazată pe chat. Această aplicație folosește un format întrebare-răspuns, permițând mai multe runde de conversație continuă.', - completionDescription: 'Construiește o aplicație care generează text de înaltă calitate pe baza indicațiilor, cum ar fi generarea de articole, rezumate, traduceri și mai multe.', - completionWarning: 'Acest tip de aplicație nu va mai fi acceptat.', - agentDescription: 'Construiește un Agent inteligent care poate alege în mod autonom instrumentele pentru a îndeplini sarcinile', - workflowDescription: 'Construiește o aplicație care generează text de înaltă calitate pe baza unui flux de lucru orchestrat cu un grad ridicat de personalizare. Este potrivit pentru utilizatorii experimentați.', workflowWarning: 'În prezent în beta', - chatbotType: 'Metodă de orchestrare a chatbot-ului', - basic: 'De bază', - basicTip: 'Pentru începători, se poate comuta la Chatflow mai târziu', - basicFor: 'PENTRU ÎNCEPĂTORI', - basicDescription: 'Orchestrarea de bază permite orchestrarea unei aplicații Chatbot folosind setări simple, fără posibilitatea de a modifica prompturile încorporate. Este potrivit pentru începători.', - advanced: 'Chatflow', - advancedFor: 'Pentru utilizatori avansați', - advancedDescription: 'Orchestrarea fluxului de lucru orchestrează chatboți sub forma fluxurilor de lucru, oferind un grad ridicat de personalizare, inclusiv posibilitatea de a edita prompturile încorporate. Este potrivit pentru utilizatorii experimentați.', captionName: 'Pictogramă și nume aplicație', appNamePlaceholder: 'Dă-i aplicației tale un nume', captionDescription: 'Descriere', diff --git a/web/i18n/ro-RO/dataset-documents.ts b/web/i18n/ro-RO/dataset-documents.ts index c624d0acde..a6d7ffdfab 100644 --- a/web/i18n/ro-RO/dataset-documents.ts +++ b/web/i18n/ro-RO/dataset-documents.ts @@ -30,6 +30,7 @@ const translation = { sync: 'Sincronizează', pause: 'Pauză', resume: 'Reia', + download: 'Descărcați fișierul', }, index: { enable: 'Activează', diff --git a/web/i18n/ro-RO/login.ts b/web/i18n/ro-RO/login.ts index 342010a10e..ca1b1d4e01 100644 --- a/web/i18n/ro-RO/login.ts +++ b/web/i18n/ro-RO/login.ts @@ -9,7 +9,6 @@ const translation = { namePlaceholder: 'Numele tău de utilizator', forget: 'Ai uitat parola?', signBtn: 'Autentificare', - sso: 'Continuă cu SSO', installBtn: 'Configurare', setAdminAccount: 'Configurare cont de administrator', setAdminAccountDesc: 'Privilegii maxime pentru contul de administrator, care poate fi utilizat pentru crearea de aplicații și gestionarea furnizorilor LLM, etc.', @@ -80,9 +79,9 @@ const translation = { verificationCodePlaceholder: 'Introduceți codul din 6 cifre', emptyCode: 'Codul este necesar', verify: 'Verifica', - tips: 'Trimitem un cod de verificare la {{email}}', useAnotherMethod: 'Utilizați o altă metodă', resend: 'Retrimite', + tipsPrefix: 'Trimitem un cod de verificare la', }, usePassword: 'Utilizați parola', useVerificationCode: 'Utilizarea codului de verificare', diff --git a/web/i18n/ro-RO/workflow.ts b/web/i18n/ro-RO/workflow.ts index 4a24f7dc00..b4eb41d041 100644 --- a/web/i18n/ro-RO/workflow.ts +++ b/web/i18n/ro-RO/workflow.ts @@ -930,6 +930,7 @@ const translation = { deleteFailure: 'Ștergerea versiunii a eșuat', updateSuccess: 'Versiune actualizată', updateFailure: 'Actualizarea versiunii a eșuat', + copyIdSuccess: 'ID copiat în clipboard', }, latest: 'Cea mai recentă', title: 'Versiuni', @@ -940,6 +941,7 @@ const translation = { releaseNotesPlaceholder: 'Descrie ce s-a schimbat', deletionTip: 'Ștergerea este irreversibilă, vă rugăm să confirmați.', currentDraft: 'Draftul curent', + copyId: 'Copiază ID', }, debug: { noData: { diff --git a/web/i18n/ru-RU/app-annotation.ts b/web/i18n/ru-RU/app-annotation.ts index e189c9ca93..5d55e40174 100644 --- a/web/i18n/ru-RU/app-annotation.ts +++ b/web/i18n/ru-RU/app-annotation.ts @@ -83,6 +83,16 @@ const translation = { configConfirmBtn: 'Сохранить', }, embeddingModelSwitchTip: 'Модель векторизации текста аннотаций, переключение между моделями будет осуществлено повторно, что приведет к дополнительным затратам.', + list: { + delete: { + title: 'Вы уверены, что хотите удалить?', + }, + }, + batchAction: { + cancel: 'Отменить', + selected: 'Выбрано', + delete: 'Удалить', + }, } export default translation diff --git a/web/i18n/ru-RU/app-debug.ts b/web/i18n/ru-RU/app-debug.ts index 1d45c90a43..0ff97c6cca 100644 --- a/web/i18n/ru-RU/app-debug.ts +++ b/web/i18n/ru-RU/app-debug.ts @@ -197,6 +197,7 @@ const translation = { after: '', }, }, + contentEnableLabel: 'Включен модерируемый контент', }, fileUpload: { title: 'Загрузка файлов', @@ -294,6 +295,7 @@ const translation = { 'Пожалуйста, дождитесь завершения ответа на пакетное задание.', notSelectModel: 'Пожалуйста, выберите модель', waitForImgUpload: 'Пожалуйста, дождитесь загрузки изображения', + waitForFileUpload: 'Пожалуйста, дождитесь загрузки файла/файлов', }, chatSubTitle: 'Инструкции', completionSubTitle: 'Префикс Промпта', @@ -359,6 +361,32 @@ const translation = { 'defaultValue': 'Значение по умолчанию', 'noDefaultValue': 'Без значения по умолчанию', 'selectDefaultValue': 'Выберите значение по умолчанию', + 'file': { + image: { + name: 'Образ', + }, + audio: { + name: 'Аудио', + }, + document: { + name: 'Документ', + }, + video: { + name: 'Видео', + }, + custom: { + createPlaceholder: ' Расширение файла, например .doc', + name: 'Другие типы файлов', + description: 'Укажите другие типы файлов.', + }, + supportFileTypes: 'Типы файлов поддержки', + }, + 'both': 'Оба', + 'localUpload': 'Локальная загрузка', + 'maxNumberOfUploads': 'Максимальное количество загрузок', + 'uploadFileTypes': 'Типы файлов загрузки', + 'single-file': 'Друг за другом', + 'multi-files': 'Список файлов', }, vision: { name: 'Зрение', @@ -378,6 +406,7 @@ const translation = { url: 'URL', uploadLimit: 'Лимит загрузки', }, + onlySupportVisionModelTip: 'Поддерживает только модели машинного зрения', }, voice: { name: 'Голос', @@ -449,6 +478,7 @@ const translation = { score_threshold: 'Порог оценки', score_thresholdTip: 'Используется для установки порога сходства для фильтрации фрагментов.', retrieveChangeTip: 'Изменение режима индексации и режима поиска может повлиять на приложения, связанные с этими знаниями.', + embeddingModelRequired: 'Требуется сконфигурированная модель встраивания', }, debugAsSingleModel: 'Отладка как одной модели', debugAsMultipleModel: 'Отладка как нескольких моделей', @@ -490,6 +520,26 @@ const translation = { enabled: 'Включено', }, }, + codegen: { + generate: 'Порождать', + title: 'Генератор кодов', + resTitle: 'Сгенерированный код', + generatedCodeTitle: 'Сгенерированный код', + applyChanges: 'Применение изменений', + loading: 'Генерация кода...', + noDataLine2: 'Предварительный просмотр кода будет показан здесь.', + instruction: 'Резолюция', + apply: 'Применять', + overwriteConfirmTitle: 'Перезаписать существующий код?', + overwriteConfirmMessage: 'Это действие перезапишет существующий код. Хотите продолжить?', + instructionPlaceholder: 'Введите подробное описание кода, который вы хотите сгенерировать.', + noDataLine1: 'Опишите свой вариант использования слева,', + description: 'Генератор кода использует настроенные модели для создания высококачественного кода на основе ваших инструкций. Пожалуйста, предоставьте четкие и подробные инструкции.', + }, + warningMessage: { + timeoutExceeded: 'Результаты не отображаются из-за тайм-аута. Пожалуйста, обратитесь к журналам для получения полных результатов.', + }, + noResult: 'Вывод будет отображаться здесь.', } export default translation diff --git a/web/i18n/ru-RU/app.ts b/web/i18n/ru-RU/app.ts index b02d01b263..bc15d16ee1 100644 --- a/web/i18n/ru-RU/app.ts +++ b/web/i18n/ru-RU/app.ts @@ -31,21 +31,7 @@ const translation = { newApp: { startFromBlank: 'Создать с нуля', startFromTemplate: 'Создать из шаблона', - captionAppType: 'Какой тип приложения вы хотите создать?', - chatbotDescription: 'Создайте приложение на основе чата. Это приложение использует формат вопросов и ответов, позволяя общаться непрерывно.', - completionDescription: 'Создайте приложение, которое генерирует высококачественный текст на основе подсказок, например, генерирует статьи, резюме, переводы и многое другое.', - completionWarning: 'Этот тип приложения больше не будет поддерживаться.', - agentDescription: 'Создайте интеллектуального агента, который может автономно выбирать инструменты для выполнения задач', - workflowDescription: 'Создайте приложение, которое генерирует высококачественный текст на основе рабочего процесса, организованного с высокой степенью настройки. Подходит для опытных пользователей.', workflowWarning: 'В настоящее время находится в бета-версии', - chatbotType: 'Метод организации чат-бота', - basic: 'Базовый', - basicTip: 'Для начинающих, можно переключиться на Chatflow позже', - basicFor: 'ДЛЯ НАЧИНАЮЩИХ', - basicDescription: 'Базовый конструктор позволяет создать приложение чат-бота с помощью простых настроек, без возможности изменять встроенные подсказки. Подходит для начинающих.', - advanced: 'Chatflow', - advancedFor: 'Для продвинутых пользователей', - advancedDescription: 'Организация рабочего процесса организует чат-ботов в виде рабочих процессов, предлагая высокую степень настройки, включая возможность редактирования встроенных подсказок. Подходит для опытных пользователей.', captionName: 'Значок и название приложения', appNamePlaceholder: 'Дайте вашему приложению имя', captionDescription: 'Описание', diff --git a/web/i18n/ru-RU/dataset-documents.ts b/web/i18n/ru-RU/dataset-documents.ts index 5a72eb766c..400ada270d 100644 --- a/web/i18n/ru-RU/dataset-documents.ts +++ b/web/i18n/ru-RU/dataset-documents.ts @@ -29,8 +29,9 @@ const translation = { delete: 'Удалить', enableWarning: 'Архивный файл не может быть включен', sync: 'Синхронизировать', - resume: 'Продовжити', + resume: 'Возобновить', pause: 'Пауза', + download: 'Скачать файл', }, index: { enable: 'Включить', diff --git a/web/i18n/ru-RU/login.ts b/web/i18n/ru-RU/login.ts index 38e4559012..874b0aef0b 100644 --- a/web/i18n/ru-RU/login.ts +++ b/web/i18n/ru-RU/login.ts @@ -9,7 +9,6 @@ const translation = { namePlaceholder: 'Ваше имя пользователя', forget: 'Забыли пароль?', signBtn: 'Войти', - sso: 'Продолжить с SSO', installBtn: 'Настроить', setAdminAccount: 'Настройка учетной записи администратора', setAdminAccountDesc: 'Максимальные привилегии для учетной записи администратора, которые можно использовать для создания приложений, управления поставщиками LLM и т. д.', @@ -79,10 +78,10 @@ const translation = { emptyCode: 'Код обязателен для заполнения', verificationCode: 'Проверочный код', checkYourEmail: 'Проверьте свою электронную почту', - tips: 'Мы отправляем код подтверждения на {{email}}', validTime: 'Имейте в виду, что код действителен в течение 5 минут', verificationCodePlaceholder: 'Введите 6-значный код', useAnotherMethod: 'Используйте другой метод', + tipsPrefix: 'Мы отправляем код проверки на', }, back: 'Назад', changePasswordBtn: 'Установите пароль', diff --git a/web/i18n/ru-RU/workflow.ts b/web/i18n/ru-RU/workflow.ts index 284a88c5b2..87982d1331 100644 --- a/web/i18n/ru-RU/workflow.ts +++ b/web/i18n/ru-RU/workflow.ts @@ -930,6 +930,7 @@ const translation = { deleteSuccess: 'Версия удалена', updateFailure: 'Не удалось обновить версию', restoreFailure: 'Не удалось восстановить версию', + copyIdSuccess: 'ID скопирован в буфер обмена', }, latest: 'Последний', restorationTip: 'После восстановления версии текущий черновик будет перезаписан.', @@ -940,6 +941,7 @@ const translation = { currentDraft: 'Текущий проект', releaseNotesPlaceholder: 'Опишите, что изменилось', defaultName: 'Без названия версия', + copyId: 'Копировать ID', }, debug: { noData: { diff --git a/web/i18n/sl-SI/app-annotation.ts b/web/i18n/sl-SI/app-annotation.ts index 6cd88a47ee..69a6db57be 100644 --- a/web/i18n/sl-SI/app-annotation.ts +++ b/web/i18n/sl-SI/app-annotation.ts @@ -83,6 +83,16 @@ const translation = { configConfirmBtn: 'Shrani', }, embeddingModelSwitchTip: 'Model za vektorizacijo besedila opomb, preklapljanje modelov bo ponovno vektoriziralo, kar bo povzročilo dodatne stroške.', + list: { + delete: { + title: 'Ali ste prepričani, da želite izbrisati?', + }, + }, + batchAction: { + cancel: 'Prekliči', + delete: 'Izbriši', + selected: 'Izbrano', + }, } export default translation diff --git a/web/i18n/sl-SI/app-debug.ts b/web/i18n/sl-SI/app-debug.ts index 597a8afa06..6e02a0d586 100644 --- a/web/i18n/sl-SI/app-debug.ts +++ b/web/i18n/sl-SI/app-debug.ts @@ -185,8 +185,19 @@ const translation = { content: { input: 'Moderiraj VSEBINO VNOSA', output: 'Moderiraj VSEBINO IZHODA', + preset: 'Prednastavljeni odgovori', + errorMessage: 'Prednastavljeni odgovori ne smejo biti prazni', + condition: 'Zmerna vsebina INPUT in OUTPUT je omogočena vsaj ena', + supportMarkdown: 'Podprt za Markdown', + fromApi: 'Prednastavljene odgovore vrne API', + placeholder: 'Prednastavljena vsebina odgovorov tukaj', + }, + openaiNotConfig: { + after: '', + before: 'Za moderiranje OpenAI potrebujete ključ OpenAI API, konfiguriran v', }, }, + contentEnableLabel: 'Omogočena zmerna vsebina', }, debug: { title: 'Odpravljanje napak', @@ -264,6 +275,291 @@ const translation = { description: 'Omogočitev zvoka bo omogočila modelu, da obdela zvočne datoteke za prepisovanje in analizo.', }, }, + codegen: { + instruction: 'Navodila', + title: 'Generator kode', + resTitle: 'Ustvarjena koda', + loading: 'Generiranje kode ...', + generatedCodeTitle: 'Ustvarjena koda', + noDataLine1: 'Na levi opišite primer uporabe,', + noDataLine2: 'Predogled kode bo prikazan tukaj.', + instructionPlaceholder: 'Vnesite podroben opis kode, ki jo želite ustvariti.', + apply: 'Uporabiti', + generate: 'Ustvariti', + overwriteConfirmTitle: 'Prepisati obstoječo kodo?', + applyChanges: 'Uporaba sprememb', + overwriteConfirmMessage: 'S tem dejanjem boste prepisali obstoječo kodo. Želite nadaljevati?', + description: 'Generator kode uporablja konfigurirane modele za ustvarjanje visokokakovostne kode na podlagi vaših navodil. Navedite jasna in podrobna navodila.', + }, + generate: { + template: { + pythonDebugger: { + name: 'Python razhroščevalnik', + instruction: 'Bot, ki lahko ustvari in razhrošči vašo kodo na podlagi vaših navodil', + }, + translation: { + name: 'Prevod', + instruction: 'Prevajalec, ki zna prevesti več jezikov', + }, + professionalAnalyst: { + name: 'Strokovni analitik', + instruction: 'Pridobite vpoglede, prepoznajte tveganja in destilirajte ključne informacije iz dolgih poročil v en sam zapisek', + }, + excelFormulaExpert: { + instruction: 'Chatbot, ki lahko začetnikom pomaga razumeti, uporabljati in ustvarjati Excelove formule na podlagi uporabniških navodil', + name: 'Strokovnjak za formule v Excelu', + }, + travelPlanning: { + instruction: 'Pomočnik za načrtovanje potovanj je inteligentno orodje, ki uporabnikom pomaga pri enostavnem načrtovanju potovanj', + name: 'Načrtovanje potovanj', + }, + SQLSorcerer: { + name: 'Čarovnik SQL', + instruction: 'Pretvorba vsakdanjega jezika v poizvedbe SQL', + }, + GitGud: { + instruction: 'Ustvarite ustrezne ukaze Git na podlagi dejanj nadzora različic, ki jih je opisal uporabnik', + name: 'Git gud', + }, + meetingTakeaways: { + name: 'Povzetki s srečanja', + instruction: 'Srečanja destilirajte v jedrnate povzetke, vključno s temami za razpravo, ključnimi ugotovitvami in dejanji', + }, + writingsPolisher: { + name: 'Pisanje polir', + instruction: 'Uporabite napredne tehnike urejanja besedil za izboljšanje svojega pisanja', + }, + }, + apply: 'Uporabiti', + generate: 'Ustvariti', + instructionPlaceHolder: 'Napišite jasna in specifična navodila.', + resTitle: 'Ustvarjen poziv', + noDataLine2: 'Predogled orkestracije bo prikazan tukaj.', + overwriteMessage: 'Če uporabite ta poziv, boste preglasili obstoječo konfiguracijo.', + overwriteTitle: 'Preglasiti obstoječo konfiguracijo?', + instruction: 'Navodila', + loading: 'Orkestriranje aplikacije za vas ...', + noDataLine1: 'Na levi opišite primer uporabe,', + title: 'Generator pozivov', + tryIt: 'Poskusite', + description: 'Generator pozivov uporablja konfiguriran model za optimizacijo pozivov za višjo kakovost in boljšo strukturo. Prosimo, napišite jasna in podrobna navodila.', + }, + resetConfig: { + title: 'Potrdite ponastavitev?', + message: 'Ponastavitev zavrže spremembe in obnovi zadnjo objavljeno konfiguracijo.', + }, + errorMessage: { + notSelectModel: 'Prosimo, izberite model', + waitForImgUpload: 'Prosimo, počakajte, da se slika naloži', + waitForResponse: 'Počakajte, da se odgovor na prejšnje sporočilo dokonča.', + waitForBatchResponse: 'Počakajte, da se konča odgovor na paketno nalogo.', + queryRequired: 'Besedilo zahteve je obvezno.', + waitForFileUpload: 'Prosimo, počakajte, da se datoteka/datoteke naložijo', + }, + warningMessage: { + timeoutExceeded: 'Rezultati niso prikazani zaradi časovne omejitve. Prosimo, glejte dnevnike, da zberete popolne rezultate.', + }, + variableTable: { + action: 'Dejanja', + optional: 'Neobvezno', + typeString: 'Niz', + typeSelect: 'Izbrati', + type: 'Vrsta vnosa', + key: 'Spremenljivi ključ', + name: 'Ime uporabniškega vnosnega polja', + }, + varKeyError: {}, + otherError: { + promptNoBeEmpty: 'Poziv ne more biti prazen', + historyNoBeEmpty: 'Zgodovina pogovorov mora biti nastavljena v pozivu', + queryNoBeEmpty: 'Poizvedba mora biti nastavljena v pozivu', + }, + variableConfig: { + 'file': { + image: { + name: 'Podoba', + }, + audio: { + name: 'Avdio', + }, + document: { + name: 'Dokument', + }, + video: { + name: 'Video', + }, + custom: { + description: 'Določite druge vrste datotek.', + name: 'Druge vrste datotek', + createPlaceholder: ' Pripona datoteke, npr. .doc', + }, + supportFileTypes: 'Podporne vrste datotek', + }, + 'errorMsg': { + varNameCanBeRepeat: 'Imena spremenljivke ni mogoče ponoviti', + atLeastOneOption: 'Potrebna je vsaj ena možnost', + optionRepeat: 'Ima možnosti ponavljanja', + labelNameRequired: 'Ime nalepke je obvezno', + }, + 'content': 'Vsebina', + 'number': 'Številka', + 'selectDefaultValue': 'Izbira privzete vrednosti', + 'maxNumberOfUploads': 'Največje število nalaganj', + 'localUpload': 'Lokalno nalaganje', + 'string': 'Kratko besedilo', + 'paragraph': 'Odstavek', + 'maxLength': 'Največja dolžina', + 'defaultValue': 'Privzeta vrednost', + 'apiBasedVar': 'Spremenljivka, ki temelji na API-ju', + 'stringTitle': 'Možnosti polja z besedilom obrazca', + 'varName': 'Ime spremenljivke', + 'text-input': 'Kratko besedilo', + 'uploadFileTypes': 'Nalaganje vrst datotek', + 'noDefaultValue': 'Ni privzete vrednosti', + 'addOption': 'Dodaj možnost', + 'select': 'Izbrati', + 'hide': 'Skriti', + 'both': 'Oba', + 'multi-files': 'Seznam datotek', + 'single-file': 'Ena datoteka', + 'options': 'Možnosti', + 'addModalTitle': 'Dodajanje vhodnega polja', + 'inputPlaceholder': 'Prosimo, vnesite', + 'fieldType': 'Vrsta polja', + 'editModalTitle': 'Uredi vnosno polje', + 'required': 'Zahteva', + 'labelName': 'Ime nalepke', + }, + vision: { + visionSettings: { + resolution: 'Resolucija', + uploadMethod: 'Način nalaganja', + high: 'Visok', + url: 'Spletni naslov', + localUpload: 'Lokalno nalaganje', + uploadLimit: 'Omejitev nalaganja', + title: 'Nastavitve vida', + both: 'Oba', + low: 'Nizek', + }, + name: 'Vid', + settings: 'Nastavitve', + description: 'Omogoči vid bo modelu omogočil, da posname slike in odgovarja na vprašanja o njih.', + onlySupportVisionModelTip: 'Podpira samo modele vida', + }, + voice: { + voiceSettings: { + voice: 'Glas', + language: 'Jezik', + autoPlayDisabled: 'Off', + autoPlayEnabled: 'Na', + resolutionTooltip: 'Jezik glasovne podpore za pretvorbo besedila v govor。', + title: 'Glasovne nastavitve', + autoPlay: 'Samodejno predvajanje', + }, + defaultDisplay: 'Privzeti glas', + name: 'Glas', + settings: 'Nastavitve', + description: 'Glasovne nastavitve za pretvorbo besedila v govor', + }, + openingStatement: { + openingQuestion: 'Uvodna vprašanja', + title: 'Odpiralec pogovorov', + tooShort: 'Za ustvarjanje uvodnih pripomb za pogovor je potrebnih vsaj 20 besed začetnega poziva.', + noDataPlaceHolder: 'Začetek pogovora z uporabnikom lahko AI pomaga vzpostaviti tesnejšo povezavo z njimi v pogovornih aplikacijah.', + add: 'Dodati', + writeOpener: 'Odpiralnik za urejanje', + }, + modelConfig: { + modeType: { + chat: 'Chat', + completion: 'Dokončati', + }, + title: 'Model in parametri', + model: 'Model', + setTone: 'Nastavitev tona odzivov', + }, + inputs: { + queryPlaceholder: 'Prosimo, vnesite besedilo zahteve.', + title: 'Odpravljanje napak in predogled', + chatVarTip: 'Izpolnite vrednost spremenljivke, ki bo samodejno nadomeščena v pozivni besedi vsakič, ko se začne nova seja', + queryTitle: 'Vsebina poizvedbe', + userInputField: 'Uporabniško polje za vnos', + run: 'TEČI', + noPrompt: 'Poskusite napisati nekaj poziva v vnos pred pozivom', + previewTitle: 'Takojšen predogled', + noVar: 'Izpolnite vrednost spremenljivke, ki bo samodejno nadomeščena v pozivni besedi vsakič, ko se začne nova seja.', + completionVarTip: 'Izpolnite vrednost spremenljivke, ki bo samodejno nadomeščena v pozivnih besedah vsakič, ko boste oddali vprašanje.', + }, + datasetConfig: { + retrieveOneWay: { + title: 'Pridobivanje N-na-1', + description: 'Na podlagi namena uporabnika in opisov znanja agent avtonomno izbere najboljše znanje za poizvedovanje. Najboljše za aplikacije z izrazitim, omejenim znanjem.', + }, + retrieveMultiWay: { + title: 'Pridobivanje več poti', + description: 'Na podlagi namena uporabnika poizvedbe v celotnem znanju, pridobijo ustrezno besedilo iz več virov in izberejo najboljše rezultate, ki se ujemajo z uporabniško poizvedbo po ponovnem razvrščanju.', + }, + params: 'Params', + embeddingModelRequired: 'Potreben je konfiguriran model vdelave', + settingTitle: 'Nastavitve pridobivanja', + rerankModelRequired: 'Potreben je konfiguriran model ponovnega razvrščanja', + knowledgeTip: 'Kliknite gumb " " za dodajanje znanja', + score_threshold: 'Prag ocenjevanja', + score_thresholdTip: 'Uporablja se za nastavitev praga podobnosti za filtriranje kosov.', + retrieveChangeTip: 'Spreminjanje kazalnega načina in načina pridobivanja lahko vpliva na aplikacije, povezane s tem znanjem.', + top_k: 'Vrh K', + top_kTip: 'Uporablja se za filtriranje kosov, ki so najbolj podobni vprašanjem uporabnikov. Sistem bo tudi dinamično prilagajal vrednost Top K, glede na max_tokens izbranega modela.', + }, + assistantType: { + chatAssistant: { + name: 'Osnovni pomočnik', + description: 'Ustvarjanje pomočnika za klepet z uporabo velikega jezikovnega modela', + }, + agentAssistant: { + name: 'Pomočnik agenta', + description: 'Zgradite inteligentnega agenta, ki lahko samostojno izbere orodja za dokončanje nalog', + }, + name: 'Vrsta pomočnika', + }, + agent: { + agentModeType: { + functionCall: 'Klicanje funkcij', + ReACT: 'Reagirajo', + }, + setting: { + maximumIterations: { + description: 'Omejitev števila ponovitev, ki jih lahko izvede pomočnik agenta', + name: 'Največje število ponovitev', + }, + description: 'Nastavitve pomočnika za agente omogočajo nastavitev načina agenta in naprednih funkcij, kot so vgrajeni pozivi, ki so na voljo samo v vrsti agenta.', + name: 'Nastavitve agenta', + }, + tools: { + enabled: 'Omogočeno', + name: 'Orodja', + description: 'Uporaba orodij lahko razširi zmogljivosti LLM, kot je iskanje po internetu ali izvajanje znanstvenih izračunov', + }, + agentMode: 'Način agenta', + promptPlaceholder: 'Tukaj napišite svoj poziv', + agentModeDes: 'Nastavitev vrste načina sklepanja za agenta', + firstPrompt: 'Prvi poziv', + nextIteration: 'Naslednja ponovitev', + buildInPrompt: 'Poziv za vgradnjo', + }, + chatSubTitle: 'Navodila', + variableTitle: 'Spremenljivke', + completionSubTitle: 'Poziv za predpono', + debugAsSingleModel: 'Odpravljanje napak kot en model', + noResult: 'Tukaj bo prikazan izhod.', + debugAsMultipleModel: 'Odpravljanje napak kot več modelov', + formattingChangedText: 'Spreminjanje oblikovanja bo ponastavilo območje za odpravljanje napak, ste prepričani?', + autoAddVar: 'Nedoločene spremenljivke, na katere se sklicuje vnaprejšnji poziv, ali jih želite dodati v obrazec za vnos uporabnika?', + formattingChangedTitle: 'Spremenjeno oblikovanje', + duplicateModel: 'Dvojnik', + publishAs: 'Objavi kot', + result: 'Izhodno besedilo', + variableTip: 'Uporabniki izpolnijo spremenljivke v obrazcu in samodejno zamenjajo spremenljivke v pozivu.', } export default translation diff --git a/web/i18n/sl-SI/app.ts b/web/i18n/sl-SI/app.ts index 337bd10359..61c479e65f 100644 --- a/web/i18n/sl-SI/app.ts +++ b/web/i18n/sl-SI/app.ts @@ -31,21 +31,7 @@ const translation = { newApp: { startFromBlank: 'Ustvari iz nič', startFromTemplate: 'Ustvari iz predloge', - captionAppType: 'Kakšno aplikacijo želite ustvariti?', - chatbotDescription: 'Zgradite aplikacijo, ki temelji na klepetu. Ta aplikacija uporablja format vprašanj in odgovorov, ki omogoča več krogov neprekinjenega pogovora.', - completionDescription: 'Zgradite aplikacijo, ki na podlagi pozivov generira visokokakovostno besedilo, kot je ustvarjanje člankov, povzetkov, prevodov in več.', - completionWarning: 'Ta vrsta aplikacije ne bo več podprta.', - agentDescription: 'Zgradite inteligentnega agenta, ki lahko samostojno izbere orodja za dokončanje nalog.', - workflowDescription: 'Zgradite aplikacijo, ki generira visokokakovostno besedilo na podlagi orkestracije poteka dela z visoko stopnjo prilagodljivosti. Primerna je za izkušene uporabnike.', workflowWarning: 'Trenutno v beta različici', - chatbotType: 'Metoda orkestracije klepetalnika', - basic: 'Osnovno', - basicTip: 'Za začetnike, lahko kasneje preklopite na Chatflow', - basicFor: 'ZA ZAČETNIKE', - basicDescription: 'Osnovna orkestracija omogoča orkestracijo aplikacije klepetalnika z enostavnimi nastavitvami, brez možnosti spreminjanja vgrajenih pozivov. Primerna je za začetnike.', - advanced: 'Chatflow', - advancedFor: 'Za napredne uporabnike', - advancedDescription: 'Orkestracija poteka dela orkestrira klepetalnike v obliki potekov dela, ki ponuja visoko stopnjo prilagodljivosti, vključno z možnostjo urejanja vgrajenih pozivov. Primerna je za izkušene uporabnike.', captionName: 'Ikona in ime aplikacije', appNamePlaceholder: 'Poimenujte svojo aplikacijo', captionDescription: 'Opis', diff --git a/web/i18n/sl-SI/dataset-documents.ts b/web/i18n/sl-SI/dataset-documents.ts index ca4f10e798..a163197e86 100644 --- a/web/i18n/sl-SI/dataset-documents.ts +++ b/web/i18n/sl-SI/dataset-documents.ts @@ -31,6 +31,7 @@ const translation = { sync: 'Sinhroniziraj', pause: 'Zaustavi', resume: 'Nadaljuj', + download: 'Prenesi datoteko', }, index: { enable: 'Omogoči', diff --git a/web/i18n/sl-SI/login.ts b/web/i18n/sl-SI/login.ts index 479b8b9221..acb6aba2c6 100644 --- a/web/i18n/sl-SI/login.ts +++ b/web/i18n/sl-SI/login.ts @@ -9,7 +9,6 @@ const translation = { namePlaceholder: 'Vaše uporabniško ime', forget: 'Ste pozabili geslo?', signBtn: 'Prijava', - sso: 'Nadaljujte z SSO', installBtn: 'Namesti', setAdminAccount: 'Nastavitev administratorskega računa', setAdminAccountDesc: 'Najvišje pravice za administratorski račun, ki se lahko uporablja za ustvarjanje aplikacij in upravljanje LLM ponudnikov itd.', @@ -76,13 +75,13 @@ const translation = { verificationCodePlaceholder: 'Vnesite 6-mestno kodo', resend: 'Poslati', verificationCode: 'Koda za preverjanje', - tips: 'Kodo za preverjanje pošljemo na {{email}}', verify: 'Preveriti', validTime: 'Upoštevajte, da je koda veljavna 5 minut', checkYourEmail: 'Preverjanje e-pošte', didNotReceiveCode: 'Niste prejeli kode?', invalidCode: 'Neveljavna koda', useAnotherMethod: 'Uporabite drug način', + tipsPrefix: 'Pošljemo kodo za preverjanje na', }, useVerificationCode: 'Uporaba kode za preverjanje', licenseInactive: 'Licenca je neaktivna', diff --git a/web/i18n/sl-SI/workflow.ts b/web/i18n/sl-SI/workflow.ts index 72150701de..f267fb0d50 100644 --- a/web/i18n/sl-SI/workflow.ts +++ b/web/i18n/sl-SI/workflow.ts @@ -927,6 +927,7 @@ const translation = { restoreSuccess: 'Obnovljena različica', restoreFailure: 'Obnavljanje različice ni uspelo', updateSuccess: 'Različica posodobljena', + copyIdSuccess: 'ID kopiran v odložišče', }, defaultName: 'Nepodpisana različica', deletionTip: 'Izbris je nepovraten, prosim potrdite.', @@ -937,6 +938,7 @@ const translation = { nameThisVersion: 'Poimenujte to različico', releaseNotesPlaceholder: 'Opisujte, kaj se je spremenilo', restorationTip: 'Po obnovitvi različice bo trenutni osnutek prepisan.', + copyId: 'Kopiraj ID', }, debug: { noData: { diff --git a/web/i18n/th-TH/app-annotation.ts b/web/i18n/th-TH/app-annotation.ts index f038f5ef8c..60598267c3 100644 --- a/web/i18n/th-TH/app-annotation.ts +++ b/web/i18n/th-TH/app-annotation.ts @@ -83,6 +83,16 @@ const translation = { configConfirmBtn: 'ประหยัด', }, embeddingModelSwitchTip: 'โมเดลเวกเตอร์ข้อความคําอธิบายประกอบ โมเดลการสลับจะถูกฝังใหม่ส่งผลให้มีค่าใช้จ่ายเพิ่มเติม', + list: { + delete: { + title: 'คุณแน่ใจหรือว่าต้องการลบ?', + }, + }, + batchAction: { + delete: 'ลบ', + selected: 'เลือกไว้', + cancel: 'ยกเลิก', + }, } export default translation diff --git a/web/i18n/th-TH/app-debug.ts b/web/i18n/th-TH/app-debug.ts index a2e939b5fe..303b91353a 100644 --- a/web/i18n/th-TH/app-debug.ts +++ b/web/i18n/th-TH/app-debug.ts @@ -27,7 +27,488 @@ const translation = { title: 'เสียง', description: 'การเปิดใช้งานเสียงจะทำให้โมเดลสามารถประมวลผลไฟล์เสียงเพื่อการถอดข้อความและการวิเคราะห์ได้', }, + groupChat: { + title: 'ปรับปรุงแชท', + description: 'เพิ่มการตั้งค่าก่อนการสนทนาสําหรับแอปสามารถปรับปรุงประสบการณ์ของผู้ใช้ได้', + }, + groupExperience: { + title: 'ปรับปรุงประสบการณ์', + }, + conversationOpener: { + description: 'ในแอปแชท ประโยคแรกที่ AI พูดกับผู้ใช้อย่างแข็งขันมักจะใช้เป็นการต้อนรับ', + title: 'ที่เปิดการสนทนา', + }, + suggestedQuestionsAfterAnswer: { + title: 'ติดตาม', + resDes: '3 ข้อเสนอแนะสําหรับผู้ใช้คําถามถัดไป', + tryToAsk: 'ลองถาม', + description: 'การตั้งค่าคําแนะนําคําถามถัดไปจะช่วยให้ผู้ใช้แชทได้ดีขึ้น', + }, + moreLikeThis: { + title: 'เพิ่มเติมเช่นนี้', + tip: 'การใช้คุณสมบัตินี้จะมีค่าใช้จ่ายโทเค็นเพิ่มเติม', + generateNumTip: 'จํานวนครั้งที่สร้างขึ้นแต่ละครั้ง', + description: 'สร้างข้อความหลายข้อความพร้อมกัน จากนั้นแก้ไขและสร้างต่อไป', + }, + speechToText: { + description: 'สามารถใช้การป้อนข้อมูลด้วยเสียงในการแชทได้', + title: 'คําพูดเป็นข้อความ', + resDes: 'เปิดใช้งานการป้อนข้อมูลด้วยเสียง', + }, + textToSpeech: { + title: 'ข้อความเป็นคําพูด', + resDes: 'เปิดใช้งานข้อความเป็นเสียง', + description: 'ข้อความการสนทนาสามารถแปลงเป็นคําพูดได้', + }, + citation: { + title: 'การอ้างอิงและการระบุแหล่งที่มา', + resDes: 'เปิดใช้งานการอ้างอิงและการระบุแหล่งที่มา', + description: 'แสดงเอกสารต้นฉบับและส่วนที่มาของเนื้อหาที่สร้างขึ้น', + }, + annotation: { + scoreThreshold: { + accurateMatch: 'การจับคู่ที่แม่นยํา', + description: 'ใช้เพื่อกําหนดเกณฑ์ความคล้ายคลึงกันสําหรับการตอบกลับคําอธิบายประกอบ', + easyMatch: 'จับคู่ง่าย', + title: 'เกณฑ์คะแนน', + }, + matchVariable: { + title: 'ตัวแปรการจับคู่', + choosePlaceholder: 'เลือกตัวแปรการจับคู่', + }, + removeConfirm: 'ลบคําอธิบายประกอบนี้ ?', + cacheManagement: 'คำ อธิบาย', + title: 'คําอธิบายประกอบ ตอบกลับ', + remove: 'ถอด', + resDes: 'เปิดใช้งานการตอบสนองคําอธิบายประกอบ', + add: 'เพิ่มคําอธิบายประกอบ', + edit: 'แก้ไขคําอธิบายประกอบ', + cached: 'มีคําอธิบายประกอบ', + description: 'คุณสามารถเพิ่มการตอบกลับคุณภาพสูงลงในแคชด้วยตนเองเพื่อจับคู่ลําดับความสําคัญกับคําถามของผู้ใช้ที่คล้ายกัน', + }, + dataSet: { + queryVariable: { + ok: 'ตกลง, ได้', + noVar: 'ไม่ใช่ตัวแปร', + choosePlaceholder: 'เลือกตัวแปรแบบสอบถาม', + tip: 'ตัวแปรนี้จะถูกใช้เป็นอินพุตแบบสอบถามสําหรับการดึงบริบท โดยรับข้อมูลบริบทที่เกี่ยวข้องกับอินพุตของตัวแปรนี้', + unableToQueryDataSetTip: 'ไม่สามารถสืบค้นความรู้ได้สําเร็จ โปรดเลือกตัวแปรการสืบค้นบริบทในส่วนบริบท', + noVarTip: 'โปรดสร้างตัวแปรภายใต้ส่วนตัวแปร', + title: 'ตัวแปรคิวรี', + contextVarNotEmpty: 'ตัวแปรการสืบค้นบริบทต้องไม่ว่างเปล่า', + deleteContextVarTip: 'ตัวแปรนี้ถูกตั้งค่าเป็นตัวแปรแบบสอบถามบริบท และการลบตัวแปรนี้จะส่งผลต่อการใช้ความรู้ตามปกติ หากคุณยังต้องการลบ โปรดเลือกใหม่ในส่วนบริบท', + unableToQueryDataSet: 'ไม่สามารถสืบค้นความรู้ได้', + }, + noDataSet: 'ไม่พบความรู้', + notSupportSelectMulti: 'ปัจจุบันรองรับความรู้เพียงหนึ่งความรู้', + selected: 'เลือกความรู้', + title: 'ความรู้', + toCreate: 'ไปที่สร้าง', + words: 'นิรุกติ', + textBlocks: 'บล็อกข้อความ', + noData: 'คุณสามารถนําเข้าความรู้เป็นบริบทได้', + selectTitle: 'เลือกข้อมูลอ้างอิง ความรู้', + }, + tools: { + modal: { + toolType: { + title: 'ประเภทเครื่องมือ', + placeholder: 'โปรดเลือกประเภทเครื่องมือ', + }, + name: { + title: 'ชื่อ', + placeholder: 'กรุณากรอกชื่อ', + }, + variableName: { + title: 'ชื่อตัวแปร', + placeholder: 'กรุณากรอกชื่อตัวแปร', + }, + title: 'เครื่องมือ', + }, + title: 'เครื่อง มือ', + tips: 'เครื่องมือมีวิธีการเรียก API มาตรฐาน โดยใช้อินพุตหรือตัวแปรของผู้ใช้เป็นพารามิเตอร์คําขอสําหรับการสืบค้นข้อมูลภายนอกตามบริบท', + }, + conversationHistory: { + editModal: { + userPrefix: 'คํานําหน้าผู้ใช้', + title: 'แก้ไขชื่อบทบาทการสนทนา', + assistantPrefix: 'คํานําหน้าผู้ช่วย', + }, + description: 'ตั้งชื่อคํานําหน้าสําหรับบทบาทการสนทนา', + learnMore: 'ศึกษาเพิ่มเติม', + title: 'ประวัติการสนทนา', + }, + toolbox: { + title: 'เครื่อง มือ', + }, + moderation: { + modal: { + provider: { + openaiTip: { + suffix: '.', + prefix: 'การกลั่นกรอง OpenAI ต้องใช้คีย์ OpenAI API ที่กําหนดค่าไว้ใน', + }, + keywords: 'คำ', + openai: 'การกลั่นกรอง OpenAI', + title: 'ผู้จัดหา', + }, + keywords: { + placeholder: 'หนึ่งบรรทัดต่อบรรทัดคั่นด้วยตัวแบ่งบรรทัด', + tip: 'หนึ่งบรรทัด คั่นด้วยตัวแบ่งบรรทัด สูงสุด 100 อักขระต่อบรรทัด', + line: 'สาย', + }, + content: { + output: 'เนื้อหา OUTPUT ปานกลาง', + errorMessage: 'การตอบกลับที่ตั้งไว้ล่วงหน้าต้องไม่ว่างเปล่า', + fromApi: 'การตอบกลับที่ตั้งไว้ล่วงหน้าจะถูกส่งคืนโดย API', + supportMarkdown: 'รองรับ Markdown', + placeholder: 'เนื้อหาตอบกลับที่ตั้งไว้ล่วงหน้าที่นี่', + condition: 'เปิดใช้งานเนื้อหา INPUT และ OUTPUT กลั่นกรองอย่างน้อยหนึ่งรายการ', + input: 'กลั่นกรองเนื้อหา INPUT', + preset: 'การตอบกลับที่ตั้งไว้ล่วงหน้า', + }, + openaiNotConfig: { + after: '', + before: 'การกลั่นกรอง OpenAI ต้องใช้คีย์ OpenAI API ที่กําหนดค่าไว้ใน', + }, + title: 'การตั้งค่าการกลั่นกรองเนื้อหา', + }, + contentEnableLabel: 'เปิดใช้งานเนื้อหากลั่นกรอง', + outputEnabled: 'ผลิตภัณฑ์', + title: 'การกลั่นกรองเนื้อหา', + allEnabled: 'อินพุต & เอาต์พุต', + inputEnabled: 'อินพุต', + description: 'รักษาความปลอดภัยเอาต์พุตโมเดลโดยใช้ API การกลั่นกรองหรือรักษารายการคําที่ละเอียดอ่อน', + }, }, + pageTitle: { + line1: 'พร้อมท์', + line2: 'วิศวกรรม', + }, + promptMode: { + advancedWarning: { + ok: 'ตกลง, ได้', + description: 'ในโหมดผู้เชี่ยวชาญ คุณสามารถแก้ไข PROMPT ทั้งหมดได้', + title: 'คุณได้เปลี่ยนเป็นโหมดผู้เชี่ยวชาญแล้ว และเมื่อคุณแก้ไข PROMPT แล้ว คุณจะไม่สามารถกลับสู่โหมดพื้นฐานได้', + learnMore: 'ศึกษาเพิ่มเติม', + }, + operation: { + addMessage: 'เพิ่มข้อความ', + }, + switchBack: 'สลับกลับ', + contextMissing: 'องค์ประกอบบริบทที่พลาดไปประสิทธิภาพของพรอมต์อาจไม่ดี', + simple: 'เปลี่ยนเป็นโหมดผู้เชี่ยวชาญเพื่อแก้ไข PROMPT ทั้งหมด', + advanced: 'แฟชั่นผู้เชี่ยวชาญ', + }, + operation: { + automatic: 'ผลิต', + applyConfig: 'ตีพิมพ์', + disagree: 'ไม่ชอบ', + userAction: 'ผู้ใช้', + stopResponding: 'หยุดการตอบสนอง', + cancelAgree: 'ยกเลิกถูกใจ', + addFeature: 'เพิ่มคุณสมบัติ', + cancelDisagree: 'ยกเลิกการไม่ชอบ', + agree: 'ชอบ', + resetConfig: 'รี เซ็ต', + debugConfig: 'ดีบัก', + }, + notSetAPIKey: { + settingBtn: 'ไปที่การตั้งค่า', + trailFinished: 'เส้นทางเสร็จสิ้น', + description: 'ยังไม่ได้ตั้งค่าคีย์ผู้ให้บริการ LLM และจําเป็นต้องตั้งค่าก่อนการดีบัก', + title: 'ไม่ได้ตั้งค่าคีย์ผู้ให้บริการ LLM', + }, + trailUseGPT4Info: { + description: 'ใช้ gpt-4 โปรดตั้งค่าคีย์ API', + title: 'ไม่รองรับ gpt-4 ในขณะนี้', + }, + codegen: { + applyChanges: 'ใช้การเปลี่ยนแปลง', + generate: 'ผลิต', + instructionPlaceholder: 'ป้อนคําอธิบายโดยละเอียดของรหัสที่คุณต้องการสร้าง', + noDataLine1: 'อธิบายกรณีการใช้งานของคุณทางด้านซ้าย', + title: 'เครื่องสร้างรหัส', + overwriteConfirmMessage: 'การดําเนินการนี้จะเขียนทับโค้ดที่มีอยู่ คุณต้องการดําเนินการต่อหรือไม่?', + loading: 'กําลังสร้างโค้ด...', + generatedCodeTitle: 'รหัสที่สร้างขึ้น', + apply: 'ใช้', + overwriteConfirmTitle: 'เขียนทับรหัสที่มีอยู่ใช่ไหม', + instruction: 'คำ แนะ นำ', + resTitle: 'รหัสที่สร้างขึ้น', + noDataLine2: 'ตัวอย่างโค้ดจะแสดงที่นี่', + description: 'ตัวสร้างโค้ดใช้โมเดลที่กําหนดค่าเพื่อสร้างโค้ดคุณภาพสูงตามคําแนะนําของคุณ โปรดให้คําแนะนําที่ชัดเจนและละเอียด', + }, + generate: { + template: { + pythonDebugger: { + name: 'ดีบักเกอร์ Python', + instruction: 'บอทที่สามารถสร้างและแก้ไขข้อบกพร่องโค้ดของคุณตามคําสั่งของคุณ', + }, + translation: { + instruction: 'นักแปลที่สามารถแปลได้หลายภาษา', + name: 'การแปล', + }, + professionalAnalyst: { + name: 'นักวิเคราะห์มืออาชีพ', + instruction: 'ดึงข้อมูลเชิงลึก ระบุความเสี่ยง และกลั่นกรองข้อมูลสําคัญจากรายงานขนาดยาวลงในบันทึกเดียว', + }, + excelFormulaExpert: { + name: 'ผู้เชี่ยวชาญด้านสูตร Excel', + instruction: 'แชทบอทที่สามารถช่วยให้ผู้ใช้มือใหม่เข้าใจ ใช้ และสร้างสูตร Excel ตามคําแนะนําของผู้ใช้', + }, + travelPlanning: { + name: 'การวางแผนการเดินทาง', + instruction: 'ผู้ช่วยวางแผนการเดินทางเป็นเครื่องมืออัจฉริยะที่ออกแบบมาเพื่อช่วยให้ผู้ใช้วางแผนการเดินทางได้อย่างง่ายดาย', + }, + SQLSorcerer: { + name: 'พ่อมด SQL', + instruction: 'แปลงภาษาในชีวิตประจําวันให้เป็นแบบสอบถาม SQL', + }, + GitGud: { + name: 'กิต gud', + instruction: 'สร้างคําสั่ง Git ที่เหมาะสมตามการดําเนินการควบคุมเวอร์ชันที่ผู้ใช้อธิบาย', + }, + meetingTakeaways: { + name: 'ประเด็นการประชุม', + instruction: 'กลั่นกรองการประชุมเป็นบทสรุปที่กระชับ รวมถึงหัวข้อการสนทนา ประเด็นสําคัญ และรายการปฏิบัติ', + }, + writingsPolisher: { + name: 'เครื่องขัดเขียน', + instruction: 'ใช้เทคนิคการแก้ไขคําโฆษณาขั้นสูงเพื่อปรับปรุงงานเขียนของคุณ', + }, + }, + generate: 'ผลิต', + instruction: 'คำ แนะ นำ', + apply: 'ใช้', + resTitle: 'พรอมต์ที่สร้างขึ้น', + title: 'เครื่องกําเนิดพร้อมท์', + noDataLine2: 'ตัวอย่างการประสานเสียงจะแสดงที่นี่', + tryIt: 'ลองดู', + overwriteTitle: 'แทนที่การกําหนดค่าที่มีอยู่ใช่ไหม', + noDataLine1: 'อธิบายกรณีการใช้งานของคุณทางด้านซ้าย', + instructionPlaceHolder: 'เขียนคําแนะนําที่ชัดเจนและเฉพาะเจาะจง', + overwriteMessage: 'การใช้พรอมต์นี้จะแทนที่การกําหนดค่าที่มีอยู่', + description: 'ตัวสร้างพรอมต์ใช้โมเดลที่กําหนดค่าเพื่อปรับพรอมต์ให้เหมาะสมเพื่อคุณภาพที่สูงขึ้นและโครงสร้างที่ดีขึ้น โปรดเขียนคําแนะนําที่ชัดเจนและละเอียด', + loading: 'กําลังประสานงานแอปพลิเคชันสําหรับคุณ...', + }, + resetConfig: { + title: 'ยืนยันการรีเซ็ต?', + message: 'รีเซ็ตจะละทิ้งการเปลี่ยนแปลง โดยคืนค่าการกําหนดค่าที่เผยแพร่ล่าสุด', + }, + errorMessage: { + waitForFileUpload: 'โปรดรอให้ไฟล์/ไฟล์อัปโหลด', + notSelectModel: 'โปรดเลือกรุ่น', + waitForBatchResponse: 'โปรดรอให้การตอบกลับงานแบทช์เสร็จสมบูรณ์', + waitForResponse: 'โปรดรอให้การตอบกลับข้อความก่อนหน้าเสร็จสมบูรณ์', + waitForImgUpload: 'โปรดรอให้ภาพอัปโหลด', + queryRequired: 'ต้องส่งข้อความคําขอ', + }, + warningMessage: { + timeoutExceeded: 'ผลลัพธ์จะไม่แสดงเนื่องจากหมดเวลา โปรดดูบันทึกเพื่อรวบรวมผลลัพธ์ที่สมบูรณ์', + }, + variableTable: { + optional: 'เสริม', + key: 'ปุ่มตัวแปร', + typeString: 'เชือก', + typeSelect: 'เลือก', + type: 'ประเภทอินพุต', + name: 'ชื่อฟิลด์ป้อนข้อมูลของผู้ใช้', + action: 'การดําเนินการ', + }, + varKeyError: {}, + otherError: { + queryNoBeEmpty: 'ต้องตั้งค่าคิวรีในพร้อมท์', + promptNoBeEmpty: 'พรอมต์ไม่สามารถว่างเปล่าได้', + historyNoBeEmpty: 'ต้องตั้งค่าประวัติการสนทนาในข้อความแจ้ง', + }, + variableConfig: { + 'file': { + image: { + name: 'ภาพ', + }, + audio: { + name: 'เสียง', + }, + document: { + name: 'เอกสาร', + }, + video: { + name: 'วีดิทัศน์', + }, + custom: { + description: 'ระบุประเภทไฟล์อื่นๆ', + name: 'ไฟล์ประเภทอื่น ๆ', + createPlaceholder: ' นามสกุลไฟล์ เช่น .doc', + }, + supportFileTypes: 'ประเภทไฟล์ที่รองรับ', + }, + 'errorMsg': { + atLeastOneOption: 'จําเป็นต้องมีอย่างน้อยหนึ่งตัวเลือก', + labelNameRequired: 'ต้องมีชื่อฉลาก', + optionRepeat: 'มีตัวเลือกการทําซ้ํา', + varNameCanBeRepeat: 'ไม่สามารถทําซ้ําชื่อตัวแปรได้', + }, + 'hide': 'ซ่อน', + 'required': 'ต้องระบุ', + 'number': 'เลข', + 'inputPlaceholder': 'กรุณาป้อน', + 'uploadFileTypes': 'อัปโหลดประเภทไฟล์', + 'content': 'เนื้อหา', + 'addOption': 'เพิ่มตัวเลือก', + 'labelName': 'ชื่อฉลาก', + 'options': 'ตัวเลือก', + 'stringTitle': 'ตัวเลือกกล่องข้อความฟอร์ม', + 'noDefaultValue': 'ไม่มีค่าเริ่มต้น', + 'varName': 'ชื่อตัวแปร', + 'defaultValue': 'ค่าเริ่มต้น', + 'fieldType': 'ชนิดฟิลด์', + 'selectDefaultValue': 'เลือกค่าเริ่มต้น', + 'string': 'ข้อความสั้น', + 'text-input': 'ข้อความสั้น', + 'multi-files': 'รายการไฟล์', + 'maxLength': 'ความยาวสูงสุด', + 'addModalTitle': 'เพิ่มฟิลด์อินพุต', + 'localUpload': 'อัปโหลดในเครื่อง', + 'single-file': 'ไฟล์เดียว', + 'select': 'เลือก', + 'maxNumberOfUploads': 'จํานวนการอัปโหลดสูงสุด', + 'editModalTitle': 'แก้ไขฟิลด์อินพุต', + 'apiBasedVar': 'ตัวแปรที่ใช้ API', + 'paragraph': 'วรรค', + 'both': 'ทั้งสอง', + }, + vision: { + visionSettings: { + resolution: 'มติ', + uploadMethod: 'วิธีการอัปโหลด', + localUpload: 'อัปโหลดในเครื่อง', + low: 'ต่ํา', + high: 'สูง', + title: 'การตั้งค่าวิสัยทัศน์', + uploadLimit: 'ขีดจํากัดการอัปโหลด', + both: 'ทั้งสอง', + url: 'URL', + }, + onlySupportVisionModelTip: 'รองรับเฉพาะโมเดลการมองเห็น', + name: 'การมองเห็น', + description: 'เปิดใช้งานวิสัยทัศน์จะช่วยให้โมเดลสามารถถ่ายภาพและตอบคําถามเกี่ยวกับภาพเหล่านั้นได้', + settings: 'การตั้งค่า', + }, + voice: { + voiceSettings: { + autoPlayEnabled: 'บน', + autoPlay: 'เล่นอัตโนมัติ', + voice: 'เสียง', + resolutionTooltip: 'ภาษาสนับสนุนเสียงแปลงข้อความเป็นคําพูด。', + autoPlayDisabled: 'ไป', + title: 'การตั้งค่าเสียง', + language: 'ภาษา', + }, + name: 'เสียง', + settings: 'การตั้งค่า', + description: 'การตั้งค่าเสียงข้อความเป็นคําพูด', + defaultDisplay: 'เสียงเริ่มต้น', + }, + openingStatement: { + tooShort: 'ต้องใช้ข้อความแจ้งเริ่มต้นอย่างน้อย 20 คําเพื่อสร้างคําพูดเปิดการสนทนา', + openingQuestion: 'คําถามเปิด', + writeOpener: 'ตัวเปิดแก้ไข', + add: 'เพิ่ม', + title: 'ที่เปิดการสนทนา', + noDataPlaceHolder: 'การเริ่มการสนทนากับผู้ใช้สามารถช่วยให้ AI สร้างความสัมพันธ์ที่ใกล้ชิดกับพวกเขาในแอปพลิเคชันการสนทนา', + }, + modelConfig: { + modeType: { + completion: 'สมบูรณ์', + chat: 'สนทนา', + }, + model: 'แบบ', + title: 'รุ่นและพารามิเตอร์', + setTone: 'กําหนดน้ําเสียงของการตอบกลับ', + }, + inputs: { + run: 'วิ่ง', + userInputField: 'ฟิลด์ป้อนข้อมูลของผู้ใช้', + queryPlaceholder: 'กรุณากรอกข้อความคําขอ', + queryTitle: 'เนื้อหาแบบสอบถาม', + title: 'ดีบัก & ดูตัวอย่าง', + noVar: 'กรอกค่าของตัวแปร ซึ่งจะถูกแทนที่โดยอัตโนมัติในคําพร้อมท์ทุกครั้งที่เริ่มเซสชันใหม่', + previewTitle: 'พร้อมท์ดูตัวอย่าง', + chatVarTip: 'กรอกค่าของตัวแปร ซึ่งจะถูกแทนที่โดยอัตโนมัติในคําพร้อมท์ทุกครั้งที่เริ่มเซสชันใหม่', + noPrompt: 'ลองเขียนข้อความแจ้งในการป้อนข้อมูลล่วงหน้า', + completionVarTip: 'กรอกค่าของตัวแปร ซึ่งจะถูกแทนที่โดยอัตโนมัติในคําพร้อมท์ทุกครั้งที่มีการส่งคําถาม', + }, + datasetConfig: { + retrieveOneWay: { + title: 'การดึงข้อมูล N-to-1', + description: 'เอเจนต์จะเลือกความรู้ที่ดีที่สุดสําหรับการสืบค้นด้วยตนเอง ดีที่สุดสําหรับการใช้งานที่มีความรู้ที่แตกต่างและจํากัด', + }, + retrieveMultiWay: { + title: 'การดึงข้อมูลหลายเส้นทาง', + description: 'ตามความตั้งใจของผู้ใช้ การสืบค้นในความรู้ทั้งหมด ดึงข้อความที่เกี่ยวข้องจากหลายแหล่ง และเลือกผลลัพธ์ที่ดีที่สุดที่ตรงกับการสืบค้นของผู้ใช้หลังจากจัดอันดับใหม่', + }, + score_thresholdTip: 'ใช้เพื่อกําหนดเกณฑ์ความคล้ายคลึงกันสําหรับการกรองกลุ่ม', + settingTitle: 'การตั้งค่าการดึงข้อมูล', + rerankModelRequired: 'จําเป็นต้องมีโมเดลจัดอันดับใหม่ที่กําหนดค่าไว้', + knowledgeTip: 'คลิกปุ่ม " " เพื่อเพิ่มความรู้', + embeddingModelRequired: 'จําเป็นต้องมีโมเดลการฝังที่กําหนดค่าไว้', + score_threshold: 'เกณฑ์คะแนน', + retrieveChangeTip: 'การปรับเปลี่ยนโหมดดัชนีและโหมดการดึงข้อมูลอาจส่งผลต่อแอปพลิเคชันที่เกี่ยวข้องกับความรู้นี้', + top_k: 'ท็อป K', + params: 'พารามิเตอร์', + top_kTip: 'ใช้เพื่อกรองกลุ่มที่คล้ายกับคําถามของผู้ใช้มากที่สุด ระบบจะปรับค่าของ Top K แบบไดนามิกตาม max_tokens ของรุ่นที่เลือก', + }, + assistantType: { + chatAssistant: { + name: 'ผู้ช่วยพื้นฐาน', + description: 'สร้างผู้ช่วยตามแชทโดยใช้โมเดลภาษาขนาดใหญ่', + }, + agentAssistant: { + name: 'ผู้ช่วยตัวแทน', + description: 'สร้างตัวแทนอัจฉริยะที่สามารถเลือกเครื่องมือเพื่อทํางานให้เสร็จได้โดยอัตโนมัติ', + }, + name: 'ประเภทผู้ช่วย', + }, + agent: { + agentModeType: { + functionCall: 'การเรียกฟังก์ชัน', + ReACT: 'ตอบสนอง', + }, + setting: { + maximumIterations: { + description: 'จํากัดจํานวนการทําซ้ําที่ผู้ช่วยตัวแทนสามารถดําเนินการได้', + name: 'การทําซ้ําสูงสุด', + }, + name: 'การตั้งค่าตัวแทน', + description: 'การตั้งค่าผู้ช่วยตัวแทนอนุญาตให้ตั้งค่าโหมดตัวแทนและคุณสมบัติขั้นสูง เช่น ข้อความแจ้งในตัว ซึ่งใช้ได้เฉพาะในประเภทตัวแทนเท่านั้น', + }, + tools: { + enabled: 'เปิด', + name: 'เครื่อง มือ', + description: 'การใช้เครื่องมือสามารถขยายขีดความสามารถของ LLM ได้ เช่น การค้นหาทางอินเทอร์เน็ตหรือการคํานวณทางวิทยาศาสตร์', + }, + agentMode: 'โหมดตัวแทน', + firstPrompt: 'พรอมต์แรก', + buildInPrompt: 'พรอมต์ในตัว', + promptPlaceholder: 'เขียนข้อความแจ้งของคุณที่นี่', + nextIteration: 'การทําซ้ําครั้งต่อไป', + agentModeDes: 'ตั้งค่าประเภทของโหมดการอนุมานสําหรับตัวแทน', + }, + orchestrate: 'ออเคสตร้า', + variableTitle: 'ตัว แปร', + noResult: 'ผลลัพธ์จะแสดงที่นี่', + formattingChangedText: 'การแก้ไขการจัดรูปแบบจะรีเซ็ตพื้นที่ดีบัก คุณแน่ใจหรือไม่?', + publishAs: 'เผยแพร่เป็น', + result: 'ข้อความที่ส่งออก', + formattingChangedTitle: 'การจัดรูปแบบเปลี่ยนไป', + completionSubTitle: 'พรอมต์คํานําหน้า', + chatSubTitle: 'คำ แนะ นำ', + debugAsMultipleModel: 'ดีบักเป็นหลายรุ่น', + variableTip: 'ผู้ใช้กรอกตัวแปรในแบบฟอร์ม แทนที่ตัวแปรในพรอมต์โดยอัตโนมัติ', + debugAsSingleModel: 'ดีบักเป็นโมเดลเดียว', + duplicateModel: 'สำเนา', + autoAddVar: 'ตัวแปรที่ไม่ได้กําหนดอ้างอิงในพรอมต์ล่วงหน้าคุณต้องการเพิ่มในแบบฟอร์มการป้อนข้อมูลของผู้ใช้หรือไม่?', } export default translation diff --git a/web/i18n/th-TH/app.ts b/web/i18n/th-TH/app.ts index 8c8c0e02a2..d0e3394ff8 100644 --- a/web/i18n/th-TH/app.ts +++ b/web/i18n/th-TH/app.ts @@ -29,21 +29,7 @@ const translation = { newApp: { startFromBlank: 'สร้างโปรเจกต์ปล่าว', startFromTemplate: 'สร้างจากเทมเพลต', - captionAppType: 'คุณต้องการสร้างโปรเจกต์ประเภทใด', - chatbotDescription: 'สร้างโปรเจกต์เป็นแอปพลิเคชันที่ใช้การแชท โปรเจกต์นี้ใช้รูปแบบคําถามและคําตอบ ทําให้สามารถสนทนาต่อเนื่องได้หลายรอบ(Multi-turn)', - completionDescription: 'สร้างโปรเจกต์เป็นแอปพลิเคชันที่สร้างข้อความคุณภาพสูงตามข้อความแจ้ง เช่น การสร้างบทความ สรุป การแปล และอื่นๆ', - completionWarning: 'โปรเจกต์ประเภทนี้จะไม่รองรับอีกต่อไป', - agentDescription: 'สร้างตัวแทน(Agent)อัจฉริยะที่สามารถเลือกเครื่องมือเพื่อทํางานให้เสร็จได้โดยอัตโนมัติ', - workflowDescription: 'สร้างโปรเจกต์ เป็นแอปพลิเคชันที่สร้างข้อความคุณภาพสูงตามการประสานกระบวนการทำงาน(Workflow) ที่มีการปรับแต่งในระดับสูง เหมาะสําหรับผู้ใช้ที่มีประสบการณ์', workflowWarning: 'ขณะนี้อยู่ในช่วงเบต้า', - chatbotType: 'รูปแบบแชทบอท', - basic: 'พื้นฐาน', - basicTip: 'สําหรับผู้เริ่มต้นสามารถเปลี่ยนไปใช้ Chatflow ได้ในภายหลัง', - basicFor: 'สําหรับผู้เริ่มต้น', - basicDescription: 'Basic Orchestrate ช่วยให้สามารถประสานงานกันของ โปรเจกต์แชทบอทโดยใช้การตั้งค่าง่ายๆ โดยไม่สามารถแก้ไขข้อความแจ้งในตัวได้ เหมาะสําหรับผู้เริ่มต้น', - advanced: 'แชทโฟลว์', - advancedFor: 'สําหรับผู้ใช้ขั้นสูง ที่สามารถปรับแต่งขั้นตอนและตัวเลือกต่างๆได้อย่างอิสระ', - advancedDescription: 'Workflow Orchestrate ประสานงาน Chatbots ในรูปแบบของเวิร์กโฟลว์ โดยนําเสนอการปรับแต่งในระดับสูง รวมถึงความสามารถในการแก้ไขข้อความแจ้งในตัว เหมาะสําหรับผู้ใช้ที่มีประสบการณ์', captionName: 'ไอคอนและชื่อโปรเจกต์', appNamePlaceholder: 'ตั้งชื่อโปรเจกต์ของคุณ', captionDescription: 'คำอธิบาย', diff --git a/web/i18n/th-TH/common.ts b/web/i18n/th-TH/common.ts index dd7dd31cb1..1dcfe63a9d 100644 --- a/web/i18n/th-TH/common.ts +++ b/web/i18n/th-TH/common.ts @@ -137,7 +137,7 @@ const translation = { menus: { status: 'Beta', explore: 'สํารวจ', - apps: 'เรียน', + apps: 'สตูดิโอ', plugins: 'ปลั๊กอิน', pluginsTips: 'รวมปลั๊กอินของบุคคลที่สามหรือสร้างปลั๊กอิน AI ที่เข้ากันได้กับ ChatGPT', datasets: 'ความรู้', diff --git a/web/i18n/th-TH/dataset-documents.ts b/web/i18n/th-TH/dataset-documents.ts index 1471ba365a..539dadfd18 100644 --- a/web/i18n/th-TH/dataset-documents.ts +++ b/web/i18n/th-TH/dataset-documents.ts @@ -31,6 +31,7 @@ const translation = { sync: 'ซิงค์', pause: 'หยุด', resume: 'ดำเนิน', + download: 'ดาวน์โหลดไฟล์', }, index: { enable: 'เปิด', diff --git a/web/i18n/th-TH/login.ts b/web/i18n/th-TH/login.ts index 3db8da4da8..621b9999a0 100644 --- a/web/i18n/th-TH/login.ts +++ b/web/i18n/th-TH/login.ts @@ -79,7 +79,6 @@ const translation = { validate: 'ตรวจ สอบ', checkCode: { checkYourEmail: 'ตรวจสอบอีเมลของคุณ', - tips: 'เราส่งรหัสยืนยันไปที่ {{email}}', validTime: 'โปรดทราบว่ารหัสนี้ใช้ได้นาน 5 นาที', verificationCode: 'รหัสยืนยัน', verificationCodePlaceholder: 'ป้อนรหัส 6 หลัก', @@ -89,6 +88,7 @@ const translation = { useAnotherMethod: 'ใช้วิธีอื่น', emptyCode: 'ต้องใช้รหัส', invalidCode: 'รหัสไม่ถูกต้อง', + tipsPrefix: 'เราส่งรหัสตรวจสอบไปยัง', }, resetPassword: 'รีเซ็ตรหัสผ่าน', resetPasswordDesc: 'พิมพ์อีเมลที่คุณใช้ลงทะเบียนบน Dify แล้วเราจะส่งอีเมลรีเซ็ตรหัสผ่านให้คุณ', diff --git a/web/i18n/th-TH/workflow.ts b/web/i18n/th-TH/workflow.ts index 45b61b011e..a9a1ca7923 100644 --- a/web/i18n/th-TH/workflow.ts +++ b/web/i18n/th-TH/workflow.ts @@ -930,6 +930,7 @@ const translation = { restoreSuccess: 'เวอร์ชันที่กู้คืน', restoreFailure: 'ไม่สามารถกู้คืนเวอร์ชันได้', updateSuccess: 'อัปเดตเวอร์ชัน', + copyIdSuccess: 'คัดลอกรหัสไปยังคลิปบอร์ด', }, releaseNotesPlaceholder: 'อธิบายว่าสิ่งที่เปลี่ยนแปลงไปคืออะไร', currentDraft: 'ร่างปัจจุบัน', @@ -940,6 +941,7 @@ const translation = { nameThisVersion: 'ชื่อเวอร์ชันนี้', title: 'เวอร์ชัน', latest: 'ล่าสุด', + copyId: 'คัดลอก ID', }, debug: { noData: { diff --git a/web/i18n/tr-TR/app-annotation.ts b/web/i18n/tr-TR/app-annotation.ts index f9b29bb711..a5974093e8 100644 --- a/web/i18n/tr-TR/app-annotation.ts +++ b/web/i18n/tr-TR/app-annotation.ts @@ -83,6 +83,16 @@ const translation = { configConfirmBtn: 'Kaydet', }, embeddingModelSwitchTip: 'Ek açıklama metin vektörleştirme modeli, model değiştirmek yeniden yerleştirilecek ve ek maliyetlere yol açacaktır.', + list: { + delete: { + title: 'Silmekte emin misin?', + }, + }, + batchAction: { + delete: 'Sil', + selected: 'Seçildi', + cancel: 'İptal et', + }, } export default translation diff --git a/web/i18n/tr-TR/app-debug.ts b/web/i18n/tr-TR/app-debug.ts index c9a5f7b585..152a00e428 100644 --- a/web/i18n/tr-TR/app-debug.ts +++ b/web/i18n/tr-TR/app-debug.ts @@ -197,6 +197,7 @@ const translation = { after: '', }, }, + contentEnableLabel: 'Etkin modere içerik', }, fileUpload: { title: 'Dosya Yükleme', @@ -294,6 +295,7 @@ const translation = { 'Toplu görevin yanıtını tamamlamasını bekleyin.', notSelectModel: 'Lütfen bir model seçin', waitForImgUpload: 'Lütfen görüntünün yüklenmesini bekleyin', + waitForFileUpload: 'Lütfen dosyanın/dosyaların yüklenmesini bekleyin', }, chatSubTitle: 'Talimatlar', completionSubTitle: 'Ön Prompt', @@ -329,36 +331,64 @@ const translation = { queryNoBeEmpty: 'Sorgu prompt\'ta ayarlanmalıdır', }, variableConfig: { - addModalTitle: 'Giriş Alanı Ekle', - editModalTitle: 'Giriş Alanı Düzenle', - description: 'Değişken ayarı {{varName}}', - fieldType: 'Alan türü', - string: 'Kısa Metin', - textInput: 'Kısa Metin', - paragraph: 'Paragraf', - select: 'Seçim', - number: 'Numara', - notSet: 'Ayarlanmamış, ön promptta {{input}} yazmayı deneyin', - stringTitle: 'Form metin kutusu seçenekleri', - maxLength: 'En uzunluk', - options: 'Seçenekler', - addOption: 'Seçenek ekle', - apiBasedVar: 'API tabanlı Değişken', - varName: 'Değişken Adı', - labelName: 'Etiket Adı', - inputPlaceholder: 'Lütfen girin', - content: 'İçerik', - required: 'Gerekli', - errorMsg: { + 'addModalTitle': 'Giriş Alanı Ekle', + 'editModalTitle': 'Giriş Alanı Düzenle', + 'description': 'Değişken ayarı {{varName}}', + 'fieldType': 'Alan türü', + 'string': 'Kısa Metin', + 'textInput': 'Kısa Metin', + 'paragraph': 'Paragraf', + 'select': 'Seçim', + 'number': 'Numara', + 'notSet': 'Ayarlanmamış, ön promptta {{input}} yazmayı deneyin', + 'stringTitle': 'Form metin kutusu seçenekleri', + 'maxLength': 'En uzunluk', + 'options': 'Seçenekler', + 'addOption': 'Seçenek ekle', + 'apiBasedVar': 'API tabanlı Değişken', + 'varName': 'Değişken Adı', + 'labelName': 'Etiket Adı', + 'inputPlaceholder': 'Lütfen girin', + 'content': 'İçerik', + 'required': 'Gerekli', + 'errorMsg': { varNameRequired: 'Değişken adı gereklidir', labelNameRequired: 'Etiket adı gereklidir', varNameCanBeRepeat: 'Değişken adı tekrar edemez', atLeastOneOption: 'En az bir seçenek gereklidir', optionRepeat: 'Yinelenen seçenekler var', }, - defaultValue: 'Varsayılan değer', - noDefaultValue: 'Varsayılan değer yok', - selectDefaultValue: 'Varsayılan değer seç', + 'defaultValue': 'Varsayılan değer', + 'noDefaultValue': 'Varsayılan değer yok', + 'selectDefaultValue': 'Varsayılan değer seç', + 'file': { + image: { + name: 'Resim', + }, + audio: { + name: 'Ses', + }, + document: { + name: 'Belge', + }, + video: { + name: 'Video', + }, + custom: { + description: 'Diğer dosya türlerini belirtin.', + createPlaceholder: ' Dosya uzantısı, örneğin .doc', + name: 'Diğer dosya türleri', + }, + supportFileTypes: 'Destek Dosya Türleri', + }, + 'hide': 'Gizlemek', + 'uploadFileTypes': 'Dosya Türlerini Yükle', + 'localUpload': 'Yerel Yükleme', + 'single-file': 'Tek Dosya', + 'multi-files': 'Dosya Listesi', + 'text-input': 'Kısa Metin', + 'both': 'Her ikisi', + 'maxNumberOfUploads': 'Maksimum yükleme sayısı', }, vision: { name: 'Görüş', @@ -376,6 +406,7 @@ const translation = { url: 'URL', uploadLimit: 'Yükleme Limiti', }, + onlySupportVisionModelTip: 'Yalnızca görme modellerini destekler', }, voice: { name: 'Konuşma', @@ -445,6 +476,7 @@ const translation = { score_threshold: 'Skor Eşiği', score_thresholdTip: 'Parça filtreleme için benzerlik eşiğini ayarlamak için kullanılır.', retrieveChangeTip: 'Dizin modunu ve geri alım modunu değiştirmek, bu Bilgi ile ilişkili uygulamaları etkileyebilir.', + embeddingModelRequired: 'Yapılandırılmış bir Gömme Modeli gereklidir', }, debugAsSingleModel: 'Tek Model Olarak Hata Ayıkla', debugAsMultipleModel: 'Çoklu Model Olarak Hata Ayıkla', @@ -486,6 +518,26 @@ const translation = { enabled: 'Etkinleştirildi', }, }, + codegen: { + generatedCodeTitle: 'Oluşturulan Kod', + overwriteConfirmTitle: 'Mevcut kodun üzerine yazılsın mı?', + applyChanges: 'Değişiklikleri Uygula', + generate: 'Oluşturmak', + noDataLine2: 'Kod önizlemesi burada gösterilecektir.', + title: 'Kod Oluşturucu', + apply: 'Uygulamak', + instructionPlaceholder: 'Oluşturmak istediğiniz kodun ayrıntılı açıklamasını girin.', + description: 'Kod Oluşturucu, talimatlarınıza göre yüksek kaliteli kod oluşturmak için yapılandırılmış modelleri kullanır. Lütfen açık ve ayrıntılı talimatlar verin.', + resTitle: 'Oluşturulan Kod', + noDataLine1: 'Solda kullanım durumunuzu açıklayın,', + loading: 'Kod oluşturuluyor...', + instruction: 'Talimat -ları', + overwriteConfirmMessage: 'Bu eylem mevcut kodun üzerine yazacaktır. Devam etmek istiyor musunuz?', + }, + warningMessage: { + timeoutExceeded: 'Zaman aşımı nedeniyle sonuçlar görüntülenmez. Tam sonuçları almak için lütfen günlüklere bakın.', + }, + noResult: 'Çıktı burada görüntülenecektir.', } export default translation diff --git a/web/i18n/tr-TR/app.ts b/web/i18n/tr-TR/app.ts index 05ad7c1378..1852ee29d2 100644 --- a/web/i18n/tr-TR/app.ts +++ b/web/i18n/tr-TR/app.ts @@ -29,21 +29,7 @@ const translation = { newApp: { startFromBlank: 'Boş Oluştur', startFromTemplate: 'Şablondan Oluştur', - captionAppType: 'Ne tür bir uygulama oluşturmak istiyorsunuz?', - chatbotDescription: 'Sohbete dayalı bir uygulama oluşturun. Bu uygulama, çoklu turlar halinde sürekli konuşmaya izin veren bir soru-cevap formatı kullanır.', - completionDescription: 'Prompt temelinde yüksek kaliteli metinler üreten bir uygulama oluşturun, örneğin makaleler, özetler, çeviriler ve daha fazlasını oluşturmak için.', - completionWarning: 'Bu tür bir uygulama artık desteklenmeyecek.', - agentDescription: 'Görevleri tamamlamak için araçları bağımsız olarak seçebilen bir zeki Agent oluşturun', - workflowDescription: 'Yüksek derecede özelleştirilebilir bir workflow ile yüksek kaliteli metinler üreten bir uygulama oluşturun. Deneyimli kullanıcılar için uygundur.', workflowWarning: 'Şu anda beta aşamasında', - chatbotType: 'Chatbot düzenleme yöntemi', - basic: 'Temel', - basicTip: 'Yeni başlayanlar için, daha sonra Chatflow\'a geçilebilir', - basicFor: 'YENİ BAŞLAYANLAR İÇİN', - basicDescription: 'Temel Orkestrasyon, yerleşik promptları değiştirme yeteneği olmadan, basit ayarlarla bir Chatbot uygulamasının orkestrasyonuna olanak tanır. Yeni başlayanlar için uygundur.', - advanced: 'Chatflow', - advancedFor: 'Gelişmiş kullanıcılar için', - advancedDescription: 'Workflow Orkestrasyonu, yerleşik promptları düzenleme yeteneği de dahil olmak üzere yüksek derecede özelleştirme sunarak Chatbotları workflow formunda düzenler. Deneyimli kullanıcılar için uygundur.', captionName: 'Uygulama simgesi & ismi', appNamePlaceholder: 'Uygulamanıza bir isim verin', captionDescription: 'Açıklama', diff --git a/web/i18n/tr-TR/dataset-documents.ts b/web/i18n/tr-TR/dataset-documents.ts index c40c111d5d..984aad5a0a 100644 --- a/web/i18n/tr-TR/dataset-documents.ts +++ b/web/i18n/tr-TR/dataset-documents.ts @@ -31,6 +31,7 @@ const translation = { sync: 'Senkronize et', pause: 'Duraklat', resume: 'Devam Et', + download: 'Dosyayı İndir', }, index: { enable: 'Etkinleştir', diff --git a/web/i18n/tr-TR/login.ts b/web/i18n/tr-TR/login.ts index b525dd0dd7..96832ae581 100644 --- a/web/i18n/tr-TR/login.ts +++ b/web/i18n/tr-TR/login.ts @@ -9,7 +9,6 @@ const translation = { namePlaceholder: 'Kullanıcı adınız', forget: 'Şifrenizi mi unuttunuz?', signBtn: 'Giriş yap', - sso: 'SSO ile devam et', installBtn: 'Kurulum', setAdminAccount: 'Yönetici hesabı ayarlama', setAdminAccountDesc: 'Yönetici hesabı için maksimum ayrıcalıklar, uygulama oluşturma ve LLM sağlayıcılarını yönetme gibi işlemler için kullanılabilir.', @@ -81,8 +80,8 @@ const translation = { verificationCodePlaceholder: '6 haneli kodu girin', useAnotherMethod: 'Başka bir yöntem kullanın', didNotReceiveCode: 'Kodu almadınız mı?', - tips: '{{email}} adresine bir doğrulama kodu gönderiyoruz', resend: 'Tekrar Gönder', + tipsPrefix: 'Bir doğrulama kodu gönderiyoruz', }, enterYourName: 'Lütfen kullanıcı adınızı giriniz', resetPassword: 'Şifre Sıfırlama', diff --git a/web/i18n/tr-TR/workflow.ts b/web/i18n/tr-TR/workflow.ts index 8fac474b26..499ba86807 100644 --- a/web/i18n/tr-TR/workflow.ts +++ b/web/i18n/tr-TR/workflow.ts @@ -931,6 +931,7 @@ const translation = { updateFailure: 'Sürüm güncellenemedi', updateSuccess: 'Sürüm güncellendi', deleteSuccess: 'Sürüm silindi', + copyIdSuccess: 'Kimlik panoya kopyalandı', }, latest: 'Sonuncu', currentDraft: 'Mevcut Taslak', @@ -941,6 +942,7 @@ const translation = { releaseNotesPlaceholder: 'Değişen şeyleri tanımlayın', nameThisVersion: 'Bu versiyona isim ver', deletionTip: 'Silme işlemi geri alınamaz, lütfen onaylayın.', + copyId: 'ID Kopyala', }, debug: { noData: { diff --git a/web/i18n/uk-UA/app-annotation.ts b/web/i18n/uk-UA/app-annotation.ts index 918cea529a..bda4037c9c 100644 --- a/web/i18n/uk-UA/app-annotation.ts +++ b/web/i18n/uk-UA/app-annotation.ts @@ -83,6 +83,16 @@ const translation = { configConfirmBtn: 'Зберегти', }, embeddingModelSwitchTip: 'Модель векторизації тексту анотації, перемикання моделей буде повторно вбудовано, що призведе до додаткових витрат.', + list: { + delete: { + title: 'Ви впевнені, що хочете видалити?', + }, + }, + batchAction: { + selected: 'Вибрано', + cancel: 'Скасувати', + delete: 'Видалити', + }, } export default translation diff --git a/web/i18n/uk-UA/app-debug.ts b/web/i18n/uk-UA/app-debug.ts index fe6fefa801..4a9d77b761 100644 --- a/web/i18n/uk-UA/app-debug.ts +++ b/web/i18n/uk-UA/app-debug.ts @@ -161,11 +161,16 @@ const translation = { title: 'ІНСТРУМЕНТИ', // TOOLBOX (all caps to convey its section title nature) }, moderation: { - title: 'Модерація контенту', // Content moderation - description: 'Захистіть вивід моделі, використовуючи API модерації або список конфіденційних слів.', // Secure model output... - allEnabled: 'Вміст ВВЕДЕННЯ/ВИВЕДЕННЯ ввімкнено', // INPUT/OUTPUT Content Enabled - inputEnabled: 'Вміст ВВЕДЕННЯ ввімкнено', // INPUT Content Enabled - outputEnabled: 'Вміст ВИВЕДЕННЯ ввімкнено', // OUTPUT Content Enabled + // Content moderation + title: 'Модерація контенту', + // Secure model output... + description: 'Захистіть вивід моделі, використовуючи API модерації або список конфіденційних слів.', + // INPUT/OUTPUT Content Enabled + allEnabled: 'Вміст ВВЕДЕННЯ/ВИВЕДЕННЯ ввімкнено', + // INPUT Content Enabled + inputEnabled: 'Вміст ВВЕДЕННЯ ввімкнено', + // OUTPUT Content Enabled + outputEnabled: 'Вміст ВИВЕДЕННЯ ввімкнено', modal: { title: 'Налаштування модерації вмісту', // Content moderation settings provider: { @@ -197,6 +202,7 @@ const translation = { after: '', }, }, + contentEnableLabel: 'Увімкнено помірний контент', }, fileUpload: { title: 'Завантаження файлу', @@ -248,23 +254,37 @@ const translation = { message: 'Скидання призводить до скасування змін, відновлюючи останню опубліковану конфігурацію.', }, errorMessage: { - nameOfKeyRequired: 'назва ключа: {{key}} обов’язкова', // name of the key: {{key}} required - valueOfVarRequired: 'значення {{key}} не може бути порожнім', // {{key}} value can not be empty - queryRequired: 'Текст запиту обов’язковий.', // Request text is required. - waitForResponse: 'Будь ласка, зачекайте, доки буде завершено відповідь на попереднє повідомлення.', // Please wait for the response to the previous message to complete. - waitForBatchResponse: 'Будь ласка, дочекайтеся завершення відповіді на пакетне завдання.', // Please wait for the response to the batch task to complete. - notSelectModel: 'Будь ласка, виберіть модель', // Please choose a model - waitForImgUpload: 'Будь ласка, зачекайте, поки зображення завантажиться', // Please wait for the image to upload + // name of the key: {{key}} required + nameOfKeyRequired: 'назва ключа: {{key}} обов’язкова', + // {{key}} value can not be empty + valueOfVarRequired: 'значення {{key}} не може бути порожнім', + // Request text is required. + queryRequired: 'Текст запиту обов’язковий.', + // Please wait for the response to the previous message to complete. + waitForResponse: 'Будь ласка, зачекайте, доки буде завершено відповідь на попереднє повідомлення.', + // Please wait for the response to the batch task to complete. + waitForBatchResponse: 'Будь ласка, дочекайтеся завершення відповіді на пакетне завдання.', + // Please choose a model + notSelectModel: 'Будь ласка, виберіть модель', + // Please wait for the image to upload + waitForImgUpload: 'Будь ласка, зачекайте, поки зображення завантажиться', + waitForFileUpload: 'Будь ласка, зачекайте, поки файл/файли завантажаться', }, - chatSubTitle: 'Інструкції', // Instructions - completionSubTitle: 'Префікс команди', // Prefix Prompt + // Instructions + chatSubTitle: 'Інструкції', + // Prefix Prompt + completionSubTitle: 'Префікс команди', promptTip: 'Запити керують відповідями ШІ, надаючи інструкції та обмеження. Вставте змінні, як-от {{input}}. Цей запит не буде видно користувачам.', - formattingChangedTitle: 'Змінено форматування', // Formatting changed - formattingChangedText: 'Змінення форматування призведе до скидання області налагодження. Ви впевнені?', // Modifying the formatting will reset the debug area, are you sure? - variableTitle: 'Змінні', // Variables + // Formatting changed + formattingChangedTitle: 'Змінено форматування', + // Modifying the formatting will reset the debug area, are you sure? + formattingChangedText: 'Змінення форматування призведе до скидання області налагодження. Ви впевнені?', + // Variables + variableTitle: 'Змінні', variableTip: 'Користувачі заповнюють змінні у формі, автоматично замінюючи змінні в команді.', notSetVar: 'Змінні дозволяють користувачам вводити підказки або вступні зауваження під час заповнення форм. Ви можете спробувати ввести "{{input}}" у слова підказки.', - autoAddVar: 'На невизначені змінні, на які посилаються в попередньому запиті, є посилання. Ви хочете додати їх у форму вводу користувача?', // Undefined variables referenced in pre-prompt, are you want to add them in user input form? + // Undefined variables referenced in pre-prompt, are you want to add them in user input form? + autoAddVar: 'На невизначені змінні, на які посилаються в попередньому запиті, є посилання. Ви хочете додати їх у форму вводу користувача?', variableTable: { key: 'Ключ змінної', // Variable Key name: 'Назва поля для введення користувача', // User Input Field Name @@ -316,11 +336,40 @@ const translation = { 'defaultValue': 'Значення за замовчуванням', 'noDefaultValue': 'Без значення за замовчуванням', 'selectDefaultValue': 'Обрати значення за замовчуванням', + 'file': { + image: { + name: 'Образ', + }, + audio: { + name: 'Аудіо', + }, + document: { + name: 'Документ', + }, + video: { + name: 'Відео', + }, + custom: { + description: 'Укажіть інші типи файлів.', + createPlaceholder: ' Розширення файлу, наприклад .doc', + name: 'Інші типи файлів', + }, + supportFileTypes: 'Підтримка типів файлів', + }, + 'content': 'Вміст', + 'both': 'Як', + 'single-file': 'Один файл', + 'multi-files': 'Список файлів', + 'localUpload': 'Локальне завантаження', + 'uploadFileTypes': 'Типи файлів для завантаження', + 'maxNumberOfUploads': 'Максимальна кількість завантажень', }, vision: { - name: 'Зображення', // Vision + // Vision + name: 'Зображення', description: 'Увімкнення функції "Зображення" дозволить моделі приймати зображення та відповідати на запитання про них.', - settings: 'Налаштування', // Settings + // Settings + settings: 'Налаштування', visionSettings: { title: 'Налаштування зображень', // Vision Settings resolution: 'Роздільна здатність', // Resolution @@ -335,6 +384,7 @@ const translation = { url: 'URL-адреса', // URL uploadLimit: 'Ліміт завантаження', // Upload Limit }, + onlySupportVisionModelTip: 'Підтримує лише моделі зору', }, voice: { name: 'Голос', // Voice @@ -384,9 +434,11 @@ const translation = { queryPlaceholder: 'Будь ласка, введіть текст запиту', // Please enter the request text. run: 'ЗАПУСТИТИ', // RUN }, - result: 'Вихідний текст', // Output Text + // Output Text + result: 'Вихідний текст', datasetConfig: { - settingTitle: 'Налаштування пошуку', // Retrieval settings + // Retrieval settings + settingTitle: 'Налаштування пошуку', knowledgeTip: 'Клацніть кнопку “+”, щоб додати знання', retrieveOneWay: { title: 'Односторонній пошук', // N-to-1 retrieval @@ -396,18 +448,28 @@ const translation = { title: 'Багатосторонній пошук', // Multi-path retrieval description: 'На основі намірів користувача запитує по всіх Базах Знань, отримує релевантний текст із кількох джерел і вибирає найкращі результати, що відповідають запиту користувача, після переранжування. Необхідна конфігурація API моделі переранжування.', }, - rerankModelRequired: 'Необхідна модель переранжування', // Rerank model is required - params: 'Параметри', // Params - top_k: 'Найкращих K', // Top K + // Rerank model is required + rerankModelRequired: 'Необхідна модель переранжування', + // Params + params: 'Параметри', + // Top K + top_k: 'Найкращих K', top_kTip: 'Використовується для фільтрації фрагментів, найбільш схожих на запитання користувачів. Система також динамічно регулюватиме значення K у відповідності з max_tokens обраної моделі.', - score_threshold: 'Поріг оцінки', // Score Threshold + // Score Threshold + score_threshold: 'Поріг оцінки', score_thresholdTip: 'Використовується для встановлення порогу схожості для фільтрації фрагментів.', - retrieveChangeTip: 'Зміна режиму індексування та режиму отримання може вплинути на застосунки, пов’язані з цими знаннями.', // Modifying... + // Modifying... + retrieveChangeTip: 'Зміна режиму індексування та режиму отримання може вплинути на застосунки, пов’язані з цими знаннями.', + embeddingModelRequired: 'Потрібна налаштована модель вбудовування', }, - debugAsSingleModel: 'Налагодження як одна модель', // Debug as Single Model - debugAsMultipleModel: 'Налагодження як багато моделей', // Debug as Multiple Models - duplicateModel: 'Дублювання', // Duplicate - publishAs: 'Опублікувати як', // Publish as + // Debug as Single Model + debugAsSingleModel: 'Налагодження як одна модель', + // Debug as Multiple Models + debugAsMultipleModel: 'Налагодження як багато моделей', + // Duplicate + duplicateModel: 'Дублювання', + // Publish as + publishAs: 'Опублікувати як', assistantType: { name: 'Тип Асистента', // Assistant Type chatAssistant: { @@ -444,6 +506,79 @@ const translation = { enabled: 'Увімкнено', // Enabled }, }, + codegen: { + generatedCodeTitle: 'Згенерований код', + generate: 'Генерувати', + title: 'Генератор коду', + loading: 'Генерація коду...', + instruction: 'Інструкції', + applyChanges: 'Застосувати зміни', + resTitle: 'Згенерований код', + noDataLine2: 'Тут з\'явиться попередній перегляд коду.', + noDataLine1: 'Опишіть свій випадок використання зліва,', + apply: 'Застосовувати', + overwriteConfirmTitle: 'Перезаписати існуючий код?', + overwriteConfirmMessage: 'Ця дія перезапише існуючий код. Хочете продовжити?', + instructionPlaceholder: 'Введіть детальний опис коду, який ви хочете згенерувати.', + description: 'Генератор коду використовує налаштовані моделі для генерації високоякісного коду на основі ваших інструкцій. Будь ласка, надайте чіткі та детальні інструкції.', + }, + generate: { + template: { + pythonDebugger: { + name: 'Налагоджувач Python', + instruction: 'Бот, який може генерувати та налагоджувати ваш код на основі ваших інструкцій', + }, + translation: { + name: 'Переклад', + instruction: 'Перекладач, який може перекладати кількома мовами', + }, + professionalAnalyst: { + name: 'Професійний аналітик', + instruction: 'Отримуйте аналітичні дані, виявляйте ризики та перетворюйте ключову інформацію з довгих звітів в єдину записку', + }, + excelFormulaExpert: { + name: 'Експерт з формул Excel', + instruction: 'Чат-бот, який може допомогти користувачам-початківцям розуміти, використовувати та створювати формули Excel на основі інструкцій користувача', + }, + travelPlanning: { + name: 'Планування подорожей', + instruction: 'Помічник із планування подорожей — це інтелектуальний інструмент, розроблений, щоб допомогти користувачам без зусиль планувати свої поїздки', + }, + SQLSorcerer: { + name: 'SQL чаклун', + instruction: 'Перетворюйте повсякденну мову на SQL-запити', + }, + GitGud: { + name: 'Git gud', + instruction: 'Генеруйте відповідні команди Git на основі описаних користувачем дій контролю версій', + }, + meetingTakeaways: { + name: 'Підсумки зустрічі', + instruction: 'Перетворіть зустрічі на стислі підсумки, включаючи теми для обговорення, ключові висновки та пункти дій', + }, + writingsPolisher: { + name: 'Письменницька полірувальна машина', + instruction: 'Використовуйте передові методи редагування тексту, щоб покращити свої тексти', + }, + }, + instruction: 'Інструкції', + generate: 'Генерувати', + apply: 'Застосовувати', + tryIt: 'Спробуйте', + overwriteTitle: 'Змінити існуючу конфігурацію?', + instructionPlaceHolder: 'Пишіть чіткі та конкретні інструкції.', + loading: 'Оркестрування програми для вас...', + noDataLine1: 'Опишіть свій випадок використання зліва,', + resTitle: 'Згенерований запит', + title: 'Генератор підказок', + noDataLine2: 'Тут буде показано попередній перегляд оркестровки.', + overwriteMessage: 'Застосування цього рядка замінить існуючу конфігурацію.', + description: 'Генератор підказок використовує налаштовану модель для оптимізації запитів для кращої якості та кращої структури. Напишіть, будь ласка, зрозумілу та детальну інструкцію.', + }, + warningMessage: { + timeoutExceeded: 'Результати не відображаються через тайм-аут. Будь ласка, зверніться до журналів, щоб отримати повні результати.', + }, + noResult: 'Тут буде відображено вихідні дані.', } export default translation diff --git a/web/i18n/uk-UA/app.ts b/web/i18n/uk-UA/app.ts index 26c059f727..77b98beebe 100644 --- a/web/i18n/uk-UA/app.ts +++ b/web/i18n/uk-UA/app.ts @@ -27,21 +27,7 @@ const translation = { newApp: { startFromBlank: 'Створити з нуля', startFromTemplate: 'Створити з шаблону', - captionAppType: 'Який тип додатка ви хочете створити?', - chatbotDescription: 'Побудуйте додаток на основі чату. Цей додаток використовує формат запитань та відповідей, що дозволяє проводити кілька раундів безперервного спілкування.', - completionDescription: 'Побудуйте додаток, який генерує текст високої якості на основі підказок, таких як генерація статей, резюме, перекладів тощо.', - completionWarning: 'Цей тип додатка більше не буде підтримуватися.', - agentDescription: 'Побудуйте інтелектуального агента, який може автономно обирати інструменти для виконання завдань', - workflowDescription: 'Побудуйте додаток, який генерує текст високої якості на основі робочого процесу з високим рівнем настроювання. Він підходить для досвідчених користувачів.', workflowWarning: 'Наразі в бета-версії', - chatbotType: 'Метод оркестрації чатботу', - basic: 'Базовий', - basicTip: 'Для початківців, можна перейти до Chatflow пізніше', - basicFor: 'ДЛЯ ПОЧАТКІВЦІВ', - basicDescription: 'Базовий оркестр дозволяє оркеструвати додаток чатбота за допомогою простих налаштувань, без можливості змінювати вбудовані підказки. Він підходить для початківців.', - advanced: 'Chatflow', - advancedFor: 'Для досвідчених користувачів', - advancedDescription: 'Оркестрування робочого процесу оркеструє чатботи у формі робочих процесів, пропонуючи високий рівень настроювання, включаючи можливість редагувати вбудовані підказки. Він підходить для досвідчених користувачів.', captionName: 'Іконка та назва додатка', appNamePlaceholder: 'Дайте назву вашому додатку', captionDescription: 'Опис', diff --git a/web/i18n/uk-UA/dataset-documents.ts b/web/i18n/uk-UA/dataset-documents.ts index 903e8a97c4..f4a40081c5 100644 --- a/web/i18n/uk-UA/dataset-documents.ts +++ b/web/i18n/uk-UA/dataset-documents.ts @@ -30,6 +30,7 @@ const translation = { sync: 'Синхронізувати', pause: 'Пауза', resume: 'Продовжити', + download: 'Завантажити файл', }, index: { enable: 'Активувати', diff --git a/web/i18n/uk-UA/login.ts b/web/i18n/uk-UA/login.ts index b586f3f243..a6b8d725e8 100644 --- a/web/i18n/uk-UA/login.ts +++ b/web/i18n/uk-UA/login.ts @@ -70,7 +70,6 @@ const translation = { activated: 'Увійти зараз', adminInitPassword: 'Пароль ініціалізації адміністратора', validate: 'Перевірити', - sso: 'Продовжуйте працювати з SSW', checkCode: { didNotReceiveCode: 'Не отримали код?', invalidCode: 'Невірний код', @@ -81,8 +80,8 @@ const translation = { verify: 'Перевірити', verificationCode: 'Код підтвердження', useAnotherMethod: 'Використовуйте інший спосіб', - tips: 'Ми надсилаємо код підтвердження на адресу {{email}}', validTime: 'Майте на увазі, що код дійсний протягом 5 хвилин', + tipsPrefix: 'Ми відправляємо код підтвердження на', }, back: 'Задній', backToLogin: 'Назад до входу', diff --git a/web/i18n/uk-UA/workflow.ts b/web/i18n/uk-UA/workflow.ts index f5cf52d8db..dea3704d85 100644 --- a/web/i18n/uk-UA/workflow.ts +++ b/web/i18n/uk-UA/workflow.ts @@ -930,6 +930,7 @@ const translation = { deleteSuccess: 'Версія видалена', restoreSuccess: 'Версія відновлена', updateFailure: 'Не вдалося оновити версію', + copyIdSuccess: 'ID скопійовано в буфер обміну', }, defaultName: 'Без назви версія', restorationTip: 'Після відновлення версії нинішній проект буде перезаписано.', @@ -940,6 +941,7 @@ const translation = { editVersionInfo: 'Редагувати інформацію про версію', nameThisVersion: 'Назвіть цю версію', latest: 'Останні новини', + copyId: 'Копіювати ідентифікатор', }, debug: { noData: { diff --git a/web/i18n/vi-VN/app-annotation.ts b/web/i18n/vi-VN/app-annotation.ts index 5b9f3b35a5..29499dcc21 100644 --- a/web/i18n/vi-VN/app-annotation.ts +++ b/web/i18n/vi-VN/app-annotation.ts @@ -83,6 +83,16 @@ const translation = { configConfirmBtn: 'Lưu', }, embeddingModelSwitchTip: 'Mô hình vector hóa văn bản chú thích, việc chuyển đổi mô hình sẽ dẫn đến việc nhúng lại, có thể phát sinh thêm chi phí.', + list: { + delete: { + title: 'Bạn có chắc chắn muốn xóa không?', + }, + }, + batchAction: { + delete: 'Xóa', + cancel: 'Hủy', + selected: 'Được chọn', + }, } export default translation diff --git a/web/i18n/vi-VN/app-debug.ts b/web/i18n/vi-VN/app-debug.ts index 381b766306..8882d4af38 100644 --- a/web/i18n/vi-VN/app-debug.ts +++ b/web/i18n/vi-VN/app-debug.ts @@ -197,6 +197,7 @@ const translation = { after: '', }, }, + contentEnableLabel: 'Đã bật nội dung kiểm duyệt', }, fileUpload: { title: 'Tải lên tệp', @@ -255,6 +256,7 @@ const translation = { waitForBatchResponse: 'Vui lòng đợi phản hồi của tác vụ hàng loạt để hoàn thành.', notSelectModel: 'Vui lòng chọn một mô hình', waitForImgUpload: 'Vui lòng đợi hình ảnh được tải lên', + waitForFileUpload: 'Vui lòng đợi tệp / tệp tải lên', }, chatSubTitle: 'Hướng dẫn', completionSubTitle: 'Tiền tố lời nhắc', @@ -316,6 +318,33 @@ const translation = { 'defaultValue': 'Giá trị mặc định', 'noDefaultValue': 'Không có giá trị mặc định', 'selectDefaultValue': 'Chọn giá trị mặc định', + 'file': { + image: { + name: 'Ảnh', + }, + audio: { + name: 'Âm thanh', + }, + document: { + name: 'Tài liệu', + }, + video: { + name: 'Video', + }, + custom: { + description: 'Chỉ định các loại tệp khác.', + name: 'Các loại tệp khác', + createPlaceholder: ' Phần mở rộng tệp, ví dụ: .doc', + }, + supportFileTypes: 'Các loại tệp hỗ trợ', + }, + 'both': 'Cả hai', + 'uploadFileTypes': 'Tải lên các loại tệp', + 'localUpload': 'Tải lên cục bộ', + 'single-file': 'Tệp đơn', + 'content': 'Nội dung', + 'multi-files': 'Danh sách tập tin', + 'maxNumberOfUploads': 'Số lượt tải lên tối đa', }, vision: { name: 'Thị giác', @@ -335,6 +364,7 @@ const translation = { url: 'URL', uploadLimit: 'Giới hạn tải lên', }, + onlySupportVisionModelTip: 'Chỉ hỗ trợ các mô hình thị giác', }, voice: { name: 'Giọng nói', @@ -403,6 +433,7 @@ const translation = { score_threshold: 'Ngưỡng điểm', score_thresholdTip: 'Sử dụng để thiết lập ngưỡng tương đồng cho việc lọc các phần.', retrieveChangeTip: 'Thay đổi chế độ chỉ mục và chế độ truy xuất có thể ảnh hưởng đến các ứng dụng liên quan đến kiến thức này.', + embeddingModelRequired: 'Cần có Mô hình nhúng được định cấu hình', }, debugAsSingleModel: 'Gỡ lỗi như một mô hình', debugAsMultipleModel: 'Gỡ lỗi như nhiều mô hình', @@ -444,6 +475,79 @@ const translation = { enabled: 'Đã kích hoạt', }, }, + codegen: { + generate: 'Đẻ ra', + instruction: 'Chỉ thị', + generatedCodeTitle: 'Mã được tạo', + loading: 'Đang tạo mã...', + title: 'Trình tạo mã', + instructionPlaceholder: 'Nhập mô tả chi tiết về mã bạn muốn tạo.', + overwriteConfirmMessage: 'Hành động này sẽ ghi đè lên mã hiện có. Bạn có muốn tiếp tục không?', + description: 'Trình tạo mã sử dụng các mô hình đã định cấu hình để tạo mã chất lượng cao dựa trên hướng dẫn của bạn. Vui lòng cung cấp hướng dẫn rõ ràng và chi tiết.', + resTitle: 'Mã được tạo', + apply: 'Áp dụng', + overwriteConfirmTitle: 'Ghi đè mã hiện có?', + applyChanges: 'Áp dụng thay đổi', + noDataLine1: 'Mô tả trường hợp sử dụng của bạn ở bên trái,', + noDataLine2: 'Bản xem trước mã sẽ hiển thị ở đây.', + }, + generate: { + template: { + pythonDebugger: { + instruction: 'Một bot có thể tạo và gỡ lỗi mã của bạn dựa trên hướng dẫn của bạn', + name: 'Trình gỡ lỗi Python', + }, + translation: { + name: 'Dịch', + instruction: 'Một dịch giả có thể dịch nhiều ngôn ngữ', + }, + professionalAnalyst: { + name: 'Chuyên viên phân tích chuyên nghiệp', + instruction: 'Trích xuất thông tin chi tiết, xác định rủi ro và chắt lọc thông tin quan trọng từ các báo cáo dài thành một bản ghi nhớ duy nhất', + }, + excelFormulaExpert: { + name: 'Chuyên gia công thức Excel', + instruction: 'Một chatbot có thể giúp người dùng mới hiểu, sử dụng và tạo công thức Excel dựa trên hướng dẫn của người dùng', + }, + travelPlanning: { + instruction: 'Trợ lý lập kế hoạch du lịch là một công cụ thông minh được thiết kế để giúp người dùng dễ dàng lên kế hoạch cho các chuyến đi của họ', + name: 'Lập kế hoạch du lịch', + }, + SQLSorcerer: { + instruction: 'Chuyển đổi ngôn ngữ hàng ngày thành truy vấn SQL', + name: 'SQL sorcerer', + }, + GitGud: { + name: 'Git gud', + instruction: 'Tạo các lệnh Git thích hợp dựa trên các hành động kiểm soát phiên bản được người dùng mô tả', + }, + meetingTakeaways: { + name: 'Bài học rút ra trong cuộc họp', + instruction: 'Chắt lọc các cuộc họp thành các bản tóm tắt ngắn gọn bao gồm các chủ đề thảo luận, bài học chính và các mục hành động', + }, + writingsPolisher: { + name: 'Máy đánh bóng viết', + instruction: 'Sử dụng các kỹ thuật chỉnh sửa nội dung nâng cao để cải thiện bài viết của bạn', + }, + }, + generate: 'Đẻ ra', + tryIt: 'Dùng thử', + noDataLine2: 'Bản xem trước Orchestration sẽ hiển thị ở đây.', + apply: 'Áp dụng', + instruction: 'Chỉ thị', + title: 'Trình tạo nhắc nhở', + resTitle: 'Lời nhắc được tạo', + loading: 'Sắp xếp ứng dụng cho bạn...', + noDataLine1: 'Mô tả trường hợp sử dụng của bạn ở bên trái,', + description: 'Trình tạo lời nhắc sử dụng mô hình được định cấu hình để tối ưu hóa lời nhắc cho chất lượng cao hơn và cấu trúc tốt hơn. Vui lòng viết hướng dẫn rõ ràng và chi tiết.', + overwriteMessage: 'Áp dụng lời nhắc này sẽ ghi đè cấu hình hiện có.', + overwriteTitle: 'Ghi đè cấu hình hiện có?', + instructionPlaceHolder: 'Viết hướng dẫn rõ ràng và cụ thể.', + }, + warningMessage: { + timeoutExceeded: 'Kết quả không được hiển thị do hết thời gian chờ. Vui lòng tham khảo nhật ký để thu thập kết quả đầy đủ.', + }, + noResult: 'Đầu ra sẽ được hiển thị ở đây.', } export default translation diff --git a/web/i18n/vi-VN/app-log.ts b/web/i18n/vi-VN/app-log.ts index 48ae3150b7..aad594dfa8 100644 --- a/web/i18n/vi-VN/app-log.ts +++ b/web/i18n/vi-VN/app-log.ts @@ -84,10 +84,8 @@ const translation = { fileListLabel: 'Chi tiết tệp', }, promptLog: 'Nhật ký lời nhắc', - AgentLog: 'Nhật ký tác nhân', viewLog: 'Xem nhật ký', agentLogDetail: { - AgentMode: 'Chế độ tác nhân', toolUsed: 'Công cụ đã sử dụng', iterations: 'Số lần lặp', iteration: 'Lần lặp', diff --git a/web/i18n/vi-VN/app.ts b/web/i18n/vi-VN/app.ts index 9ad2058330..7a992bef77 100644 --- a/web/i18n/vi-VN/app.ts +++ b/web/i18n/vi-VN/app.ts @@ -27,21 +27,7 @@ const translation = { newApp: { startFromBlank: 'Tạo mới', startFromTemplate: 'Tạo từ mẫu', - captionAppType: 'Bạn muốn tạo loại ứng dụng nào?', - chatbotDescription: 'Xây dựng một ứng dụng trò chuyện. Ứng dụng này sử dụng định dạng hỏi đáp, cho phép nhiều vòng trò chuyện liên tục.', - completionDescription: 'Xây dựng một ứng dụng tạo văn bản chất lượng cao dựa trên gợi ý, như tạo bài viết, tóm tắt, dịch thuật và nhiều hơn nữa.', - completionWarning: 'Loại ứng dụng này sẽ không được hỗ trợ trong tương lai.', - agentDescription: 'Xây dựng một tác nhân thông minh có thể tự động chọn công cụ để hoàn thành các nhiệm vụ', - workflowDescription: 'Xây dựng một ứng dụng tạo văn bản chất lượng cao dựa trên quy trình làm việc với mức độ tùy chỉnh cao. Phù hợp cho người dùng có kinh nghiệm.', workflowWarning: 'Hiện đang trong phiên bản beta', - chatbotType: 'Phương pháp quản lý Chatbot', - basic: 'Cơ bản', - basicTip: 'Dành cho người mới bắt đầu, có thể chuyển sang Chatflow sau này', - basicFor: 'DÀNH CHO NGƯỜI MỚI BẮT ĐẦU', - basicDescription: 'Quản lý cơ bản cho phép quản lý ứng dụng Chatbot bằng cách sử dụng các cài đặt đơn giản, không cần sửa đổi các lời nhắc tích hợp sẵn. Phù hợp cho người mới bắt đầu.', - advanced: 'Chatflow', - advancedFor: 'Dành cho người dùng có kinh nghiệm', - advancedDescription: 'Quản lý Chatbot dưới dạng các quy trình làm việc, cung cấp mức độ tùy chỉnh cao, bao gồm khả năng chỉnh sửa các lời nhắc tích hợp sẵn. Phù hợp cho người dùng có kinh nghiệm.', captionName: 'Biểu tượng và tên ứng dụng', appNamePlaceholder: 'Đặt tên cho ứng dụng của bạn', captionDescription: 'Mô tả', diff --git a/web/i18n/vi-VN/dataset-documents.ts b/web/i18n/vi-VN/dataset-documents.ts index c6fcd4ed45..1f514a1d6f 100644 --- a/web/i18n/vi-VN/dataset-documents.ts +++ b/web/i18n/vi-VN/dataset-documents.ts @@ -30,6 +30,7 @@ const translation = { sync: 'Đồng bộ', pause: 'Tạm dừng', resume: 'Tiếp tục', + download: 'Tải xuống tập tin', }, index: { enable: 'Kích hoạt', diff --git a/web/i18n/vi-VN/login.ts b/web/i18n/vi-VN/login.ts index 520d5250a8..1e770402dd 100644 --- a/web/i18n/vi-VN/login.ts +++ b/web/i18n/vi-VN/login.ts @@ -70,7 +70,6 @@ const translation = { activated: 'Đăng nhập ngay', adminInitPassword: 'Mật khẩu khởi tạo quản trị viên', validate: 'Xác thực', - sso: 'Tiếp tục với SSO', checkCode: { checkYourEmail: 'Kiểm tra email của bạn', verify: 'Xác minh', @@ -82,7 +81,7 @@ const translation = { useAnotherMethod: 'Sử dụng phương pháp khác', emptyCode: 'Mã là bắt buộc', verificationCodePlaceholder: 'Nhập mã gồm 6 chữ số', - tips: 'Chúng tôi gửi mã xác minh đến {{email}}', + tipsPrefix: 'Chúng tôi gửi mã xác minh đến', }, back: 'Lưng', withSSO: 'Tiếp tục với SSO', diff --git a/web/i18n/vi-VN/workflow.ts b/web/i18n/vi-VN/workflow.ts index 77f22613b4..257fd1ed67 100644 --- a/web/i18n/vi-VN/workflow.ts +++ b/web/i18n/vi-VN/workflow.ts @@ -930,6 +930,7 @@ const translation = { updateSuccess: 'Phiên bản đã được cập nhật', restoreSuccess: 'Phiên bản đã được khôi phục', restoreFailure: 'Không thể khôi phục phiên bản', + copyIdSuccess: 'ID được sao chép vào khay nhớ tạm', }, defaultName: 'Phiên bản không được đặt tên', releaseNotesPlaceholder: 'Mô tả những gì đã thay đổi', @@ -940,6 +941,7 @@ const translation = { nameThisVersion: 'Đặt tên cho phiên bản này', restorationTip: 'Sau khi phục hồi phiên bản, bản nháp hiện tại sẽ bị ghi đè.', title: 'Các phiên bản', + copyId: 'Sao chép ID', }, debug: { noData: { diff --git a/web/i18n/zh-Hans/dataset-documents.ts b/web/i18n/zh-Hans/dataset-documents.ts index 9ab18f1a20..a000e87663 100644 --- a/web/i18n/zh-Hans/dataset-documents.ts +++ b/web/i18n/zh-Hans/dataset-documents.ts @@ -32,6 +32,7 @@ const translation = { sync: '同步', pause: '暂停', resume: '恢复', + download: '下载文件', }, index: { enable: '启用中', diff --git a/web/i18n/zh-Hans/login.ts b/web/i18n/zh-Hans/login.ts index 2276436d0e..d0b2cbe8c5 100644 --- a/web/i18n/zh-Hans/login.ts +++ b/web/i18n/zh-Hans/login.ts @@ -79,7 +79,6 @@ const translation = { validate: '验证', checkCode: { checkYourEmail: '验证您的电子邮件', - tips: '验证码已经发送到您的邮箱 {{email}}', validTime: '请注意验证码 5 分钟内有效', verificationCode: '验证码', verificationCodePlaceholder: '输入 6 位验证码', @@ -89,6 +88,7 @@ const translation = { useAnotherMethod: '使用其他方式登录', emptyCode: '验证码不能为空', invalidCode: '验证码无效', + tipsPrefix: '我们发送一个验证码到', }, resetPassword: '重置密码', resetPasswordDesc: '请输入您的电子邮件地址以重置密码。我们将向您发送一封电子邮件。', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 2d4c5537f4..f085a20b0b 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -955,6 +955,7 @@ const translation = { defaultName: '未命名', nameThisVersion: '命名', editVersionInfo: '编辑信息', + copyId: '复制 ID', editField: { title: '标题', releaseNotes: '发布说明', @@ -971,6 +972,7 @@ const translation = { deleteFailure: '删除失败', updateSuccess: '版本信息已更新', updateFailure: '更新失败', + copyIdSuccess: 'ID 已复制到剪贴板', }, }, debug: { diff --git a/web/i18n/zh-Hant/app-annotation.ts b/web/i18n/zh-Hant/app-annotation.ts index 538546928c..b7416161d6 100644 --- a/web/i18n/zh-Hant/app-annotation.ts +++ b/web/i18n/zh-Hant/app-annotation.ts @@ -83,6 +83,15 @@ const translation = { configConfirmBtn: '儲存', }, embeddingModelSwitchTip: '標註文字向量化模型,切換模型會重新嵌入,產生額外費用消耗', + list: { + delete: { + title: '您確定要刪除嗎?', + }, + }, + batchAction: { + selected: '選擇的', + delete: '刪除', + }, } export default translation diff --git a/web/i18n/zh-Hant/app-debug.ts b/web/i18n/zh-Hant/app-debug.ts index 434bc830a5..5309f03da3 100644 --- a/web/i18n/zh-Hant/app-debug.ts +++ b/web/i18n/zh-Hant/app-debug.ts @@ -197,6 +197,7 @@ const translation = { after: '中配置 OpenAI API 金鑰。', }, }, + contentEnableLabel: '啟用了中等內容', }, fileUpload: { title: '檔案上傳', @@ -238,6 +239,7 @@ const translation = { waitForBatchResponse: '請等待批次任務完成', notSelectModel: '請選擇模型', waitForImgUpload: '請等待圖片上傳完成', + waitForFileUpload: '請等待檔上傳', }, chatSubTitle: '提示詞', completionSubTitle: '字首提示詞', @@ -302,6 +304,33 @@ const translation = { 'defaultValue': '預設值', 'noDefaultValue': '無預設值', 'selectDefaultValue': '選擇預設值', + 'file': { + image: { + name: '圖像', + }, + audio: { + name: '音訊', + }, + document: { + name: '公文', + }, + video: { + name: '視頻', + }, + custom: { + name: '其他文件類型', + description: '指定其他檔案類型。', + createPlaceholder: '檔擴展名,例如 .doc', + }, + supportFileTypes: '支援檔案類型', + }, + 'both': '雙', + 'uploadFileTypes': '上傳檔類型', + 'multi-files': '檔案清單', + 'content': '內容', + 'localUpload': '本地上傳', + 'single-file': '單個檔', + 'maxNumberOfUploads': '最大上傳次數', }, vision: { name: '視覺', @@ -321,6 +350,7 @@ const translation = { url: 'URL', uploadLimit: '上傳數量限制', }, + onlySupportVisionModelTip: '僅支持視覺模型', }, voice: { name: '音色', @@ -390,6 +420,7 @@ const translation = { score_threshold: 'Score 閾值', score_thresholdTip: '用於設定文字片段篩選的相似度閾值。', retrieveChangeTip: '修改索引模式和檢索模式可能會影響與該知識庫關聯的應用程式。', + embeddingModelRequired: '需要配置的嵌入模型', }, debugAsSingleModel: '單一模型進行除錯', debugAsMultipleModel: '多個模型進行除錯', @@ -431,6 +462,79 @@ const translation = { enabled: '啟用', }, }, + codegen: { + resTitle: '生成的代碼', + apply: '應用', + overwriteConfirmMessage: '此作將覆蓋現有代碼。你想繼續嗎?', + instruction: '指示', + instructionPlaceholder: '輸入要生成的代碼的詳細說明。', + generate: '生成', + noDataLine2: '代碼預覽將在此處顯示。', + applyChanges: '應用更改', + noDataLine1: '在左側描述您的用例,', + overwriteConfirmTitle: '覆蓋現有代碼?', + title: '代碼生成器', + generatedCodeTitle: '生成的代碼', + loading: '產生代碼...', + description: '代碼生成器使用配置的模型根據您的指令生成高質量的代碼。請提供清晰詳細的說明。', + }, + generate: { + template: { + pythonDebugger: { + instruction: '可以根據您的指令生成和調試代碼的機器人', + name: 'Python 調試器', + }, + translation: { + name: '譯本', + instruction: '可以翻譯多種語言的翻譯器', + }, + professionalAnalyst: { + instruction: '提取見解、識別風險並將長報告中的關鍵資訊提煉成單個備忘錄', + name: '專業分析師', + }, + excelFormulaExpert: { + name: 'Excel公式專家', + instruction: '一個聊天機器人,可以説明新手使用者根據使用者指令理解、使用和創建Excel公式', + }, + travelPlanning: { + instruction: '旅行計劃助手是一款智慧工具,旨在説明用戶輕鬆計劃他們的旅行', + name: '旅行計劃', + }, + SQLSorcerer: { + instruction: '將日常語言轉換為 SQL 查詢', + name: 'SQL 巫師', + }, + GitGud: { + instruction: '根據使用者描述的版本控制作生成適當的 Git 命令', + name: '吉特古德', + }, + meetingTakeaways: { + name: '會議要點', + instruction: '將會議提煉成簡潔的摘要,包括討論主題、關鍵要點和行動專案', + }, + writingsPolisher: { + instruction: '使用先進的文案編輯技術來改進您的寫作', + name: '書寫拋光機', + }, + }, + overwriteMessage: '應用此提示將覆蓋現有配置。', + tryIt: '試試看', + noDataLine1: '在左側描述您的用例,', + instruction: '指示', + description: '提示生成器使用配置的模型來優化提示,以獲得更高的品質和更好的結構。請寫出清晰詳細的說明。', + generate: '生成', + apply: '應用', + instructionPlaceHolder: '寫出清晰具體的說明。', + overwriteTitle: '覆蓋現有配置?', + title: '提示生成器', + loading: '為您編排應用程式...', + noDataLine2: '業務流程預覽將在此處顯示。', + resTitle: '生成的提示', + }, + warningMessage: { + timeoutExceeded: '由於超時,不顯示結果。請參閱日誌以收集完整結果。', + }, + noResult: '輸出將顯示在此處。', } export default translation diff --git a/web/i18n/zh-Hant/dataset-documents.ts b/web/i18n/zh-Hant/dataset-documents.ts index 1b482f181f..7344db2df7 100644 --- a/web/i18n/zh-Hant/dataset-documents.ts +++ b/web/i18n/zh-Hant/dataset-documents.ts @@ -30,6 +30,7 @@ const translation = { sync: '同步', resume: '恢復', pause: '暫停', + download: '下載檔案', }, index: { enable: '啟用中', diff --git a/web/i18n/zh-Hant/login.ts b/web/i18n/zh-Hant/login.ts index 8187323276..64f8122857 100644 --- a/web/i18n/zh-Hant/login.ts +++ b/web/i18n/zh-Hant/login.ts @@ -76,12 +76,12 @@ const translation = { didNotReceiveCode: '沒有收到驗證碼?', emptyCode: '驗證碼是必需的', checkYourEmail: '檢查您的電子郵件', - tips: '我們將驗證碼發送到 {{email}}', verificationCodePlaceholder: '輸入 6 位代碼', useAnotherMethod: '使用其他方法', validTime: '請記住,該代碼的有效期為 5 分鐘', verificationCode: '驗證碼', invalidCode: '無效代碼', + tipsPrefix: '我們發送一個驗證碼到', }, continueWithCode: 'Continue With Code', or: '或', diff --git a/web/i18n/zh-Hant/workflow.ts b/web/i18n/zh-Hant/workflow.ts index f522e990b0..6a5e990909 100644 --- a/web/i18n/zh-Hant/workflow.ts +++ b/web/i18n/zh-Hant/workflow.ts @@ -927,6 +927,7 @@ const translation = { updateSuccess: '版本已更新', deleteSuccess: '版本已刪除', deleteFailure: '無法刪除版本', + copyIdSuccess: 'ID 已複製到剪貼板', }, nameThisVersion: '給這個版本命名', latest: '最新', @@ -937,6 +938,7 @@ const translation = { deletionTip: '刪除是不可逆的,請確認。', releaseNotesPlaceholder: '描述發生了什麼變化', defaultName: '未命名版本', + copyId: '複製ID', }, debug: { noData: { diff --git a/web/models/common.ts b/web/models/common.ts index 867f4cf8fe..92aa263717 100644 --- a/web/models/common.ts +++ b/web/models/common.ts @@ -5,6 +5,18 @@ export type CommonResponse = { result: 'success' | 'fail' } +export type FileDownloadResponse = { + id: string + name: string + size: number + extension: string + url: string + download_url: string + mime_type: string + created_by: string + created_at: number +} + export type OauthResponse = { redirect_url: string } diff --git a/web/service/apps.ts b/web/service/apps.ts index 8e506a0987..1d7b0bccdb 100644 --- a/web/service/apps.ts +++ b/web/service/apps.ts @@ -9,7 +9,12 @@ export const fetchAppList: Fetcher(url, { params }) } -export const fetchAppDetail = ({ url, id }: { url: string; id: string }) => { +export const fetchAppDetail: Fetcher = ({ url, id }) => { + return get(`${url}/${id}`) +} + +// Direct API call function for non-SWR usage +export const fetchAppDetailDirect = async ({ url, id }: { url: string; id: string }): Promise => { return get(`${url}/${id}`) } diff --git a/web/service/knowledge/use-document.ts b/web/service/knowledge/use-document.ts index 5691128e7d..79fa113c22 100644 --- a/web/service/knowledge/use-document.ts +++ b/web/service/knowledge/use-document.ts @@ -8,7 +8,9 @@ import type { MetadataType, SortType } from '../datasets' import { pauseDocIndexing, resumeDocIndexing } from '../datasets' import type { DocumentDetailResponse, DocumentListResponse, UpdateDocumentBatchParams } from '@/models/datasets' import { DocumentActionType } from '@/models/datasets' -import type { CommonResponse } from '@/models/common' +import type { CommonResponse, FileDownloadResponse } from '@/models/common' +// Download document with authentication (sends Authorization header) +import Toast from '@/app/components/base/toast' const NAME_SPACE = 'knowledge/document' @@ -95,6 +97,21 @@ export const useSyncDocument = () => { }) } +// Download document with authentication (sends Authorization header) +export const useDocumentDownload = () => { + return useMutation({ + mutationFn: async ({ datasetId, documentId }: { datasetId: string; documentId: string }) => { + // The get helper automatically adds the Authorization header from localStorage + return get(`/datasets/${datasetId}/documents/${documentId}/upload-file`) + }, + onError: (error: any) => { + // Show a toast notification if download fails + const message = error?.message || 'Download failed.' + Toast.notify({ type: 'error', message }) + }, + }) +} + export const useSyncWebsite = () => { return useMutation({ mutationFn: ({ datasetId, documentId }: UpdateDocumentBatchParams) => { diff --git a/web/themes/manual-dark.css b/web/themes/manual-dark.css index 646c62b64f..e272aca95f 100644 --- a/web/themes/manual-dark.css +++ b/web/themes/manual-dark.css @@ -53,8 +53,8 @@ html[data-theme="dark"] { --color-dataset-option-card-orange-gradient: linear-gradient(90deg, #2B2322 0%, #1E1E21 100%); --color-dataset-chunk-list-mask-bg: linear-gradient(180deg, rgba(34, 34, 37, 0.00) 0%, #222225 100%); --mask-top2bottom-gray-50-to-transparent: linear-gradient(180deg, - rgba(24, 24, 27, 0.08) 0%, - rgba(0, 0, 0, 0) 100%); + rgba(29, 29, 32, 0.9) 0%, + rgba(29, 29, 32, 0.08) 100%); --color-line-divider-bg: linear-gradient(90deg, rgba(200, 206, 218, 0.14) 0%, rgba(0, 0, 0, 0) 100%); --color-access-app-icon-mask-bg: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.03) 100%); --color-premium-yearly-tip-text-background: linear-gradient(91deg, #FDB022 2.18%, #F79009 108.79%); diff --git a/web/themes/manual-light.css b/web/themes/manual-light.css index c01288118d..b1d14e86ab 100644 --- a/web/themes/manual-light.css +++ b/web/themes/manual-light.css @@ -53,8 +53,8 @@ html[data-theme="light"] { --color-dataset-option-card-orange-gradient: linear-gradient(90deg, #F8F2EE 0%, #F9FAFB 100%); --color-dataset-chunk-list-mask-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, #FCFCFD 100%); --mask-top2bottom-gray-50-to-transparent: linear-gradient(180deg, - rgba(200, 206, 218, 0.2) 0%, - rgba(255, 255, 255, 0) 100%); + rgba(242, 244, 247, 0.9) 0%, + rgba(242, 244, 247, 0.05) 100%); --color-line-divider-bg: linear-gradient(90deg, rgba(16, 24, 40, 0.08) 0%, rgba(255, 255, 255, 0) 100%); --color-access-app-icon-mask-bg: linear-gradient(135deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.08) 100%); --color-premium-yearly-tip-text-background: linear-gradient(91deg, #F79009 2.18%, #DC6803 108.79%); diff --git a/web/utils/navigation.ts b/web/utils/navigation.ts new file mode 100644 index 0000000000..fec2291f3f --- /dev/null +++ b/web/utils/navigation.ts @@ -0,0 +1,189 @@ +/** + * Navigation Utilities + * + * Provides helper functions for consistent navigation behavior throughout the application, + * specifically for preserving query parameters when navigating between related pages. + */ + +/** + * Creates a navigation path that preserves current URL query parameters + * + * @param basePath - The base path to navigate to (e.g., '/datasets/123/documents') + * @param preserveParams - Whether to preserve current query parameters (default: true) + * @returns The complete navigation path with preserved query parameters + * + * @example + * // Current URL: /datasets/123/documents/456?page=3&limit=10&keyword=test + * const backPath = createNavigationPath('/datasets/123/documents') + * // Returns: '/datasets/123/documents?page=3&limit=10&keyword=test' + * + * @example + * // Navigate without preserving params + * const cleanPath = createNavigationPath('/datasets/123/documents', false) + * // Returns: '/datasets/123/documents' + */ +export function createNavigationPath(basePath: string, preserveParams: boolean = true): string { + if (!preserveParams) + return basePath + + try { + const searchParams = new URLSearchParams(window.location.search) + const queryString = searchParams.toString() + const separator = queryString ? '?' : '' + return `${basePath}${separator}${queryString}` + } + catch (error) { + // Fallback to base path if there's any error accessing location + console.warn('Failed to preserve query parameters:', error) + return basePath + } +} + +/** + * Creates a back navigation function that preserves query parameters + * + * @param router - Next.js router instance + * @param basePath - The base path to navigate back to + * @param preserveParams - Whether to preserve current query parameters (default: true) + * @returns A function that navigates back with preserved parameters + * + * @example + * const router = useRouter() + * const backToPrev = createBackNavigation(router, `/datasets/${datasetId}/documents`) + * + * // Later, when user clicks back: + * backToPrev() + */ +export function createBackNavigation( + router: { push: (path: string) => void }, + basePath: string, + preserveParams: boolean = true, +): () => void { + return () => { + const navigationPath = createNavigationPath(basePath, preserveParams) + router.push(navigationPath) + } +} + +/** + * Extracts specific query parameters from current URL + * + * @param paramNames - Array of parameter names to extract + * @returns Object with extracted parameters + * + * @example + * // Current URL: /page?page=3&limit=10&keyword=test&other=value + * const params = extractQueryParams(['page', 'limit', 'keyword']) + * // Returns: { page: '3', limit: '10', keyword: 'test' } + */ +export function extractQueryParams(paramNames: string[]): Record { + try { + const searchParams = new URLSearchParams(window.location.search) + const extracted: Record = {} + + paramNames.forEach((name) => { + const value = searchParams.get(name) + if (value !== null) + extracted[name] = value + }) + + return extracted + } + catch (error) { + console.warn('Failed to extract query parameters:', error) + return {} + } +} + +/** + * Creates a navigation path with specific query parameters + * + * @param basePath - The base path + * @param params - Object of query parameters to include + * @returns Navigation path with specified parameters + * + * @example + * const path = createNavigationPathWithParams('/datasets/123/documents', { + * page: '1', + * limit: '25', + * keyword: 'search term' + * }) + * // Returns: '/datasets/123/documents?page=1&limit=25&keyword=search+term' + */ +export function createNavigationPathWithParams( + basePath: string, + params: Record, +): string { + try { + const searchParams = new URLSearchParams() + + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') + searchParams.set(key, String(value)) + }) + + const queryString = searchParams.toString() + const separator = queryString ? '?' : '' + return `${basePath}${separator}${queryString}` + } + catch (error) { + console.warn('Failed to create navigation path with params:', error) + return basePath + } +} + +/** + * Merges current query parameters with new ones + * + * @param newParams - New parameters to add or override + * @param preserveExisting - Whether to preserve existing parameters (default: true) + * @returns URLSearchParams object with merged parameters + * + * @example + * // Current URL: /page?page=3&limit=10 + * const merged = mergeQueryParams({ keyword: 'test', page: '1' }) + * // Results in: page=1&limit=10&keyword=test (page overridden, limit preserved, keyword added) + */ +export function mergeQueryParams( + newParams: Record, + preserveExisting: boolean = true, +): URLSearchParams { + const searchParams = preserveExisting + ? new URLSearchParams(window.location.search) + : new URLSearchParams() + + Object.entries(newParams).forEach(([key, value]) => { + if (value === null || value === undefined) + searchParams.delete(key) + else if (value !== '') + searchParams.set(key, String(value)) + }) + + return searchParams +} + +/** + * Navigation utilities for common dataset/document patterns + */ +export const datasetNavigation = { + /** + * Creates navigation back to dataset documents list with preserved state + */ + backToDocuments: (router: { push: (path: string) => void }, datasetId: string) => { + return createBackNavigation(router, `/datasets/${datasetId}/documents`) + }, + + /** + * Creates navigation to document detail + */ + toDocumentDetail: (router: { push: (path: string) => void }, datasetId: string, documentId: string) => { + return () => router.push(`/datasets/${datasetId}/documents/${documentId}`) + }, + + /** + * Creates navigation to document settings + */ + toDocumentSettings: (router: { push: (path: string) => void }, datasetId: string, documentId: string) => { + return () => router.push(`/datasets/${datasetId}/documents/${documentId}/settings`) + }, +} diff --git a/web/utils/var.ts b/web/utils/var.ts index 43bdde1aa2..7b032d3ec7 100644 --- a/web/utils/var.ts +++ b/web/utils/var.ts @@ -125,7 +125,7 @@ export function getMarketplaceUrl(path: string, params?: Record { +export const replaceSpaceWithUnderscoreInVarNameInput = (input: HTMLInputElement) => { const start = input.selectionStart const end = input.selectionEnd
-
- {t('datasetDocuments.list.table.header.fileName')} -
+ {renderSortHeader('name', t('datasetDocuments.list.table.header.fileName'))}
{t('datasetDocuments.list.table.header.chunkingMode')}{t('datasetDocuments.list.table.header.words')}{t('datasetDocuments.list.table.header.hitCount')} + {renderSortHeader('word_count', t('datasetDocuments.list.table.header.words'))} + -
- {t('datasetDocuments.list.table.header.uploadTime')} - -
+ {renderSortHeader('hit_count', t('datasetDocuments.list.table.header.hitCount'))} +
+ {renderSortHeader('created_at', t('datasetDocuments.list.table.header.uploadTime'))} {t('datasetDocuments.list.table.header.status')} {t('datasetDocuments.list.table.header.action')}