mirror of https://github.com/langgenius/dify.git
Merge branch 'main' into feat/rag-2
This commit is contained in:
commit
386614951f
1197
.env.example
1197
.env.example
File diff suppressed because it is too large
Load Diff
|
|
@ -82,7 +82,7 @@ jobs:
|
|||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
package_json_file: web/package.json
|
||||
run_install: false
|
||||
|
||||
- name: Setup NodeJS
|
||||
|
|
@ -95,10 +95,12 @@ jobs:
|
|||
|
||||
- name: Web dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
working-directory: ./web
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Web style check
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
working-directory: ./web
|
||||
run: pnpm run lint
|
||||
|
||||
docker-compose-template:
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ jobs:
|
|||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
package_json_file: web/package.json
|
||||
run_install: false
|
||||
|
||||
- name: Set up Node.js
|
||||
|
|
@ -59,10 +59,12 @@ jobs:
|
|||
|
||||
- name: Install dependencies
|
||||
if: env.FILES_CHANGED == 'true'
|
||||
working-directory: ./web
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Generate i18n translations
|
||||
if: env.FILES_CHANGED == 'true'
|
||||
working-directory: ./web
|
||||
run: pnpm run auto-gen-i18n ${{ env.FILE_ARGS }}
|
||||
|
||||
- name: Create Pull Request
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ jobs:
|
|||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
package_json_file: web/package.json
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
|
|
@ -48,8 +48,10 @@ jobs:
|
|||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
working-directory: ./web
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run tests
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
working-directory: ./web
|
||||
run: pnpm test
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Dify is an open-source platform for developing LLM applications with an intuitive interface combining agentic AI workflows, RAG pipelines, agent capabilities, and model management.
|
||||
|
||||
The codebase consists of:
|
||||
- **Backend API** (`/api`): Python Flask application with Domain-Driven Design architecture
|
||||
- **Frontend Web** (`/web`): Next.js 15 application with TypeScript and React 19
|
||||
- **Docker deployment** (`/docker`): Containerized deployment configurations
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Backend (API)
|
||||
|
||||
All Python commands must be prefixed with `uv run --project api`:
|
||||
|
||||
```bash
|
||||
# Start development servers
|
||||
./dev/start-api # Start API server
|
||||
./dev/start-worker # Start Celery worker
|
||||
|
||||
# Run tests
|
||||
uv run --project api pytest # Run all tests
|
||||
uv run --project api pytest tests/unit_tests/ # Unit tests only
|
||||
uv run --project api pytest tests/integration_tests/ # Integration tests
|
||||
|
||||
# Code quality
|
||||
./dev/reformat # Run all formatters and linters
|
||||
uv run --project api ruff check --fix ./ # Fix linting issues
|
||||
uv run --project api ruff format ./ # Format code
|
||||
uv run --project api mypy . # Type checking
|
||||
```
|
||||
|
||||
### Frontend (Web)
|
||||
|
||||
```bash
|
||||
cd web
|
||||
pnpm lint # Run ESLint
|
||||
pnpm eslint-fix # Fix ESLint issues
|
||||
pnpm test # Run Jest tests
|
||||
```
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
### Backend Testing
|
||||
- Use `pytest` for all backend tests
|
||||
- Write tests first (TDD approach)
|
||||
- Test structure: Arrange-Act-Assert
|
||||
|
||||
## Code Style Requirements
|
||||
|
||||
### Python
|
||||
- Use type hints for all functions and class attributes
|
||||
- No `Any` types unless absolutely necessary
|
||||
- Implement special methods (`__repr__`, `__str__`) appropriately
|
||||
|
||||
### TypeScript/JavaScript
|
||||
- Strict TypeScript configuration
|
||||
- ESLint with Prettier integration
|
||||
- Avoid `any` type
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Environment Variables**: Always use UV for Python commands: `uv run --project api <command>`
|
||||
- **Comments**: Only write meaningful comments that explain "why", not "what"
|
||||
- **File Creation**: Always prefer editing existing files over creating new ones
|
||||
- **Documentation**: Don't create documentation files unless explicitly requested
|
||||
- **Code Quality**: Always run `./dev/reformat` before committing backend changes
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Adding a New API Endpoint
|
||||
1. Create controller in `/api/controllers/`
|
||||
2. Add service logic in `/api/services/`
|
||||
3. Update routes in controller's `__init__.py`
|
||||
4. Write tests in `/api/tests/`
|
||||
|
||||
## Project-Specific Conventions
|
||||
|
||||
- All async tasks use Celery with Redis as broker
|
||||
|
|
@ -225,7 +225,8 @@ Deploy Dify to AWS with [CDK](https://aws.amazon.com/cdk/)
|
|||
|
||||
##### AWS
|
||||
|
||||
- [AWS CDK by @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK by @KevinZhao (EKS based)](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK by @tmokmss (ECS based)](https://github.com/aws-samples/dify-self-hosted-on-aws)
|
||||
|
||||
#### Using Alibaba Cloud Computing Nest
|
||||
|
||||
|
|
|
|||
|
|
@ -208,7 +208,8 @@ docker compose up -d
|
|||
|
||||
##### AWS
|
||||
|
||||
- [AWS CDK بواسطة @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK بواسطة @KevinZhao (EKS based)](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK بواسطة @tmokmss (ECS based)](https://github.com/aws-samples/dify-self-hosted-on-aws)
|
||||
|
||||
#### استخدام Alibaba Cloud للنشر
|
||||
[بسرعة نشر Dify إلى سحابة علي بابا مع عش الحوسبة السحابية علي بابا](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Dify%E7%A4%BE%E5%8C%BA%E7%89%88)
|
||||
|
|
|
|||
|
|
@ -225,7 +225,8 @@ GitHub-এ ডিফাইকে স্টার দিয়ে রাখুন
|
|||
|
||||
##### AWS
|
||||
|
||||
- [AWS CDK by @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK by @KevinZhao (EKS based)](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK by @tmokmss (ECS based)](https://github.com/aws-samples/dify-self-hosted-on-aws)
|
||||
|
||||
#### Alibaba Cloud ব্যবহার করে ডিপ্লয়
|
||||
|
||||
|
|
|
|||
|
|
@ -223,7 +223,8 @@ docker compose up -d
|
|||
使用 [CDK](https://aws.amazon.com/cdk/) 将 Dify 部署到 AWS
|
||||
|
||||
##### AWS
|
||||
- [AWS CDK by @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK by @KevinZhao (EKS based)](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK by @tmokmss (ECS based)](https://github.com/aws-samples/dify-self-hosted-on-aws)
|
||||
|
||||
#### 使用 阿里云计算巢 部署
|
||||
|
||||
|
|
|
|||
|
|
@ -220,7 +220,8 @@ Stellen Sie Dify mit nur einem Klick mithilfe von [terraform](https://www.terraf
|
|||
Bereitstellung von Dify auf AWS mit [CDK](https://aws.amazon.com/cdk/)
|
||||
|
||||
##### AWS
|
||||
- [AWS CDK by @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK by @KevinZhao (EKS based)](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK by @tmokmss (ECS based)](https://github.com/aws-samples/dify-self-hosted-on-aws)
|
||||
|
||||
#### Alibaba Cloud
|
||||
|
||||
|
|
|
|||
|
|
@ -220,7 +220,8 @@ Despliega Dify en una plataforma en la nube con un solo clic utilizando [terrafo
|
|||
Despliegue Dify en AWS usando [CDK](https://aws.amazon.com/cdk/)
|
||||
|
||||
##### AWS
|
||||
- [AWS CDK por @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK por @KevinZhao (EKS based)](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK por @tmokmss (ECS based)](https://github.com/aws-samples/dify-self-hosted-on-aws)
|
||||
|
||||
#### Alibaba Cloud
|
||||
|
||||
|
|
|
|||
|
|
@ -218,7 +218,8 @@ Déployez Dify sur une plateforme cloud en un clic en utilisant [terraform](http
|
|||
Déployez Dify sur AWS en utilisant [CDK](https://aws.amazon.com/cdk/)
|
||||
|
||||
##### AWS
|
||||
- [AWS CDK par @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK par @KevinZhao (EKS based)](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK par @tmokmss (ECS based)](https://github.com/aws-samples/dify-self-hosted-on-aws)
|
||||
|
||||
#### Alibaba Cloud
|
||||
|
||||
|
|
|
|||
|
|
@ -219,7 +219,8 @@ docker compose up -d
|
|||
[CDK](https://aws.amazon.com/cdk/) を使用して、DifyをAWSにデプロイします
|
||||
|
||||
##### AWS
|
||||
- [@KevinZhaoによるAWS CDK](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [@KevinZhaoによるAWS CDK (EKS based)](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [@tmokmssによるAWS CDK (ECS based)](https://github.com/aws-samples/dify-self-hosted-on-aws)
|
||||
|
||||
#### Alibaba Cloud
|
||||
[Alibaba Cloud Computing Nest](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Dify%E7%A4%BE%E5%8C%BA%E7%89%88)
|
||||
|
|
|
|||
|
|
@ -218,7 +218,8 @@ wa'logh nIqHom neH ghun deployment toy'wI' [terraform](https://www.terraform.io/
|
|||
wa'logh nIqHom neH ghun deployment toy'wI' [CDK](https://aws.amazon.com/cdk/) lo'laH.
|
||||
|
||||
##### AWS
|
||||
- [AWS CDK qachlot @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK qachlot @KevinZhao (EKS based)](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK qachlot @tmokmss (ECS based)](https://github.com/aws-samples/dify-self-hosted-on-aws)
|
||||
|
||||
#### Alibaba Cloud
|
||||
|
||||
|
|
|
|||
|
|
@ -212,7 +212,8 @@ Dify를 Kubernetes에 배포하고 프리미엄 스케일링 설정을 구성했
|
|||
[CDK](https://aws.amazon.com/cdk/)를 사용하여 AWS에 Dify 배포
|
||||
|
||||
##### AWS
|
||||
- [KevinZhao의 AWS CDK](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [KevinZhao의 AWS CDK (EKS based)](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [tmokmss의 AWS CDK (ECS based)](https://github.com/aws-samples/dify-self-hosted-on-aws)
|
||||
|
||||
#### Alibaba Cloud
|
||||
|
||||
|
|
|
|||
|
|
@ -217,7 +217,8 @@ Implante o Dify na Plataforma Cloud com um único clique usando [terraform](http
|
|||
Implante o Dify na AWS usando [CDK](https://aws.amazon.com/cdk/)
|
||||
|
||||
##### AWS
|
||||
- [AWS CDK por @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK por @KevinZhao (EKS based)](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK por @tmokmss (ECS based)](https://github.com/aws-samples/dify-self-hosted-on-aws)
|
||||
|
||||
#### Alibaba Cloud
|
||||
|
||||
|
|
|
|||
|
|
@ -218,7 +218,8 @@ namestite Dify v Cloud Platform z enim klikom z uporabo [terraform](https://www.
|
|||
Uvedite Dify v AWS z uporabo [CDK](https://aws.amazon.com/cdk/)
|
||||
|
||||
##### AWS
|
||||
- [AWS CDK by @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK by @KevinZhao (EKS based)](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK by @tmokmss (ECS based)](https://github.com/aws-samples/dify-self-hosted-on-aws)
|
||||
|
||||
#### Alibaba Cloud
|
||||
|
||||
|
|
|
|||
|
|
@ -211,7 +211,8 @@ Dify'ı bulut platformuna tek tıklamayla dağıtın [terraform](https://www.ter
|
|||
[CDK](https://aws.amazon.com/cdk/) kullanarak Dify'ı AWS'ye dağıtın
|
||||
|
||||
##### AWS
|
||||
- [AWS CDK tarafından @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK tarafından @KevinZhao (EKS based)](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK tarafından @tmokmss (ECS based)](https://github.com/aws-samples/dify-self-hosted-on-aws)
|
||||
|
||||
#### Alibaba Cloud
|
||||
|
||||
|
|
|
|||
|
|
@ -223,7 +223,8 @@ Dify 的所有功能都提供相應的 API,因此您可以輕鬆地將 Dify
|
|||
|
||||
### AWS
|
||||
|
||||
- [由 @KevinZhao 提供的 AWS CDK](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [由 @KevinZhao 提供的 AWS CDK (EKS based)](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [由 @tmokmss 提供的 AWS CDK (ECS based)](https://github.com/aws-samples/dify-self-hosted-on-aws)
|
||||
|
||||
#### 使用 阿里云计算巢進行部署
|
||||
|
||||
|
|
|
|||
|
|
@ -213,7 +213,8 @@ Triển khai Dify lên nền tảng đám mây với một cú nhấp chuột b
|
|||
Triển khai Dify trên AWS bằng [CDK](https://aws.amazon.com/cdk/)
|
||||
|
||||
##### AWS
|
||||
- [AWS CDK bởi @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK bởi @KevinZhao (EKS based)](https://github.com/aws-samples/solution-for-deploying-dify-on-aws)
|
||||
- [AWS CDK bởi @tmokmss (ECS based)](https://github.com/aws-samples/dify-self-hosted-on-aws)
|
||||
|
||||
|
||||
#### Alibaba Cloud
|
||||
|
|
|
|||
|
|
@ -42,6 +42,15 @@ REDIS_PORT=6379
|
|||
REDIS_USERNAME=
|
||||
REDIS_PASSWORD=difyai123456
|
||||
REDIS_USE_SSL=false
|
||||
# SSL configuration for Redis (when REDIS_USE_SSL=true)
|
||||
REDIS_SSL_CERT_REQS=CERT_NONE
|
||||
# Options: CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED
|
||||
REDIS_SSL_CA_CERTS=
|
||||
# Path to CA certificate file for SSL verification
|
||||
REDIS_SSL_CERTFILE=
|
||||
# Path to client certificate file for SSL authentication
|
||||
REDIS_SSL_KEYFILE=
|
||||
# Path to client private key file for SSL authentication
|
||||
REDIS_DB=0
|
||||
|
||||
# redis Sentinel configuration.
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ def initialize_extensions(app: DifyApp):
|
|||
ext_login,
|
||||
ext_mail,
|
||||
ext_migrate,
|
||||
ext_orjson,
|
||||
ext_otel,
|
||||
ext_proxy_fix,
|
||||
ext_redis,
|
||||
|
|
@ -67,6 +68,7 @@ def initialize_extensions(app: DifyApp):
|
|||
ext_logging,
|
||||
ext_warnings,
|
||||
ext_import_modules,
|
||||
ext_orjson,
|
||||
ext_set_secretkey,
|
||||
ext_compress,
|
||||
ext_code_based_extension,
|
||||
|
|
|
|||
|
|
@ -39,6 +39,26 @@ class RedisConfig(BaseSettings):
|
|||
default=False,
|
||||
)
|
||||
|
||||
REDIS_SSL_CERT_REQS: str = Field(
|
||||
description="SSL certificate requirements (CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED)",
|
||||
default="CERT_NONE",
|
||||
)
|
||||
|
||||
REDIS_SSL_CA_CERTS: Optional[str] = Field(
|
||||
description="Path to the CA certificate file for SSL verification",
|
||||
default=None,
|
||||
)
|
||||
|
||||
REDIS_SSL_CERTFILE: Optional[str] = Field(
|
||||
description="Path to the client certificate file for SSL authentication",
|
||||
default=None,
|
||||
)
|
||||
|
||||
REDIS_SSL_KEYFILE: Optional[str] = Field(
|
||||
description="Path to the client private key file for SSL authentication",
|
||||
default=None,
|
||||
)
|
||||
|
||||
REDIS_USE_SENTINEL: Optional[bool] = Field(
|
||||
description="Enable Redis Sentinel mode for high availability",
|
||||
default=False,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import logging
|
||||
|
||||
import flask_login
|
||||
from flask import request
|
||||
from flask_restful import Resource, reqparse
|
||||
from werkzeug.exceptions import InternalServerError, NotFound
|
||||
|
||||
|
|
@ -24,6 +25,7 @@ from core.errors.error import (
|
|||
ProviderTokenNotInitError,
|
||||
QuotaExceededError,
|
||||
)
|
||||
from core.helper.trace_id_helper import get_external_trace_id
|
||||
from core.model_runtime.errors.invoke import InvokeError
|
||||
from libs import helper
|
||||
from libs.helper import uuid_value
|
||||
|
|
@ -115,6 +117,10 @@ class ChatMessageApi(Resource):
|
|||
streaming = args["response_mode"] != "blocking"
|
||||
args["auto_generate_name"] = False
|
||||
|
||||
external_trace_id = get_external_trace_id(request)
|
||||
if external_trace_id:
|
||||
args["external_trace_id"] = external_trace_id
|
||||
|
||||
account = flask_login.current_user
|
||||
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from core.app.app_config.features.file_upload.manager import FileUploadConfigMan
|
|||
from core.app.apps.base_app_queue_manager import AppQueueManager
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from core.file.models import File
|
||||
from core.helper.trace_id_helper import get_external_trace_id
|
||||
from extensions.ext_database import db
|
||||
from factories import file_factory, variable_factory
|
||||
from fields.workflow_fields import workflow_fields, workflow_pagination_fields
|
||||
|
|
@ -185,6 +186,10 @@ class AdvancedChatDraftWorkflowRunApi(Resource):
|
|||
|
||||
args = parser.parse_args()
|
||||
|
||||
external_trace_id = get_external_trace_id(request)
|
||||
if external_trace_id:
|
||||
args["external_trace_id"] = external_trace_id
|
||||
|
||||
try:
|
||||
response = AppGenerateService.generate(
|
||||
app_model=app_model, user=current_user, args=args, invoke_from=InvokeFrom.DEBUGGER, streaming=True
|
||||
|
|
@ -373,6 +378,10 @@ class DraftWorkflowRunApi(Resource):
|
|||
parser.add_argument("files", type=list, required=False, location="json")
|
||||
args = parser.parse_args()
|
||||
|
||||
external_trace_id = get_external_trace_id(request)
|
||||
if external_trace_id:
|
||||
args["external_trace_id"] = external_trace_id
|
||||
|
||||
try:
|
||||
response = AppGenerateService.generate(
|
||||
app_model=app_model,
|
||||
|
|
|
|||
|
|
@ -163,11 +163,11 @@ class WorkflowVariableCollectionApi(Resource):
|
|||
draft_var_srv = WorkflowDraftVariableService(
|
||||
session=session,
|
||||
)
|
||||
workflow_vars = draft_var_srv.list_variables_without_values(
|
||||
app_id=app_model.id,
|
||||
page=args.page,
|
||||
limit=args.limit,
|
||||
)
|
||||
workflow_vars = draft_var_srv.list_variables_without_values(
|
||||
app_id=app_model.id,
|
||||
page=args.page,
|
||||
limit=args.limit,
|
||||
)
|
||||
|
||||
return workflow_vars
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class VersionApi(Resource):
|
|||
return result
|
||||
|
||||
try:
|
||||
response = requests.get(check_update_url, {"current_version": args.get("current_version")})
|
||||
response = requests.get(check_update_url, {"current_version": args.get("current_version")}, timeout=(3, 10))
|
||||
except Exception as error:
|
||||
logging.warning("Check update version error: %s.", str(error))
|
||||
result["version"] = args.get("current_version")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import json
|
||||
|
||||
from flask_restful import Resource, marshal_with, reqparse
|
||||
from flask_restful.inputs import int_range
|
||||
from sqlalchemy.orm import Session
|
||||
|
|
@ -136,12 +134,15 @@ class ConversationVariableDetailApi(Resource):
|
|||
variable_id = str(variable_id)
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("value", required=True, location="json")
|
||||
# using lambda is for passing the already-typed value without modification
|
||||
# if no lambda, it will be converted to string
|
||||
# the string cannot be converted using json.loads
|
||||
parser.add_argument("value", required=True, location="json", type=lambda x: x)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
return ConversationService.update_conversation_variable(
|
||||
app_model, conversation_id, variable_id, end_user, json.loads(args["value"])
|
||||
app_model, conversation_id, variable_id, end_user, args["value"]
|
||||
)
|
||||
except services.errors.conversation.ConversationNotExistsError:
|
||||
raise NotFound("Conversation Not Exists.")
|
||||
|
|
|
|||
|
|
@ -140,7 +140,9 @@ class ChatAppGenerator(MessageBasedAppGenerator):
|
|||
)
|
||||
|
||||
# get tracing instance
|
||||
trace_manager = TraceQueueManager(app_id=app_model.id)
|
||||
trace_manager = TraceQueueManager(
|
||||
app_id=app_model.id, user_id=user.id if isinstance(user, Account) else user.session_id
|
||||
)
|
||||
|
||||
# init application generate entity
|
||||
application_generate_entity = ChatAppGenerateEntity(
|
||||
|
|
|
|||
|
|
@ -124,7 +124,9 @@ class CompletionAppGenerator(MessageBasedAppGenerator):
|
|||
)
|
||||
|
||||
# get tracing instance
|
||||
trace_manager = TraceQueueManager(app_model.id)
|
||||
trace_manager = TraceQueueManager(
|
||||
app_id=app_model.id, user_id=user.id if isinstance(user, Account) else user.session_id
|
||||
)
|
||||
|
||||
# init application generate entity
|
||||
application_generate_entity = CompletionAppGenerateEntity(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ from core.app.entities.queue_entities import (
|
|||
MessageQueueMessage,
|
||||
QueueAdvancedChatMessageEndEvent,
|
||||
QueueErrorEvent,
|
||||
QueueMessage,
|
||||
QueueMessageEndEvent,
|
||||
QueueStopEvent,
|
||||
)
|
||||
|
|
@ -22,15 +21,6 @@ class MessageBasedAppQueueManager(AppQueueManager):
|
|||
self._app_mode = app_mode
|
||||
self._message_id = str(message_id)
|
||||
|
||||
def construct_queue_message(self, event: AppQueueEvent) -> QueueMessage:
|
||||
return MessageQueueMessage(
|
||||
task_id=self._task_id,
|
||||
message_id=self._message_id,
|
||||
conversation_id=self._conversation_id,
|
||||
app_mode=self._app_mode,
|
||||
event=event,
|
||||
)
|
||||
|
||||
def _publish(self, event: AppQueueEvent, pub_from: PublishFrom) -> None:
|
||||
"""
|
||||
Publish event to queue
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from base64 import b64encode
|
|||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from core.variables.utils import SegmentJSONEncoder
|
||||
from core.variables.utils import dumps_with_segments
|
||||
|
||||
|
||||
class TemplateTransformer(ABC):
|
||||
|
|
@ -93,7 +93,7 @@ class TemplateTransformer(ABC):
|
|||
|
||||
@classmethod
|
||||
def serialize_inputs(cls, inputs: Mapping[str, Any]) -> str:
|
||||
inputs_json_str = json.dumps(inputs, ensure_ascii=False, cls=SegmentJSONEncoder).encode()
|
||||
inputs_json_str = dumps_with_segments(inputs, ensure_ascii=False).encode()
|
||||
input_base64_encoded = b64encode(inputs_json_str).decode("utf-8")
|
||||
return input_base64_encoded
|
||||
|
||||
|
|
|
|||
|
|
@ -16,15 +16,33 @@ def get_external_trace_id(request: Any) -> Optional[str]:
|
|||
"""
|
||||
Retrieve the trace_id from the request.
|
||||
|
||||
Priority: header ('X-Trace-Id'), then parameters, then JSON body. Returns None if not provided or invalid.
|
||||
Priority:
|
||||
1. header ('X-Trace-Id')
|
||||
2. parameters
|
||||
3. JSON body
|
||||
4. Current OpenTelemetry context (if enabled)
|
||||
5. OpenTelemetry traceparent header (if present and valid)
|
||||
|
||||
Returns None if no valid trace_id is provided.
|
||||
"""
|
||||
trace_id = request.headers.get("X-Trace-Id")
|
||||
|
||||
if not trace_id:
|
||||
trace_id = request.args.get("trace_id")
|
||||
|
||||
if not trace_id and getattr(request, "is_json", False):
|
||||
json_data = getattr(request, "json", None)
|
||||
if json_data:
|
||||
trace_id = json_data.get("trace_id")
|
||||
|
||||
if not trace_id:
|
||||
trace_id = get_trace_id_from_otel_context()
|
||||
|
||||
if not trace_id:
|
||||
traceparent = request.headers.get("traceparent")
|
||||
if traceparent:
|
||||
trace_id = parse_traceparent_header(traceparent)
|
||||
|
||||
if isinstance(trace_id, str) and is_valid_trace_id(trace_id):
|
||||
return trace_id
|
||||
return None
|
||||
|
|
@ -40,3 +58,49 @@ def extract_external_trace_id_from_args(args: Mapping[str, Any]) -> dict:
|
|||
if trace_id:
|
||||
return {"external_trace_id": trace_id}
|
||||
return {}
|
||||
|
||||
|
||||
def get_trace_id_from_otel_context() -> Optional[str]:
|
||||
"""
|
||||
Retrieve the current trace ID from the active OpenTelemetry trace context.
|
||||
Returns None if:
|
||||
1. OpenTelemetry SDK is not installed or enabled.
|
||||
2. There is no active span or trace context.
|
||||
"""
|
||||
try:
|
||||
from opentelemetry.trace import SpanContext, get_current_span
|
||||
from opentelemetry.trace.span import INVALID_TRACE_ID
|
||||
|
||||
span = get_current_span()
|
||||
if not span:
|
||||
return None
|
||||
|
||||
span_context: SpanContext = span.get_span_context()
|
||||
|
||||
if not span_context or span_context.trace_id == INVALID_TRACE_ID:
|
||||
return None
|
||||
|
||||
trace_id_hex = f"{span_context.trace_id:032x}"
|
||||
return trace_id_hex
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def parse_traceparent_header(traceparent: str) -> Optional[str]:
|
||||
"""
|
||||
Parse the `traceparent` header to extract the trace_id.
|
||||
|
||||
Expected format:
|
||||
'version-trace_id-span_id-flags'
|
||||
|
||||
Reference:
|
||||
W3C Trace Context Specification: https://www.w3.org/TR/trace-context/
|
||||
"""
|
||||
try:
|
||||
parts = traceparent.split("-")
|
||||
if len(parts) == 4 and len(parts[1]) == 32:
|
||||
return parts[1]
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ from core.mcp.types import (
|
|||
from models.tools import MCPToolProvider
|
||||
from services.tools.mcp_tools_manage_service import MCPToolManageService
|
||||
|
||||
LATEST_PROTOCOL_VERSION = "1.0"
|
||||
|
||||
|
||||
class OAuthClientProvider:
|
||||
mcp_provider: MCPToolProvider
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from typing import Any, TypeAlias, final
|
|||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
import httpx
|
||||
from httpx_sse import EventSource, ServerSentEvent
|
||||
from sseclient import SSEClient
|
||||
|
||||
from core.mcp import types
|
||||
|
|
@ -37,11 +38,6 @@ WriteQueue: TypeAlias = queue.Queue[SessionMessage | Exception | None]
|
|||
StatusQueue: TypeAlias = queue.Queue[_StatusReady | _StatusError]
|
||||
|
||||
|
||||
def remove_request_params(url: str) -> str:
|
||||
"""Remove request parameters from URL, keeping only the path."""
|
||||
return urljoin(url, urlparse(url).path)
|
||||
|
||||
|
||||
class SSETransport:
|
||||
"""SSE client transport implementation."""
|
||||
|
||||
|
|
@ -114,7 +110,7 @@ class SSETransport:
|
|||
logger.exception("Error parsing server message")
|
||||
read_queue.put(exc)
|
||||
|
||||
def _handle_sse_event(self, sse, read_queue: ReadQueue, status_queue: StatusQueue) -> None:
|
||||
def _handle_sse_event(self, sse: ServerSentEvent, read_queue: ReadQueue, status_queue: StatusQueue) -> None:
|
||||
"""Handle a single SSE event.
|
||||
|
||||
Args:
|
||||
|
|
@ -130,7 +126,7 @@ class SSETransport:
|
|||
case _:
|
||||
logger.warning("Unknown SSE event: %s", sse.event)
|
||||
|
||||
def sse_reader(self, event_source, read_queue: ReadQueue, status_queue: StatusQueue) -> None:
|
||||
def sse_reader(self, event_source: EventSource, read_queue: ReadQueue, status_queue: StatusQueue) -> None:
|
||||
"""Read and process SSE events.
|
||||
|
||||
Args:
|
||||
|
|
@ -225,7 +221,7 @@ class SSETransport:
|
|||
self,
|
||||
executor: ThreadPoolExecutor,
|
||||
client: httpx.Client,
|
||||
event_source,
|
||||
event_source: EventSource,
|
||||
) -> tuple[ReadQueue, WriteQueue]:
|
||||
"""Establish connection and start worker threads.
|
||||
|
||||
|
|
|
|||
|
|
@ -16,13 +16,14 @@ from extensions.ext_database import db
|
|||
from models.model import App, AppMCPServer, AppMode, EndUser
|
||||
from services.app_generate_service import AppGenerateService
|
||||
|
||||
"""
|
||||
Apply to MCP HTTP streamable server with stateless http
|
||||
"""
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MCPServerStreamableHTTPRequestHandler:
|
||||
"""
|
||||
Apply to MCP HTTP streamable server with stateless http
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, app: App, request: types.ClientRequest | types.ClientNotification, user_input_form: list[VariableEntity]
|
||||
):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import json
|
||||
from collections.abc import Generator
|
||||
from contextlib import AbstractContextManager
|
||||
|
||||
import httpx
|
||||
import httpx_sse
|
||||
from httpx_sse import connect_sse
|
||||
|
||||
from configs import dify_config
|
||||
from core.mcp.types import ErrorData, JSONRPCError
|
||||
|
|
@ -55,20 +59,42 @@ def create_ssrf_proxy_mcp_http_client(
|
|||
)
|
||||
|
||||
|
||||
def ssrf_proxy_sse_connect(url, **kwargs):
|
||||
def ssrf_proxy_sse_connect(url: str, **kwargs) -> AbstractContextManager[httpx_sse.EventSource]:
|
||||
"""Connect to SSE endpoint with SSRF proxy protection.
|
||||
|
||||
This function creates an SSE connection using the configured proxy settings
|
||||
to prevent SSRF attacks when connecting to external endpoints.
|
||||
to prevent SSRF attacks when connecting to external endpoints. It returns
|
||||
a context manager that yields an EventSource object for SSE streaming.
|
||||
|
||||
The function handles HTTP client creation and cleanup automatically, but
|
||||
also accepts a pre-configured client via kwargs.
|
||||
|
||||
Args:
|
||||
url: The SSE endpoint URL
|
||||
**kwargs: Additional arguments passed to the SSE connection
|
||||
url (str): The SSE endpoint URL to connect to
|
||||
**kwargs: Additional arguments passed to the SSE connection, including:
|
||||
- client (httpx.Client, optional): Pre-configured HTTP client.
|
||||
If not provided, one will be created with SSRF protection.
|
||||
- method (str, optional): HTTP method to use, defaults to "GET"
|
||||
- headers (dict, optional): HTTP headers to include in the request
|
||||
- timeout (httpx.Timeout, optional): Timeout configuration for the connection
|
||||
|
||||
Returns:
|
||||
EventSource object for SSE streaming
|
||||
AbstractContextManager[httpx_sse.EventSource]: A context manager that yields an EventSource
|
||||
object for SSE streaming. The EventSource provides access to server-sent events.
|
||||
|
||||
Example:
|
||||
```python
|
||||
with ssrf_proxy_sse_connect(url, headers=headers) as event_source:
|
||||
for sse in event_source.iter_sse():
|
||||
print(sse.event, sse.data)
|
||||
```
|
||||
|
||||
Note:
|
||||
If a client is not provided in kwargs, one will be automatically created
|
||||
with SSRF protection based on the application's configuration. If an
|
||||
exception occurs during connection, any automatically created client
|
||||
will be cleaned up automatically.
|
||||
"""
|
||||
from httpx_sse import connect_sse
|
||||
|
||||
# Extract client if provided, otherwise create one
|
||||
client = kwargs.pop("client", None)
|
||||
|
|
@ -101,7 +127,9 @@ def ssrf_proxy_sse_connect(url, **kwargs):
|
|||
raise
|
||||
|
||||
|
||||
def create_mcp_error_response(request_id: int | str | None, code: int, message: str, data=None):
|
||||
def create_mcp_error_response(
|
||||
request_id: int | str | None, code: int, message: str, data=None
|
||||
) -> Generator[bytes, None, None]:
|
||||
"""Create MCP error response"""
|
||||
error_data = ErrorData(code=code, message=message, data=data)
|
||||
json_response = JSONRPCError(
|
||||
|
|
|
|||
|
|
@ -151,12 +151,9 @@ def jsonable_encoder(
|
|||
return format(obj, "f")
|
||||
if isinstance(obj, dict):
|
||||
encoded_dict = {}
|
||||
allowed_keys = set(obj.keys())
|
||||
for key, value in obj.items():
|
||||
if (
|
||||
(not sqlalchemy_safe or (not isinstance(key, str)) or (not key.startswith("_sa")))
|
||||
and (value is not None or not exclude_none)
|
||||
and key in allowed_keys
|
||||
if (not sqlalchemy_safe or (not isinstance(key, str)) or (not key.startswith("_sa"))) and (
|
||||
value is not None or not exclude_none
|
||||
):
|
||||
encoded_key = jsonable_encoder(
|
||||
key,
|
||||
|
|
|
|||
|
|
@ -4,15 +4,15 @@ from collections.abc import Sequence
|
|||
from typing import Optional
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from opentelemetry.trace import Status, StatusCode
|
||||
from opentelemetry.trace import Link, Status, StatusCode
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from core.ops.aliyun_trace.data_exporter.traceclient import (
|
||||
TraceClient,
|
||||
convert_datetime_to_nanoseconds,
|
||||
convert_string_to_id,
|
||||
convert_to_span_id,
|
||||
convert_to_trace_id,
|
||||
create_link,
|
||||
generate_span_id,
|
||||
)
|
||||
from core.ops.aliyun_trace.entities.aliyun_trace_entity import SpanData
|
||||
|
|
@ -103,10 +103,11 @@ class AliyunDataTrace(BaseTraceInstance):
|
|||
|
||||
def workflow_trace(self, trace_info: WorkflowTraceInfo):
|
||||
trace_id = convert_to_trace_id(trace_info.workflow_run_id)
|
||||
links = []
|
||||
if trace_info.trace_id:
|
||||
trace_id = convert_string_to_id(trace_info.trace_id)
|
||||
links.append(create_link(trace_id_str=trace_info.trace_id))
|
||||
workflow_span_id = convert_to_span_id(trace_info.workflow_run_id, "workflow")
|
||||
self.add_workflow_span(trace_id, workflow_span_id, trace_info)
|
||||
self.add_workflow_span(trace_id, workflow_span_id, trace_info, links)
|
||||
|
||||
workflow_node_executions = self.get_workflow_node_executions(trace_info)
|
||||
for node_execution in workflow_node_executions:
|
||||
|
|
@ -132,8 +133,9 @@ class AliyunDataTrace(BaseTraceInstance):
|
|||
status = Status(StatusCode.ERROR, trace_info.error)
|
||||
|
||||
trace_id = convert_to_trace_id(message_id)
|
||||
links = []
|
||||
if trace_info.trace_id:
|
||||
trace_id = convert_string_to_id(trace_info.trace_id)
|
||||
links.append(create_link(trace_id_str=trace_info.trace_id))
|
||||
|
||||
message_span_id = convert_to_span_id(message_id, "message")
|
||||
message_span = SpanData(
|
||||
|
|
@ -152,6 +154,7 @@ class AliyunDataTrace(BaseTraceInstance):
|
|||
OUTPUT_VALUE: str(trace_info.outputs),
|
||||
},
|
||||
status=status,
|
||||
links=links,
|
||||
)
|
||||
self.trace_client.add_span(message_span)
|
||||
|
||||
|
|
@ -192,8 +195,9 @@ class AliyunDataTrace(BaseTraceInstance):
|
|||
message_id = trace_info.message_id
|
||||
|
||||
trace_id = convert_to_trace_id(message_id)
|
||||
links = []
|
||||
if trace_info.trace_id:
|
||||
trace_id = convert_string_to_id(trace_info.trace_id)
|
||||
links.append(create_link(trace_id_str=trace_info.trace_id))
|
||||
|
||||
documents_data = extract_retrieval_documents(trace_info.documents)
|
||||
dataset_retrieval_span = SpanData(
|
||||
|
|
@ -211,6 +215,7 @@ class AliyunDataTrace(BaseTraceInstance):
|
|||
INPUT_VALUE: str(trace_info.inputs),
|
||||
OUTPUT_VALUE: json.dumps(documents_data, ensure_ascii=False),
|
||||
},
|
||||
links=links,
|
||||
)
|
||||
self.trace_client.add_span(dataset_retrieval_span)
|
||||
|
||||
|
|
@ -224,8 +229,9 @@ class AliyunDataTrace(BaseTraceInstance):
|
|||
status = Status(StatusCode.ERROR, trace_info.error)
|
||||
|
||||
trace_id = convert_to_trace_id(message_id)
|
||||
links = []
|
||||
if trace_info.trace_id:
|
||||
trace_id = convert_string_to_id(trace_info.trace_id)
|
||||
links.append(create_link(trace_id_str=trace_info.trace_id))
|
||||
|
||||
tool_span = SpanData(
|
||||
trace_id=trace_id,
|
||||
|
|
@ -244,6 +250,7 @@ class AliyunDataTrace(BaseTraceInstance):
|
|||
OUTPUT_VALUE: str(trace_info.tool_outputs),
|
||||
},
|
||||
status=status,
|
||||
links=links,
|
||||
)
|
||||
self.trace_client.add_span(tool_span)
|
||||
|
||||
|
|
@ -413,7 +420,9 @@ class AliyunDataTrace(BaseTraceInstance):
|
|||
status=self.get_workflow_node_status(node_execution),
|
||||
)
|
||||
|
||||
def add_workflow_span(self, trace_id: int, workflow_span_id: int, trace_info: WorkflowTraceInfo):
|
||||
def add_workflow_span(
|
||||
self, trace_id: int, workflow_span_id: int, trace_info: WorkflowTraceInfo, links: Sequence[Link]
|
||||
):
|
||||
message_span_id = None
|
||||
if trace_info.message_id:
|
||||
message_span_id = convert_to_span_id(trace_info.message_id, "message")
|
||||
|
|
@ -438,6 +447,7 @@ class AliyunDataTrace(BaseTraceInstance):
|
|||
OUTPUT_VALUE: json.dumps(trace_info.workflow_run_outputs, ensure_ascii=False),
|
||||
},
|
||||
status=status,
|
||||
links=links,
|
||||
)
|
||||
self.trace_client.add_span(message_span)
|
||||
|
||||
|
|
@ -456,6 +466,7 @@ class AliyunDataTrace(BaseTraceInstance):
|
|||
OUTPUT_VALUE: json.dumps(trace_info.workflow_run_outputs, ensure_ascii=False),
|
||||
},
|
||||
status=status,
|
||||
links=links,
|
||||
)
|
||||
self.trace_client.add_span(workflow_span)
|
||||
|
||||
|
|
@ -466,8 +477,9 @@ class AliyunDataTrace(BaseTraceInstance):
|
|||
status = Status(StatusCode.ERROR, trace_info.error)
|
||||
|
||||
trace_id = convert_to_trace_id(message_id)
|
||||
links = []
|
||||
if trace_info.trace_id:
|
||||
trace_id = convert_string_to_id(trace_info.trace_id)
|
||||
links.append(create_link(trace_id_str=trace_info.trace_id))
|
||||
|
||||
suggested_question_span = SpanData(
|
||||
trace_id=trace_id,
|
||||
|
|
@ -487,6 +499,7 @@ class AliyunDataTrace(BaseTraceInstance):
|
|||
OUTPUT_VALUE: json.dumps(trace_info.suggested_question, ensure_ascii=False),
|
||||
},
|
||||
status=status,
|
||||
links=links,
|
||||
)
|
||||
self.trace_client.add_span(suggested_question_span)
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from opentelemetry.sdk.resources import Resource
|
|||
from opentelemetry.sdk.trace import ReadableSpan
|
||||
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
|
||||
from opentelemetry.semconv.resource import ResourceAttributes
|
||||
from opentelemetry.trace import Link, SpanContext, TraceFlags
|
||||
|
||||
from configs import dify_config
|
||||
from core.ops.aliyun_trace.entities.aliyun_trace_entity import SpanData
|
||||
|
|
@ -166,6 +167,16 @@ class SpanBuilder:
|
|||
return span
|
||||
|
||||
|
||||
def create_link(trace_id_str: str) -> Link:
|
||||
placeholder_span_id = 0x0000000000000000
|
||||
trace_id = int(trace_id_str, 16)
|
||||
span_context = SpanContext(
|
||||
trace_id=trace_id, span_id=placeholder_span_id, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED)
|
||||
)
|
||||
|
||||
return Link(span_context)
|
||||
|
||||
|
||||
def generate_span_id() -> int:
|
||||
span_id = random.getrandbits(64)
|
||||
while span_id == INVALID_SPAN_ID:
|
||||
|
|
|
|||
|
|
@ -523,7 +523,7 @@ class ProviderManager:
|
|||
# Init trial provider records if not exists
|
||||
if ProviderQuotaType.TRIAL not in provider_quota_to_provider_record_dict:
|
||||
try:
|
||||
# FIXME ignore the type errork, onyl TrialHostingQuota has limit need to change the logic
|
||||
# FIXME ignore the type error, only TrialHostingQuota has limit need to change the logic
|
||||
new_provider_record = Provider(
|
||||
tenant_id=tenant_id,
|
||||
# TODO: Use provider name with prefix after the data migration.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import json
|
||||
from collections import defaultdict
|
||||
from typing import Any, Optional
|
||||
|
||||
import orjson
|
||||
from pydantic import BaseModel
|
||||
|
||||
from configs import dify_config
|
||||
|
|
@ -135,13 +135,13 @@ class Jieba(BaseKeyword):
|
|||
dataset_keyword_table = self.dataset.dataset_keyword_table
|
||||
keyword_data_source_type = dataset_keyword_table.data_source_type
|
||||
if keyword_data_source_type == "database":
|
||||
dataset_keyword_table.keyword_table = json.dumps(keyword_table_dict, cls=SetEncoder)
|
||||
dataset_keyword_table.keyword_table = dumps_with_sets(keyword_table_dict)
|
||||
db.session.commit()
|
||||
else:
|
||||
file_key = "keyword_files/" + self.dataset.tenant_id + "/" + self.dataset.id + ".txt"
|
||||
if storage.exists(file_key):
|
||||
storage.delete(file_key)
|
||||
storage.save(file_key, json.dumps(keyword_table_dict, cls=SetEncoder).encode("utf-8"))
|
||||
storage.save(file_key, dumps_with_sets(keyword_table_dict).encode("utf-8"))
|
||||
|
||||
def _get_dataset_keyword_table(self) -> Optional[dict]:
|
||||
dataset_keyword_table = self.dataset.dataset_keyword_table
|
||||
|
|
@ -157,12 +157,11 @@ class Jieba(BaseKeyword):
|
|||
data_source_type=keyword_data_source_type,
|
||||
)
|
||||
if keyword_data_source_type == "database":
|
||||
dataset_keyword_table.keyword_table = json.dumps(
|
||||
dataset_keyword_table.keyword_table = dumps_with_sets(
|
||||
{
|
||||
"__type__": "keyword_table",
|
||||
"__data__": {"index_id": self.dataset.id, "summary": None, "table": {}},
|
||||
},
|
||||
cls=SetEncoder,
|
||||
}
|
||||
)
|
||||
db.session.add(dataset_keyword_table)
|
||||
db.session.commit()
|
||||
|
|
@ -257,8 +256,13 @@ class Jieba(BaseKeyword):
|
|||
self._save_dataset_keyword_table(keyword_table)
|
||||
|
||||
|
||||
class SetEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, set):
|
||||
return list(obj)
|
||||
return super().default(obj)
|
||||
def set_orjson_default(obj: Any) -> Any:
|
||||
"""Default function for orjson serialization of set types"""
|
||||
if isinstance(obj, set):
|
||||
return list(obj)
|
||||
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
|
||||
|
||||
|
||||
def dumps_with_sets(obj: Any) -> str:
|
||||
"""JSON dumps with set support using orjson"""
|
||||
return orjson.dumps(obj, default=set_orjson_default).decode("utf-8")
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -108,10 +108,18 @@ class ApiProviderAuthType(Enum):
|
|||
:param value: mode value
|
||||
:return: mode
|
||||
"""
|
||||
# 'api_key' deprecated in PR #21656
|
||||
# normalize & tiny alias for backward compatibility
|
||||
v = (value or "").strip().lower()
|
||||
if v == "api_key":
|
||||
v = cls.API_KEY_HEADER.value
|
||||
|
||||
for mode in cls:
|
||||
if mode.value == value:
|
||||
if mode.value == v:
|
||||
return mode
|
||||
raise ValueError(f"invalid mode value {value}")
|
||||
|
||||
valid = ", ".join(m.value for m in cls)
|
||||
raise ValueError(f"invalid mode value '{value}', expected one of: {valid}")
|
||||
|
||||
|
||||
class ToolInvokeMessage(BaseModel):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import json
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import Any
|
||||
|
||||
import orjson
|
||||
|
||||
from .segment_group import SegmentGroup
|
||||
from .segments import ArrayFileSegment, FileSegment, Segment
|
||||
|
|
@ -12,15 +14,20 @@ def to_selector(node_id: str, name: str, paths: Iterable[str] = ()) -> Sequence[
|
|||
return selectors
|
||||
|
||||
|
||||
class SegmentJSONEncoder(json.JSONEncoder):
|
||||
def default(self, o):
|
||||
if isinstance(o, ArrayFileSegment):
|
||||
return [v.model_dump() for v in o.value]
|
||||
elif isinstance(o, FileSegment):
|
||||
return o.value.model_dump()
|
||||
elif isinstance(o, SegmentGroup):
|
||||
return [self.default(seg) for seg in o.value]
|
||||
elif isinstance(o, Segment):
|
||||
return o.value
|
||||
else:
|
||||
super().default(o)
|
||||
def segment_orjson_default(o: Any) -> Any:
|
||||
"""Default function for orjson serialization of Segment types"""
|
||||
if isinstance(o, ArrayFileSegment):
|
||||
return [v.model_dump() for v in o.value]
|
||||
elif isinstance(o, FileSegment):
|
||||
return o.value.model_dump()
|
||||
elif isinstance(o, SegmentGroup):
|
||||
return [segment_orjson_default(seg) for seg in o.value]
|
||||
elif isinstance(o, Segment):
|
||||
return o.value
|
||||
raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
|
||||
|
||||
|
||||
def dumps_with_segments(obj: Any, ensure_ascii: bool = False) -> str:
|
||||
"""JSON dumps with segment support using orjson"""
|
||||
option = orjson.OPT_NON_STR_KEYS
|
||||
return orjson.dumps(obj, default=segment_orjson_default, option=option).decode("utf-8")
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import logging
|
|||
from collections.abc import Generator, Mapping, Sequence
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom, ModelConfigWithCredentialsEntity
|
||||
from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity
|
||||
from core.file import FileType, file_manager
|
||||
from core.helper.code_executor import CodeExecutor, CodeLanguage
|
||||
from core.llm_generator.output_parser.errors import OutputParserError
|
||||
|
|
@ -194,17 +194,6 @@ class LLMNode(BaseNode):
|
|||
else []
|
||||
)
|
||||
|
||||
# single step run fetch file from sys files
|
||||
if not files and self.invoke_from == InvokeFrom.DEBUGGER and not self.previous_node_id:
|
||||
files = (
|
||||
llm_utils.fetch_files(
|
||||
variable_pool=variable_pool,
|
||||
selector=["sys", "files"],
|
||||
)
|
||||
if self._node_data.vision.enabled
|
||||
else []
|
||||
)
|
||||
|
||||
if files:
|
||||
node_inputs["#files#"] = [file.to_dict() for file in files]
|
||||
|
||||
|
|
|
|||
|
|
@ -318,33 +318,6 @@ class ToolNode(BaseNode):
|
|||
json.append(message.message.json_object)
|
||||
elif message.type == ToolInvokeMessage.MessageType.LINK:
|
||||
assert isinstance(message.message, ToolInvokeMessage.TextMessage)
|
||||
|
||||
if message.meta:
|
||||
transfer_method = message.meta.get("transfer_method", FileTransferMethod.TOOL_FILE)
|
||||
else:
|
||||
transfer_method = FileTransferMethod.TOOL_FILE
|
||||
|
||||
tool_file_id = message.message.text.split("/")[-1].split(".")[0]
|
||||
|
||||
with Session(db.engine) as session:
|
||||
stmt = select(ToolFile).where(ToolFile.id == tool_file_id)
|
||||
tool_file = session.scalar(stmt)
|
||||
if tool_file is None:
|
||||
raise ToolFileError(f"Tool file {tool_file_id} does not exist")
|
||||
|
||||
mapping = {
|
||||
"tool_file_id": tool_file_id,
|
||||
"type": file_factory.get_file_type_by_mime_type(tool_file.mimetype),
|
||||
"transfer_method": transfer_method,
|
||||
"url": message.message.text,
|
||||
}
|
||||
|
||||
file = file_factory.build_from_mapping(
|
||||
mapping=mapping,
|
||||
tenant_id=self.tenant_id,
|
||||
)
|
||||
files.append(file)
|
||||
|
||||
stream_text = f"Link: {message.message.text}\n"
|
||||
text += stream_text
|
||||
yield RunStreamChunkEvent(chunk_content=stream_text, from_variable_selector=[node_id, "text"])
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import click
|
|||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from core.indexing_runner import DocumentIsPausedError, IndexingRunner
|
||||
from events.event_handlers.document_index_event import document_index_created
|
||||
from events.document_index_event import document_index_created
|
||||
from extensions.ext_database import db
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from models.dataset import Document
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import ssl
|
||||
from datetime import timedelta
|
||||
from typing import Any, Optional
|
||||
|
||||
import pytz
|
||||
from celery import Celery, Task # type: ignore
|
||||
|
|
@ -8,6 +10,40 @@ from configs import dify_config
|
|||
from dify_app import DifyApp
|
||||
|
||||
|
||||
def _get_celery_ssl_options() -> Optional[dict[str, Any]]:
|
||||
"""Get SSL configuration for Celery broker/backend connections."""
|
||||
# Use REDIS_USE_SSL for consistency with the main Redis client
|
||||
# Only apply SSL if we're using Redis as broker/backend
|
||||
if not dify_config.REDIS_USE_SSL:
|
||||
return None
|
||||
|
||||
# Check if Celery is actually using Redis
|
||||
broker_is_redis = dify_config.CELERY_BROKER_URL and (
|
||||
dify_config.CELERY_BROKER_URL.startswith("redis://") or dify_config.CELERY_BROKER_URL.startswith("rediss://")
|
||||
)
|
||||
|
||||
if not broker_is_redis:
|
||||
return None
|
||||
|
||||
# Map certificate requirement strings to SSL constants
|
||||
cert_reqs_map = {
|
||||
"CERT_NONE": ssl.CERT_NONE,
|
||||
"CERT_OPTIONAL": ssl.CERT_OPTIONAL,
|
||||
"CERT_REQUIRED": ssl.CERT_REQUIRED,
|
||||
}
|
||||
|
||||
ssl_cert_reqs = cert_reqs_map.get(dify_config.REDIS_SSL_CERT_REQS, ssl.CERT_NONE)
|
||||
|
||||
ssl_options = {
|
||||
"ssl_cert_reqs": ssl_cert_reqs,
|
||||
"ssl_ca_certs": dify_config.REDIS_SSL_CA_CERTS,
|
||||
"ssl_certfile": dify_config.REDIS_SSL_CERTFILE,
|
||||
"ssl_keyfile": dify_config.REDIS_SSL_KEYFILE,
|
||||
}
|
||||
|
||||
return ssl_options
|
||||
|
||||
|
||||
def init_app(app: DifyApp) -> Celery:
|
||||
class FlaskTask(Task):
|
||||
def __call__(self, *args: object, **kwargs: object) -> object:
|
||||
|
|
@ -33,14 +69,6 @@ def init_app(app: DifyApp) -> Celery:
|
|||
task_ignore_result=True,
|
||||
)
|
||||
|
||||
# Add SSL options to the Celery configuration
|
||||
ssl_options = {
|
||||
"ssl_cert_reqs": None,
|
||||
"ssl_ca_certs": None,
|
||||
"ssl_certfile": None,
|
||||
"ssl_keyfile": None,
|
||||
}
|
||||
|
||||
celery_app.conf.update(
|
||||
result_backend=dify_config.CELERY_RESULT_BACKEND,
|
||||
broker_transport_options=broker_transport_options,
|
||||
|
|
@ -51,9 +79,13 @@ def init_app(app: DifyApp) -> Celery:
|
|||
timezone=pytz.timezone(dify_config.LOG_TZ or "UTC"),
|
||||
)
|
||||
|
||||
if dify_config.BROKER_USE_SSL:
|
||||
# Apply SSL configuration if enabled
|
||||
ssl_options = _get_celery_ssl_options()
|
||||
if ssl_options:
|
||||
celery_app.conf.update(
|
||||
broker_use_ssl=ssl_options, # Add the SSL options to the broker configuration
|
||||
broker_use_ssl=ssl_options,
|
||||
# Also apply SSL to the backend if it's Redis
|
||||
redis_backend_use_ssl=ssl_options if dify_config.CELERY_BACKEND == "redis" else None,
|
||||
)
|
||||
|
||||
if dify_config.LOG_FILE:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
from flask_orjson import OrjsonProvider
|
||||
|
||||
from dify_app import DifyApp
|
||||
|
||||
|
||||
def init_app(app: DifyApp) -> None:
|
||||
"""Initialize Flask-Orjson extension for faster JSON serialization"""
|
||||
app.json = OrjsonProvider(app)
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import functools
|
||||
import logging
|
||||
import ssl
|
||||
from collections.abc import Callable
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, Any, Union
|
||||
|
|
@ -116,76 +117,132 @@ class RedisClientWrapper:
|
|||
redis_client: RedisClientWrapper = RedisClientWrapper()
|
||||
|
||||
|
||||
def init_app(app: DifyApp):
|
||||
global redis_client
|
||||
connection_class: type[Union[Connection, SSLConnection]] = Connection
|
||||
if dify_config.REDIS_USE_SSL:
|
||||
connection_class = SSLConnection
|
||||
resp_protocol = dify_config.REDIS_SERIALIZATION_PROTOCOL
|
||||
if dify_config.REDIS_ENABLE_CLIENT_SIDE_CACHE:
|
||||
if resp_protocol >= 3:
|
||||
clientside_cache_config = CacheConfig()
|
||||
else:
|
||||
raise ValueError("Client side cache is only supported in RESP3")
|
||||
else:
|
||||
clientside_cache_config = None
|
||||
def _get_ssl_configuration() -> tuple[type[Union[Connection, SSLConnection]], dict[str, Any]]:
|
||||
"""Get SSL configuration for Redis connection."""
|
||||
if not dify_config.REDIS_USE_SSL:
|
||||
return Connection, {}
|
||||
|
||||
redis_params: dict[str, Any] = {
|
||||
cert_reqs_map = {
|
||||
"CERT_NONE": ssl.CERT_NONE,
|
||||
"CERT_OPTIONAL": ssl.CERT_OPTIONAL,
|
||||
"CERT_REQUIRED": ssl.CERT_REQUIRED,
|
||||
}
|
||||
ssl_cert_reqs = cert_reqs_map.get(dify_config.REDIS_SSL_CERT_REQS, ssl.CERT_NONE)
|
||||
|
||||
ssl_kwargs = {
|
||||
"ssl_cert_reqs": ssl_cert_reqs,
|
||||
"ssl_ca_certs": dify_config.REDIS_SSL_CA_CERTS,
|
||||
"ssl_certfile": dify_config.REDIS_SSL_CERTFILE,
|
||||
"ssl_keyfile": dify_config.REDIS_SSL_KEYFILE,
|
||||
}
|
||||
|
||||
return SSLConnection, ssl_kwargs
|
||||
|
||||
|
||||
def _get_cache_configuration() -> CacheConfig | None:
|
||||
"""Get client-side cache configuration if enabled."""
|
||||
if not dify_config.REDIS_ENABLE_CLIENT_SIDE_CACHE:
|
||||
return None
|
||||
|
||||
resp_protocol = dify_config.REDIS_SERIALIZATION_PROTOCOL
|
||||
if resp_protocol < 3:
|
||||
raise ValueError("Client side cache is only supported in RESP3")
|
||||
|
||||
return CacheConfig()
|
||||
|
||||
|
||||
def _get_base_redis_params() -> dict[str, Any]:
|
||||
"""Get base Redis connection parameters."""
|
||||
return {
|
||||
"username": dify_config.REDIS_USERNAME,
|
||||
"password": dify_config.REDIS_PASSWORD or None, # Temporary fix for empty password
|
||||
"password": dify_config.REDIS_PASSWORD or None,
|
||||
"db": dify_config.REDIS_DB,
|
||||
"encoding": "utf-8",
|
||||
"encoding_errors": "strict",
|
||||
"decode_responses": False,
|
||||
"protocol": resp_protocol,
|
||||
"cache_config": clientside_cache_config,
|
||||
"protocol": dify_config.REDIS_SERIALIZATION_PROTOCOL,
|
||||
"cache_config": _get_cache_configuration(),
|
||||
}
|
||||
|
||||
if dify_config.REDIS_USE_SENTINEL:
|
||||
assert dify_config.REDIS_SENTINELS is not None, "REDIS_SENTINELS must be set when REDIS_USE_SENTINEL is True"
|
||||
assert dify_config.REDIS_SENTINEL_SERVICE_NAME is not None, (
|
||||
"REDIS_SENTINEL_SERVICE_NAME must be set when REDIS_USE_SENTINEL is True"
|
||||
)
|
||||
sentinel_hosts = [
|
||||
(node.split(":")[0], int(node.split(":")[1])) for node in dify_config.REDIS_SENTINELS.split(",")
|
||||
]
|
||||
sentinel = Sentinel(
|
||||
sentinel_hosts,
|
||||
sentinel_kwargs={
|
||||
"socket_timeout": dify_config.REDIS_SENTINEL_SOCKET_TIMEOUT,
|
||||
"username": dify_config.REDIS_SENTINEL_USERNAME,
|
||||
"password": dify_config.REDIS_SENTINEL_PASSWORD,
|
||||
},
|
||||
)
|
||||
master = sentinel.master_for(dify_config.REDIS_SENTINEL_SERVICE_NAME, **redis_params)
|
||||
redis_client.initialize(master)
|
||||
elif dify_config.REDIS_USE_CLUSTERS:
|
||||
assert dify_config.REDIS_CLUSTERS is not None, "REDIS_CLUSTERS must be set when REDIS_USE_CLUSTERS is True"
|
||||
nodes = [
|
||||
ClusterNode(host=node.split(":")[0], port=int(node.split(":")[1]))
|
||||
for node in dify_config.REDIS_CLUSTERS.split(",")
|
||||
]
|
||||
redis_client.initialize(
|
||||
RedisCluster(
|
||||
startup_nodes=nodes,
|
||||
password=dify_config.REDIS_CLUSTERS_PASSWORD,
|
||||
protocol=resp_protocol,
|
||||
cache_config=clientside_cache_config,
|
||||
)
|
||||
)
|
||||
else:
|
||||
redis_params.update(
|
||||
{
|
||||
"host": dify_config.REDIS_HOST,
|
||||
"port": dify_config.REDIS_PORT,
|
||||
"connection_class": connection_class,
|
||||
"protocol": resp_protocol,
|
||||
"cache_config": clientside_cache_config,
|
||||
}
|
||||
)
|
||||
pool = redis.ConnectionPool(**redis_params)
|
||||
redis_client.initialize(redis.Redis(connection_pool=pool))
|
||||
|
||||
def _create_sentinel_client(redis_params: dict[str, Any]) -> Union[redis.Redis, RedisCluster]:
|
||||
"""Create Redis client using Sentinel configuration."""
|
||||
if not dify_config.REDIS_SENTINELS:
|
||||
raise ValueError("REDIS_SENTINELS must be set when REDIS_USE_SENTINEL is True")
|
||||
|
||||
if not dify_config.REDIS_SENTINEL_SERVICE_NAME:
|
||||
raise ValueError("REDIS_SENTINEL_SERVICE_NAME must be set when REDIS_USE_SENTINEL is True")
|
||||
|
||||
sentinel_hosts = [(node.split(":")[0], int(node.split(":")[1])) for node in dify_config.REDIS_SENTINELS.split(",")]
|
||||
|
||||
sentinel = Sentinel(
|
||||
sentinel_hosts,
|
||||
sentinel_kwargs={
|
||||
"socket_timeout": dify_config.REDIS_SENTINEL_SOCKET_TIMEOUT,
|
||||
"username": dify_config.REDIS_SENTINEL_USERNAME,
|
||||
"password": dify_config.REDIS_SENTINEL_PASSWORD,
|
||||
},
|
||||
)
|
||||
|
||||
master: redis.Redis = sentinel.master_for(dify_config.REDIS_SENTINEL_SERVICE_NAME, **redis_params)
|
||||
return master
|
||||
|
||||
|
||||
def _create_cluster_client() -> Union[redis.Redis, RedisCluster]:
|
||||
"""Create Redis cluster client."""
|
||||
if not dify_config.REDIS_CLUSTERS:
|
||||
raise ValueError("REDIS_CLUSTERS must be set when REDIS_USE_CLUSTERS is True")
|
||||
|
||||
nodes = [
|
||||
ClusterNode(host=node.split(":")[0], port=int(node.split(":")[1]))
|
||||
for node in dify_config.REDIS_CLUSTERS.split(",")
|
||||
]
|
||||
|
||||
cluster: RedisCluster = RedisCluster(
|
||||
startup_nodes=nodes,
|
||||
password=dify_config.REDIS_CLUSTERS_PASSWORD,
|
||||
protocol=dify_config.REDIS_SERIALIZATION_PROTOCOL,
|
||||
cache_config=_get_cache_configuration(),
|
||||
)
|
||||
return cluster
|
||||
|
||||
|
||||
def _create_standalone_client(redis_params: dict[str, Any]) -> Union[redis.Redis, RedisCluster]:
|
||||
"""Create standalone Redis client."""
|
||||
connection_class, ssl_kwargs = _get_ssl_configuration()
|
||||
|
||||
redis_params.update(
|
||||
{
|
||||
"host": dify_config.REDIS_HOST,
|
||||
"port": dify_config.REDIS_PORT,
|
||||
"connection_class": connection_class,
|
||||
}
|
||||
)
|
||||
|
||||
if ssl_kwargs:
|
||||
redis_params.update(ssl_kwargs)
|
||||
|
||||
pool = redis.ConnectionPool(**redis_params)
|
||||
client: redis.Redis = redis.Redis(connection_pool=pool)
|
||||
return client
|
||||
|
||||
|
||||
def init_app(app: DifyApp):
|
||||
"""Initialize Redis client and attach it to the app."""
|
||||
global redis_client
|
||||
|
||||
# Determine Redis mode and create appropriate client
|
||||
if dify_config.REDIS_USE_SENTINEL:
|
||||
redis_params = _get_base_redis_params()
|
||||
client = _create_sentinel_client(redis_params)
|
||||
elif dify_config.REDIS_USE_CLUSTERS:
|
||||
client = _create_cluster_client()
|
||||
else:
|
||||
redis_params = _get_base_redis_params()
|
||||
client = _create_standalone_client(redis_params)
|
||||
|
||||
# Initialize the wrapper and attach to app
|
||||
redis_client.initialize(client)
|
||||
app.extensions["redis"] = redis_client
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1184,7 +1184,7 @@ class WorkflowDraftVariable(Base):
|
|||
value: The Segment object to store as the variable's value.
|
||||
"""
|
||||
self.__value = value
|
||||
self.value = json.dumps(value, cls=variable_utils.SegmentJSONEncoder)
|
||||
self.value = variable_utils.dumps_with_segments(value)
|
||||
self.value_type = value.value_type
|
||||
|
||||
def get_node_id(self) -> str | None:
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@ check_untyped_defs = True
|
|||
cache_fine_grained = True
|
||||
sqlite_cache = True
|
||||
exclude = (?x)(
|
||||
core/model_runtime/model_providers/
|
||||
| tests/
|
||||
tests/
|
||||
| migrations/
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ dependencies = [
|
|||
"flask-cors~=6.0.0",
|
||||
"flask-login~=0.6.3",
|
||||
"flask-migrate~=4.0.7",
|
||||
"flask-orjson~=2.0.0",
|
||||
"flask-restful~=0.3.10",
|
||||
"flask-sqlalchemy~=3.1.1",
|
||||
"gevent~=24.11.1",
|
||||
|
|
|
|||
|
|
@ -103,10 +103,10 @@ class ConversationService:
|
|||
@classmethod
|
||||
def _build_filter_condition(cls, sort_field: str, sort_direction: Callable, reference_conversation: Conversation):
|
||||
field_value = getattr(reference_conversation, sort_field)
|
||||
if sort_direction == desc:
|
||||
if sort_direction is desc:
|
||||
return getattr(Conversation, sort_field) < field_value
|
||||
else:
|
||||
return getattr(Conversation, sort_field) > field_value
|
||||
|
||||
return getattr(Conversation, sort_field) > field_value
|
||||
|
||||
@classmethod
|
||||
def rename(
|
||||
|
|
@ -147,7 +147,7 @@ class ConversationService:
|
|||
app_model.tenant_id, message.query, conversation.id, app_model.id
|
||||
)
|
||||
conversation.name = name
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
db.session.commit()
|
||||
|
|
@ -277,6 +277,11 @@ class ConversationService:
|
|||
|
||||
# Validate that the new value type matches the expected variable type
|
||||
expected_type = SegmentType(current_variable.value_type)
|
||||
|
||||
# There is showing number in web ui but int in db
|
||||
if expected_type == SegmentType.INTEGER:
|
||||
expected_type = SegmentType.NUMBER
|
||||
|
||||
if not expected_type.is_valid(new_value):
|
||||
inferred_type = SegmentType.infer_segment_type(new_value)
|
||||
raise ConversationVariableTypeMismatchError(
|
||||
|
|
|
|||
|
|
@ -55,7 +55,9 @@ class OAuthProxyService(BasePluginClient):
|
|||
if not context_id:
|
||||
raise ValueError("context_id is required")
|
||||
# get data from redis
|
||||
data = redis_client.getdel(f"{OAuthProxyService.__KEY_PREFIX__}{context_id}")
|
||||
key = f"{OAuthProxyService.__KEY_PREFIX__}{context_id}"
|
||||
data = redis_client.get(key)
|
||||
if not data:
|
||||
raise ValueError("context_id is invalid")
|
||||
redis_client.delete(key)
|
||||
return json.loads(data)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,474 @@
|
|||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from faker import Faker
|
||||
|
||||
from models.account import TenantAccountJoin, TenantAccountRole
|
||||
from models.model import Account, Tenant
|
||||
from models.provider import LoadBalancingModelConfig, Provider, ProviderModelSetting
|
||||
from services.model_load_balancing_service import ModelLoadBalancingService
|
||||
|
||||
|
||||
class TestModelLoadBalancingService:
|
||||
"""Integration tests for ModelLoadBalancingService using testcontainers."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_external_service_dependencies(self):
|
||||
"""Mock setup for external service dependencies."""
|
||||
with (
|
||||
patch("services.model_load_balancing_service.ProviderManager") as mock_provider_manager,
|
||||
patch("services.model_load_balancing_service.LBModelManager") as mock_lb_model_manager,
|
||||
patch("services.model_load_balancing_service.ModelProviderFactory") as mock_model_provider_factory,
|
||||
patch("services.model_load_balancing_service.encrypter") as mock_encrypter,
|
||||
):
|
||||
# Setup default mock returns
|
||||
mock_provider_manager_instance = mock_provider_manager.return_value
|
||||
|
||||
# Mock provider configuration
|
||||
mock_provider_config = MagicMock()
|
||||
mock_provider_config.provider.provider = "openai"
|
||||
mock_provider_config.custom_configuration.provider = None
|
||||
|
||||
# Mock provider model setting
|
||||
mock_provider_model_setting = MagicMock()
|
||||
mock_provider_model_setting.load_balancing_enabled = False
|
||||
|
||||
mock_provider_config.get_provider_model_setting.return_value = mock_provider_model_setting
|
||||
|
||||
# Mock provider configurations dict
|
||||
mock_provider_configs = {"openai": mock_provider_config}
|
||||
mock_provider_manager_instance.get_configurations.return_value = mock_provider_configs
|
||||
|
||||
# Mock LBModelManager
|
||||
mock_lb_model_manager.get_config_in_cooldown_and_ttl.return_value = (False, 0)
|
||||
|
||||
# Mock ModelProviderFactory
|
||||
mock_model_provider_factory_instance = mock_model_provider_factory.return_value
|
||||
|
||||
# Mock credential schemas
|
||||
mock_credential_schema = MagicMock()
|
||||
mock_credential_schema.credential_form_schemas = []
|
||||
|
||||
# Mock provider configuration methods
|
||||
mock_provider_config.extract_secret_variables.return_value = []
|
||||
mock_provider_config.obfuscated_credentials.return_value = {}
|
||||
mock_provider_config._get_credential_schema.return_value = mock_credential_schema
|
||||
|
||||
yield {
|
||||
"provider_manager": mock_provider_manager,
|
||||
"lb_model_manager": mock_lb_model_manager,
|
||||
"model_provider_factory": mock_model_provider_factory,
|
||||
"encrypter": mock_encrypter,
|
||||
"provider_config": mock_provider_config,
|
||||
"provider_model_setting": mock_provider_model_setting,
|
||||
"credential_schema": mock_credential_schema,
|
||||
}
|
||||
|
||||
def _create_test_account_and_tenant(self, db_session_with_containers, mock_external_service_dependencies):
|
||||
"""
|
||||
Helper method to create a test account and tenant for testing.
|
||||
|
||||
Args:
|
||||
db_session_with_containers: Database session from testcontainers infrastructure
|
||||
mock_external_service_dependencies: Mock dependencies
|
||||
|
||||
Returns:
|
||||
tuple: (account, tenant) - Created account and tenant instances
|
||||
"""
|
||||
fake = Faker()
|
||||
|
||||
# Create account
|
||||
account = Account(
|
||||
email=fake.email(),
|
||||
name=fake.name(),
|
||||
interface_language="en-US",
|
||||
status="active",
|
||||
)
|
||||
|
||||
from extensions.ext_database import db
|
||||
|
||||
db.session.add(account)
|
||||
db.session.commit()
|
||||
|
||||
# Create tenant for the account
|
||||
tenant = Tenant(
|
||||
name=fake.company(),
|
||||
status="normal",
|
||||
)
|
||||
db.session.add(tenant)
|
||||
db.session.commit()
|
||||
|
||||
# Create tenant-account join
|
||||
join = TenantAccountJoin(
|
||||
tenant_id=tenant.id,
|
||||
account_id=account.id,
|
||||
role=TenantAccountRole.OWNER.value,
|
||||
current=True,
|
||||
)
|
||||
db.session.add(join)
|
||||
db.session.commit()
|
||||
|
||||
# Set current tenant for account
|
||||
account.current_tenant = tenant
|
||||
|
||||
return account, tenant
|
||||
|
||||
def _create_test_provider_and_setting(
|
||||
self, db_session_with_containers, tenant_id, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Helper method to create a test provider and provider model setting.
|
||||
|
||||
Args:
|
||||
db_session_with_containers: Database session from testcontainers infrastructure
|
||||
tenant_id: Tenant ID for the provider
|
||||
mock_external_service_dependencies: Mock dependencies
|
||||
|
||||
Returns:
|
||||
tuple: (provider, provider_model_setting) - Created provider and setting instances
|
||||
"""
|
||||
fake = Faker()
|
||||
|
||||
from extensions.ext_database import db
|
||||
|
||||
# Create provider
|
||||
provider = Provider(
|
||||
tenant_id=tenant_id,
|
||||
provider_name="openai",
|
||||
provider_type="custom",
|
||||
is_valid=True,
|
||||
)
|
||||
db.session.add(provider)
|
||||
db.session.commit()
|
||||
|
||||
# Create provider model setting
|
||||
provider_model_setting = ProviderModelSetting(
|
||||
tenant_id=tenant_id,
|
||||
provider_name="openai",
|
||||
model_name="gpt-3.5-turbo",
|
||||
model_type="text-generation", # Use the origin model type that matches the query
|
||||
enabled=True,
|
||||
load_balancing_enabled=False,
|
||||
)
|
||||
db.session.add(provider_model_setting)
|
||||
db.session.commit()
|
||||
|
||||
return provider, provider_model_setting
|
||||
|
||||
def test_enable_model_load_balancing_success(self, db_session_with_containers, mock_external_service_dependencies):
|
||||
"""
|
||||
Test successful model load balancing enablement.
|
||||
|
||||
This test verifies:
|
||||
- Proper provider configuration retrieval
|
||||
- Successful enablement of model load balancing
|
||||
- Correct method calls to provider configuration
|
||||
"""
|
||||
# Arrange: Create test data
|
||||
fake = Faker()
|
||||
account, tenant = self._create_test_account_and_tenant(
|
||||
db_session_with_containers, mock_external_service_dependencies
|
||||
)
|
||||
provider, provider_model_setting = self._create_test_provider_and_setting(
|
||||
db_session_with_containers, tenant.id, mock_external_service_dependencies
|
||||
)
|
||||
|
||||
# Setup mocks for enable method
|
||||
mock_provider_config = mock_external_service_dependencies["provider_config"]
|
||||
mock_provider_config.enable_model_load_balancing = MagicMock()
|
||||
|
||||
# Act: Execute the method under test
|
||||
service = ModelLoadBalancingService()
|
||||
service.enable_model_load_balancing(
|
||||
tenant_id=tenant.id, provider="openai", model="gpt-3.5-turbo", model_type="llm"
|
||||
)
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
mock_provider_config.enable_model_load_balancing.assert_called_once()
|
||||
call_args = mock_provider_config.enable_model_load_balancing.call_args
|
||||
assert call_args.kwargs["model"] == "gpt-3.5-turbo"
|
||||
assert call_args.kwargs["model_type"].value == "llm" # ModelType enum value
|
||||
|
||||
# Verify database state
|
||||
from extensions.ext_database import db
|
||||
|
||||
db.session.refresh(provider)
|
||||
db.session.refresh(provider_model_setting)
|
||||
assert provider.id is not None
|
||||
assert provider_model_setting.id is not None
|
||||
|
||||
def test_disable_model_load_balancing_success(self, db_session_with_containers, mock_external_service_dependencies):
|
||||
"""
|
||||
Test successful model load balancing disablement.
|
||||
|
||||
This test verifies:
|
||||
- Proper provider configuration retrieval
|
||||
- Successful disablement of model load balancing
|
||||
- Correct method calls to provider configuration
|
||||
"""
|
||||
# Arrange: Create test data
|
||||
fake = Faker()
|
||||
account, tenant = self._create_test_account_and_tenant(
|
||||
db_session_with_containers, mock_external_service_dependencies
|
||||
)
|
||||
provider, provider_model_setting = self._create_test_provider_and_setting(
|
||||
db_session_with_containers, tenant.id, mock_external_service_dependencies
|
||||
)
|
||||
|
||||
# Setup mocks for disable method
|
||||
mock_provider_config = mock_external_service_dependencies["provider_config"]
|
||||
mock_provider_config.disable_model_load_balancing = MagicMock()
|
||||
|
||||
# Act: Execute the method under test
|
||||
service = ModelLoadBalancingService()
|
||||
service.disable_model_load_balancing(
|
||||
tenant_id=tenant.id, provider="openai", model="gpt-3.5-turbo", model_type="llm"
|
||||
)
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
mock_provider_config.disable_model_load_balancing.assert_called_once()
|
||||
call_args = mock_provider_config.disable_model_load_balancing.call_args
|
||||
assert call_args.kwargs["model"] == "gpt-3.5-turbo"
|
||||
assert call_args.kwargs["model_type"].value == "llm" # ModelType enum value
|
||||
|
||||
# Verify database state
|
||||
from extensions.ext_database import db
|
||||
|
||||
db.session.refresh(provider)
|
||||
db.session.refresh(provider_model_setting)
|
||||
assert provider.id is not None
|
||||
assert provider_model_setting.id is not None
|
||||
|
||||
def test_enable_model_load_balancing_provider_not_found(
|
||||
self, db_session_with_containers, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test error handling when provider does not exist.
|
||||
|
||||
This test verifies:
|
||||
- Proper error handling for non-existent provider
|
||||
- Correct exception type and message
|
||||
- No database state changes
|
||||
"""
|
||||
# Arrange: Create test data
|
||||
fake = Faker()
|
||||
account, tenant = self._create_test_account_and_tenant(
|
||||
db_session_with_containers, mock_external_service_dependencies
|
||||
)
|
||||
|
||||
# Setup mocks to return empty provider configurations
|
||||
mock_provider_manager = mock_external_service_dependencies["provider_manager"]
|
||||
mock_provider_manager_instance = mock_provider_manager.return_value
|
||||
mock_provider_manager_instance.get_configurations.return_value = {}
|
||||
|
||||
# Act & Assert: Verify proper error handling
|
||||
service = ModelLoadBalancingService()
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
service.enable_model_load_balancing(
|
||||
tenant_id=tenant.id, provider="nonexistent_provider", model="gpt-3.5-turbo", model_type="llm"
|
||||
)
|
||||
|
||||
# Verify correct error message
|
||||
assert "Provider nonexistent_provider does not exist." in str(exc_info.value)
|
||||
|
||||
# Verify no database state changes occurred
|
||||
from extensions.ext_database import db
|
||||
|
||||
db.session.rollback()
|
||||
|
||||
def test_get_load_balancing_configs_success(self, db_session_with_containers, mock_external_service_dependencies):
|
||||
"""
|
||||
Test successful retrieval of load balancing configurations.
|
||||
|
||||
This test verifies:
|
||||
- Proper provider configuration retrieval
|
||||
- Successful database query for load balancing configs
|
||||
- Correct return format and data structure
|
||||
"""
|
||||
# Arrange: Create test data
|
||||
fake = Faker()
|
||||
account, tenant = self._create_test_account_and_tenant(
|
||||
db_session_with_containers, mock_external_service_dependencies
|
||||
)
|
||||
provider, provider_model_setting = self._create_test_provider_and_setting(
|
||||
db_session_with_containers, tenant.id, mock_external_service_dependencies
|
||||
)
|
||||
|
||||
# Create load balancing config
|
||||
from extensions.ext_database import db
|
||||
|
||||
load_balancing_config = LoadBalancingModelConfig(
|
||||
tenant_id=tenant.id,
|
||||
provider_name="openai",
|
||||
model_name="gpt-3.5-turbo",
|
||||
model_type="text-generation", # Use the origin model type that matches the query
|
||||
name="config1",
|
||||
encrypted_config='{"api_key": "test_key"}',
|
||||
enabled=True,
|
||||
)
|
||||
db.session.add(load_balancing_config)
|
||||
db.session.commit()
|
||||
|
||||
# Verify the config was created
|
||||
db.session.refresh(load_balancing_config)
|
||||
assert load_balancing_config.id is not None
|
||||
|
||||
# Setup mocks for get_load_balancing_configs method
|
||||
mock_provider_config = mock_external_service_dependencies["provider_config"]
|
||||
mock_provider_model_setting = mock_external_service_dependencies["provider_model_setting"]
|
||||
mock_provider_model_setting.load_balancing_enabled = True
|
||||
|
||||
# Mock credential schema methods
|
||||
mock_credential_schema = mock_external_service_dependencies["credential_schema"]
|
||||
mock_credential_schema.credential_form_schemas = []
|
||||
|
||||
# Mock encrypter
|
||||
mock_encrypter = mock_external_service_dependencies["encrypter"]
|
||||
mock_encrypter.get_decrypt_decoding.return_value = ("key", "cipher")
|
||||
|
||||
# Mock _get_credential_schema method
|
||||
mock_provider_config._get_credential_schema.return_value = mock_credential_schema
|
||||
|
||||
# Mock extract_secret_variables method
|
||||
mock_provider_config.extract_secret_variables.return_value = []
|
||||
|
||||
# Mock obfuscated_credentials method
|
||||
mock_provider_config.obfuscated_credentials.return_value = {}
|
||||
|
||||
# Mock LBModelManager.get_config_in_cooldown_and_ttl
|
||||
mock_lb_model_manager = mock_external_service_dependencies["lb_model_manager"]
|
||||
mock_lb_model_manager.get_config_in_cooldown_and_ttl.return_value = (False, 0)
|
||||
|
||||
# Act: Execute the method under test
|
||||
service = ModelLoadBalancingService()
|
||||
is_enabled, configs = service.get_load_balancing_configs(
|
||||
tenant_id=tenant.id, provider="openai", model="gpt-3.5-turbo", model_type="llm"
|
||||
)
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert is_enabled is True
|
||||
assert len(configs) == 1
|
||||
assert configs[0]["id"] == load_balancing_config.id
|
||||
assert configs[0]["name"] == "config1"
|
||||
assert configs[0]["enabled"] is True
|
||||
assert configs[0]["in_cooldown"] is False
|
||||
assert configs[0]["ttl"] == 0
|
||||
|
||||
# Verify database state
|
||||
db.session.refresh(load_balancing_config)
|
||||
assert load_balancing_config.id is not None
|
||||
|
||||
def test_get_load_balancing_configs_provider_not_found(
|
||||
self, db_session_with_containers, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test error handling when provider does not exist in get_load_balancing_configs.
|
||||
|
||||
This test verifies:
|
||||
- Proper error handling for non-existent provider
|
||||
- Correct exception type and message
|
||||
- No database state changes
|
||||
"""
|
||||
# Arrange: Create test data
|
||||
fake = Faker()
|
||||
account, tenant = self._create_test_account_and_tenant(
|
||||
db_session_with_containers, mock_external_service_dependencies
|
||||
)
|
||||
|
||||
# Setup mocks to return empty provider configurations
|
||||
mock_provider_manager = mock_external_service_dependencies["provider_manager"]
|
||||
mock_provider_manager_instance = mock_provider_manager.return_value
|
||||
mock_provider_manager_instance.get_configurations.return_value = {}
|
||||
|
||||
# Act & Assert: Verify proper error handling
|
||||
service = ModelLoadBalancingService()
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
service.get_load_balancing_configs(
|
||||
tenant_id=tenant.id, provider="nonexistent_provider", model="gpt-3.5-turbo", model_type="llm"
|
||||
)
|
||||
|
||||
# Verify correct error message
|
||||
assert "Provider nonexistent_provider does not exist." in str(exc_info.value)
|
||||
|
||||
# Verify no database state changes occurred
|
||||
from extensions.ext_database import db
|
||||
|
||||
db.session.rollback()
|
||||
|
||||
def test_get_load_balancing_configs_with_inherit_config(
|
||||
self, db_session_with_containers, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test load balancing configs retrieval with inherit configuration.
|
||||
|
||||
This test verifies:
|
||||
- Proper handling of inherit configuration
|
||||
- Correct ordering of configurations
|
||||
- Inherit config initialization when needed
|
||||
"""
|
||||
# Arrange: Create test data
|
||||
fake = Faker()
|
||||
account, tenant = self._create_test_account_and_tenant(
|
||||
db_session_with_containers, mock_external_service_dependencies
|
||||
)
|
||||
provider, provider_model_setting = self._create_test_provider_and_setting(
|
||||
db_session_with_containers, tenant.id, mock_external_service_dependencies
|
||||
)
|
||||
|
||||
# Create load balancing config
|
||||
from extensions.ext_database import db
|
||||
|
||||
load_balancing_config = LoadBalancingModelConfig(
|
||||
tenant_id=tenant.id,
|
||||
provider_name="openai",
|
||||
model_name="gpt-3.5-turbo",
|
||||
model_type="text-generation", # Use the origin model type that matches the query
|
||||
name="config1",
|
||||
encrypted_config='{"api_key": "test_key"}',
|
||||
enabled=True,
|
||||
)
|
||||
db.session.add(load_balancing_config)
|
||||
db.session.commit()
|
||||
|
||||
# Setup mocks for inherit config scenario
|
||||
mock_provider_config = mock_external_service_dependencies["provider_config"]
|
||||
mock_provider_config.custom_configuration.provider = MagicMock() # Enable custom config
|
||||
|
||||
mock_provider_model_setting = mock_external_service_dependencies["provider_model_setting"]
|
||||
mock_provider_model_setting.load_balancing_enabled = True
|
||||
|
||||
# Mock credential schema methods
|
||||
mock_credential_schema = mock_external_service_dependencies["credential_schema"]
|
||||
mock_credential_schema.credential_form_schemas = []
|
||||
|
||||
# Mock encrypter
|
||||
mock_encrypter = mock_external_service_dependencies["encrypter"]
|
||||
mock_encrypter.get_decrypt_decoding.return_value = ("key", "cipher")
|
||||
|
||||
# Act: Execute the method under test
|
||||
service = ModelLoadBalancingService()
|
||||
is_enabled, configs = service.get_load_balancing_configs(
|
||||
tenant_id=tenant.id, provider="openai", model="gpt-3.5-turbo", model_type="llm"
|
||||
)
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert is_enabled is True
|
||||
assert len(configs) == 2 # inherit config + existing config
|
||||
|
||||
# First config should be inherit config
|
||||
assert configs[0]["name"] == "__inherit__"
|
||||
assert configs[0]["enabled"] is True
|
||||
|
||||
# Second config should be the existing config
|
||||
assert configs[1]["id"] == load_balancing_config.id
|
||||
assert configs[1]["name"] == "config1"
|
||||
|
||||
# Verify database state
|
||||
db.session.refresh(load_balancing_config)
|
||||
assert load_balancing_config.id is not None
|
||||
|
||||
# Verify inherit config was created in database
|
||||
inherit_configs = (
|
||||
db.session.query(LoadBalancingModelConfig).filter(LoadBalancingModelConfig.name == "__inherit__").all()
|
||||
)
|
||||
assert len(inherit_configs) == 1
|
||||
|
|
@ -262,26 +262,6 @@ def test_sse_client_queue_cleanup():
|
|||
# Note: In real implementation, cleanup should put None to signal shutdown
|
||||
|
||||
|
||||
def test_sse_client_url_processing():
|
||||
"""Test SSE client URL processing functions."""
|
||||
from core.mcp.client.sse_client import remove_request_params
|
||||
|
||||
# Test URL with parameters
|
||||
url_with_params = "http://example.com/sse?param1=value1¶m2=value2"
|
||||
cleaned_url = remove_request_params(url_with_params)
|
||||
assert cleaned_url == "http://example.com/sse"
|
||||
|
||||
# Test URL without parameters
|
||||
url_without_params = "http://example.com/sse"
|
||||
cleaned_url = remove_request_params(url_without_params)
|
||||
assert cleaned_url == "http://example.com/sse"
|
||||
|
||||
# Test URL with path and parameters
|
||||
complex_url = "http://example.com/path/to/sse?session=123&token=abc"
|
||||
cleaned_url = remove_request_params(complex_url)
|
||||
assert cleaned_url == "http://example.com/path/to/sse"
|
||||
|
||||
|
||||
def test_sse_client_headers_propagation():
|
||||
"""Test that custom headers are properly propagated in SSE client."""
|
||||
test_url = "http://test.example/sse"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,481 @@
|
|||
import json
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from uuid import uuid4
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import pytz
|
||||
|
||||
from core.tools.entities.tool_entities import ToolInvokeMessage
|
||||
from core.tools.utils.message_transformer import ToolFileMessageTransformer, safe_json_dict, safe_json_value
|
||||
|
||||
|
||||
class TestSafeJsonValue:
|
||||
"""Test suite for safe_json_value function to ensure proper serialization of complex types"""
|
||||
|
||||
def test_datetime_conversion(self):
|
||||
"""Test datetime conversion with timezone handling"""
|
||||
# Test datetime with UTC timezone
|
||||
dt = datetime(2024, 1, 1, 12, 0, 0, tzinfo=pytz.UTC)
|
||||
result = safe_json_value(dt)
|
||||
assert isinstance(result, str)
|
||||
assert "2024-01-01T12:00:00+00:00" in result
|
||||
|
||||
# Test datetime without timezone (should default to UTC)
|
||||
dt_no_tz = datetime(2024, 1, 1, 12, 0, 0)
|
||||
result = safe_json_value(dt_no_tz)
|
||||
assert isinstance(result, str)
|
||||
# The exact time will depend on the system's timezone, so we check the format
|
||||
assert "T" in result # ISO format separator
|
||||
# Check that it's a valid ISO format datetime string
|
||||
assert len(result) >= 19 # At least YYYY-MM-DDTHH:MM:SS
|
||||
|
||||
def test_date_conversion(self):
|
||||
"""Test date conversion to ISO format"""
|
||||
test_date = date(2024, 1, 1)
|
||||
result = safe_json_value(test_date)
|
||||
assert result == "2024-01-01"
|
||||
|
||||
def test_uuid_conversion(self):
|
||||
"""Test UUID conversion to string"""
|
||||
test_uuid = uuid4()
|
||||
result = safe_json_value(test_uuid)
|
||||
assert isinstance(result, str)
|
||||
assert result == str(test_uuid)
|
||||
|
||||
def test_decimal_conversion(self):
|
||||
"""Test Decimal conversion to float"""
|
||||
test_decimal = Decimal("123.456")
|
||||
result = safe_json_value(test_decimal)
|
||||
assert result == 123.456
|
||||
assert isinstance(result, float)
|
||||
|
||||
def test_bytes_conversion(self):
|
||||
"""Test bytes conversion with UTF-8 decoding"""
|
||||
# Test valid UTF-8 bytes
|
||||
test_bytes = b"Hello, World!"
|
||||
result = safe_json_value(test_bytes)
|
||||
assert result == "Hello, World!"
|
||||
|
||||
# Test invalid UTF-8 bytes (should fall back to hex)
|
||||
invalid_bytes = b"\xff\xfe\xfd"
|
||||
result = safe_json_value(invalid_bytes)
|
||||
assert result == "fffefd"
|
||||
|
||||
def test_memoryview_conversion(self):
|
||||
"""Test memoryview conversion to hex string"""
|
||||
test_bytes = b"test data"
|
||||
test_memoryview = memoryview(test_bytes)
|
||||
result = safe_json_value(test_memoryview)
|
||||
assert result == "746573742064617461" # hex of "test data"
|
||||
|
||||
def test_numpy_ndarray_conversion(self):
|
||||
"""Test numpy ndarray conversion to list"""
|
||||
# Test 1D array
|
||||
test_array = np.array([1, 2, 3, 4])
|
||||
result = safe_json_value(test_array)
|
||||
assert result == [1, 2, 3, 4]
|
||||
|
||||
# Test 2D array
|
||||
test_2d_array = np.array([[1, 2], [3, 4]])
|
||||
result = safe_json_value(test_2d_array)
|
||||
assert result == [[1, 2], [3, 4]]
|
||||
|
||||
# Test array with float values
|
||||
test_float_array = np.array([1.5, 2.7, 3.14])
|
||||
result = safe_json_value(test_float_array)
|
||||
assert result == [1.5, 2.7, 3.14]
|
||||
|
||||
def test_dict_conversion(self):
|
||||
"""Test dictionary conversion using safe_json_dict"""
|
||||
test_dict = {
|
||||
"string": "value",
|
||||
"number": 42,
|
||||
"float": 3.14,
|
||||
"boolean": True,
|
||||
"list": [1, 2, 3],
|
||||
"nested": {"key": "value"},
|
||||
}
|
||||
result = safe_json_value(test_dict)
|
||||
assert isinstance(result, dict)
|
||||
assert result == test_dict
|
||||
|
||||
def test_list_conversion(self):
|
||||
"""Test list conversion with mixed types"""
|
||||
test_list = [
|
||||
"string",
|
||||
42,
|
||||
3.14,
|
||||
True,
|
||||
[1, 2, 3],
|
||||
{"key": "value"},
|
||||
datetime(2024, 1, 1, 12, 0, 0, tzinfo=pytz.UTC),
|
||||
Decimal("123.456"),
|
||||
uuid4(),
|
||||
]
|
||||
result = safe_json_value(test_list)
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == len(test_list)
|
||||
assert isinstance(result[6], str) # datetime should be converted to string
|
||||
assert isinstance(result[7], float) # Decimal should be converted to float
|
||||
assert isinstance(result[8], str) # UUID should be converted to string
|
||||
|
||||
def test_tuple_conversion(self):
|
||||
"""Test tuple conversion to list"""
|
||||
test_tuple = (1, "string", 3.14)
|
||||
result = safe_json_value(test_tuple)
|
||||
assert isinstance(result, list)
|
||||
assert result == [1, "string", 3.14]
|
||||
|
||||
def test_set_conversion(self):
|
||||
"""Test set conversion to list"""
|
||||
test_set = {1, "string", 3.14}
|
||||
result = safe_json_value(test_set)
|
||||
assert isinstance(result, list)
|
||||
# Note: set order is not guaranteed, so we check length and content
|
||||
assert len(result) == 3
|
||||
assert 1 in result
|
||||
assert "string" in result
|
||||
assert 3.14 in result
|
||||
|
||||
def test_basic_types_passthrough(self):
|
||||
"""Test that basic types are passed through unchanged"""
|
||||
assert safe_json_value("string") == "string"
|
||||
assert safe_json_value(42) == 42
|
||||
assert safe_json_value(3.14) == 3.14
|
||||
assert safe_json_value(True) is True
|
||||
assert safe_json_value(False) is False
|
||||
assert safe_json_value(None) is None
|
||||
|
||||
def test_nested_complex_structure(self):
|
||||
"""Test complex nested structure with all types"""
|
||||
complex_data = {
|
||||
"dates": [date(2024, 1, 1), date(2024, 1, 2)],
|
||||
"timestamps": [
|
||||
datetime(2024, 1, 1, 12, 0, 0, tzinfo=pytz.UTC),
|
||||
datetime(2024, 1, 2, 12, 0, 0, tzinfo=pytz.UTC),
|
||||
],
|
||||
"numbers": [Decimal("123.456"), Decimal("789.012")],
|
||||
"identifiers": [uuid4(), uuid4()],
|
||||
"binary_data": [b"hello", b"world"],
|
||||
"arrays": [np.array([1, 2, 3]), np.array([4, 5, 6])],
|
||||
}
|
||||
|
||||
result = safe_json_value(complex_data)
|
||||
|
||||
# Verify structure is maintained
|
||||
assert isinstance(result, dict)
|
||||
assert "dates" in result
|
||||
assert "timestamps" in result
|
||||
assert "numbers" in result
|
||||
assert "identifiers" in result
|
||||
assert "binary_data" in result
|
||||
assert "arrays" in result
|
||||
|
||||
# Verify conversions
|
||||
assert all(isinstance(d, str) for d in result["dates"])
|
||||
assert all(isinstance(t, str) for t in result["timestamps"])
|
||||
assert all(isinstance(n, float) for n in result["numbers"])
|
||||
assert all(isinstance(i, str) for i in result["identifiers"])
|
||||
assert all(isinstance(b, str) for b in result["binary_data"])
|
||||
assert all(isinstance(a, list) for a in result["arrays"])
|
||||
|
||||
|
||||
class TestSafeJsonDict:
|
||||
"""Test suite for safe_json_dict function"""
|
||||
|
||||
def test_valid_dict_conversion(self):
|
||||
"""Test valid dictionary conversion"""
|
||||
test_dict = {
|
||||
"string": "value",
|
||||
"number": 42,
|
||||
"datetime": datetime(2024, 1, 1, 12, 0, 0, tzinfo=pytz.UTC),
|
||||
"decimal": Decimal("123.456"),
|
||||
}
|
||||
result = safe_json_dict(test_dict)
|
||||
assert isinstance(result, dict)
|
||||
assert result["string"] == "value"
|
||||
assert result["number"] == 42
|
||||
assert isinstance(result["datetime"], str)
|
||||
assert isinstance(result["decimal"], float)
|
||||
|
||||
def test_invalid_input_type(self):
|
||||
"""Test that invalid input types raise TypeError"""
|
||||
with pytest.raises(TypeError, match="safe_json_dict\\(\\) expects a dictionary \\(dict\\) as input"):
|
||||
safe_json_dict("not a dict")
|
||||
|
||||
with pytest.raises(TypeError, match="safe_json_dict\\(\\) expects a dictionary \\(dict\\) as input"):
|
||||
safe_json_dict([1, 2, 3])
|
||||
|
||||
with pytest.raises(TypeError, match="safe_json_dict\\(\\) expects a dictionary \\(dict\\) as input"):
|
||||
safe_json_dict(42)
|
||||
|
||||
def test_empty_dict(self):
|
||||
"""Test empty dictionary handling"""
|
||||
result = safe_json_dict({})
|
||||
assert result == {}
|
||||
|
||||
def test_nested_dict_conversion(self):
|
||||
"""Test nested dictionary conversion"""
|
||||
test_dict = {
|
||||
"level1": {
|
||||
"level2": {"datetime": datetime(2024, 1, 1, 12, 0, 0, tzinfo=pytz.UTC), "decimal": Decimal("123.456")}
|
||||
}
|
||||
}
|
||||
result = safe_json_dict(test_dict)
|
||||
assert isinstance(result["level1"]["level2"]["datetime"], str)
|
||||
assert isinstance(result["level1"]["level2"]["decimal"], float)
|
||||
|
||||
|
||||
class TestToolInvokeMessageJsonSerialization:
|
||||
"""Test suite for ToolInvokeMessage JSON serialization through safe_json_value"""
|
||||
|
||||
def test_json_message_serialization(self):
|
||||
"""Test JSON message serialization with complex data"""
|
||||
complex_data = {
|
||||
"timestamp": datetime(2024, 1, 1, 12, 0, 0, tzinfo=pytz.UTC),
|
||||
"amount": Decimal("123.45"),
|
||||
"id": uuid4(),
|
||||
"binary": b"test data",
|
||||
"array": np.array([1, 2, 3]),
|
||||
}
|
||||
|
||||
# Create JSON message
|
||||
json_message = ToolInvokeMessage.JsonMessage(json_object=complex_data)
|
||||
message = ToolInvokeMessage(type=ToolInvokeMessage.MessageType.JSON, message=json_message)
|
||||
|
||||
# Apply safe_json_value transformation
|
||||
transformed_data = safe_json_value(message.message.json_object)
|
||||
|
||||
# Verify transformations
|
||||
assert isinstance(transformed_data["timestamp"], str)
|
||||
assert isinstance(transformed_data["amount"], float)
|
||||
assert isinstance(transformed_data["id"], str)
|
||||
assert isinstance(transformed_data["binary"], str)
|
||||
assert isinstance(transformed_data["array"], list)
|
||||
|
||||
# Verify JSON serialization works
|
||||
json_string = json.dumps(transformed_data, ensure_ascii=False)
|
||||
assert isinstance(json_string, str)
|
||||
|
||||
# Verify we can deserialize back
|
||||
deserialized = json.loads(json_string)
|
||||
assert deserialized["amount"] == 123.45
|
||||
assert deserialized["array"] == [1, 2, 3]
|
||||
|
||||
def test_json_message_with_nested_structures(self):
|
||||
"""Test JSON message with deeply nested complex structures"""
|
||||
nested_data = {
|
||||
"level1": {
|
||||
"level2": {
|
||||
"level3": {
|
||||
"dates": [date(2024, 1, 1), date(2024, 1, 2)],
|
||||
"timestamps": [datetime(2024, 1, 1, 12, 0, 0, tzinfo=pytz.UTC)],
|
||||
"numbers": [Decimal("1.1"), Decimal("2.2")],
|
||||
"arrays": [np.array([1, 2]), np.array([3, 4])],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json_message = ToolInvokeMessage.JsonMessage(json_object=nested_data)
|
||||
message = ToolInvokeMessage(type=ToolInvokeMessage.MessageType.JSON, message=json_message)
|
||||
|
||||
# Transform the data
|
||||
transformed_data = safe_json_value(message.message.json_object)
|
||||
|
||||
# Verify nested transformations
|
||||
level3 = transformed_data["level1"]["level2"]["level3"]
|
||||
assert all(isinstance(d, str) for d in level3["dates"])
|
||||
assert all(isinstance(t, str) for t in level3["timestamps"])
|
||||
assert all(isinstance(n, float) for n in level3["numbers"])
|
||||
assert all(isinstance(a, list) for a in level3["arrays"])
|
||||
|
||||
# Test JSON serialization
|
||||
json_string = json.dumps(transformed_data, ensure_ascii=False)
|
||||
assert isinstance(json_string, str)
|
||||
|
||||
# Verify deserialization
|
||||
deserialized = json.loads(json_string)
|
||||
assert deserialized["level1"]["level2"]["level3"]["numbers"] == [1.1, 2.2]
|
||||
|
||||
def test_json_message_transformer_integration(self):
|
||||
"""Test integration with ToolFileMessageTransformer for JSON messages"""
|
||||
complex_data = {
|
||||
"metadata": {
|
||||
"created_at": datetime(2024, 1, 1, 12, 0, 0, tzinfo=pytz.UTC),
|
||||
"version": Decimal("1.0"),
|
||||
"tags": ["tag1", "tag2"],
|
||||
},
|
||||
"data": {"values": np.array([1.1, 2.2, 3.3]), "binary": b"binary content"},
|
||||
}
|
||||
|
||||
# Create message generator
|
||||
def message_generator():
|
||||
json_message = ToolInvokeMessage.JsonMessage(json_object=complex_data)
|
||||
message = ToolInvokeMessage(type=ToolInvokeMessage.MessageType.JSON, message=json_message)
|
||||
yield message
|
||||
|
||||
# Transform messages
|
||||
transformed_messages = list(
|
||||
ToolFileMessageTransformer.transform_tool_invoke_messages(
|
||||
message_generator(), user_id="test_user", tenant_id="test_tenant"
|
||||
)
|
||||
)
|
||||
|
||||
assert len(transformed_messages) == 1
|
||||
transformed_message = transformed_messages[0]
|
||||
assert transformed_message.type == ToolInvokeMessage.MessageType.JSON
|
||||
|
||||
# Verify the JSON object was transformed
|
||||
json_obj = transformed_message.message.json_object
|
||||
assert isinstance(json_obj["metadata"]["created_at"], str)
|
||||
assert isinstance(json_obj["metadata"]["version"], float)
|
||||
assert isinstance(json_obj["data"]["values"], list)
|
||||
assert isinstance(json_obj["data"]["binary"], str)
|
||||
|
||||
# Test final JSON serialization
|
||||
final_json = json.dumps(json_obj, ensure_ascii=False)
|
||||
assert isinstance(final_json, str)
|
||||
|
||||
# Verify we can deserialize
|
||||
deserialized = json.loads(final_json)
|
||||
assert deserialized["metadata"]["version"] == 1.0
|
||||
assert deserialized["data"]["values"] == [1.1, 2.2, 3.3]
|
||||
|
||||
def test_edge_cases_and_error_handling(self):
|
||||
"""Test edge cases and error handling in JSON serialization"""
|
||||
# Test with None values
|
||||
data_with_none = {"null_value": None, "empty_string": "", "zero": 0, "false_value": False}
|
||||
|
||||
json_message = ToolInvokeMessage.JsonMessage(json_object=data_with_none)
|
||||
message = ToolInvokeMessage(type=ToolInvokeMessage.MessageType.JSON, message=json_message)
|
||||
|
||||
transformed_data = safe_json_value(message.message.json_object)
|
||||
json_string = json.dumps(transformed_data, ensure_ascii=False)
|
||||
|
||||
# Verify serialization works with edge cases
|
||||
assert json_string is not None
|
||||
deserialized = json.loads(json_string)
|
||||
assert deserialized["null_value"] is None
|
||||
assert deserialized["empty_string"] == ""
|
||||
assert deserialized["zero"] == 0
|
||||
assert deserialized["false_value"] is False
|
||||
|
||||
# Test with very large numbers
|
||||
large_data = {
|
||||
"large_int": 2**63 - 1,
|
||||
"large_float": 1.7976931348623157e308,
|
||||
"small_float": 2.2250738585072014e-308,
|
||||
}
|
||||
|
||||
json_message = ToolInvokeMessage.JsonMessage(json_object=large_data)
|
||||
message = ToolInvokeMessage(type=ToolInvokeMessage.MessageType.JSON, message=json_message)
|
||||
|
||||
transformed_data = safe_json_value(message.message.json_object)
|
||||
json_string = json.dumps(transformed_data, ensure_ascii=False)
|
||||
|
||||
# Verify large numbers are handled correctly
|
||||
deserialized = json.loads(json_string)
|
||||
assert deserialized["large_int"] == 2**63 - 1
|
||||
assert deserialized["large_float"] == 1.7976931348623157e308
|
||||
assert deserialized["small_float"] == 2.2250738585072014e-308
|
||||
|
||||
|
||||
class TestEndToEndSerialization:
|
||||
"""Test suite for end-to-end serialization workflow"""
|
||||
|
||||
def test_complete_workflow_with_real_data(self):
|
||||
"""Test complete workflow from complex data to JSON string and back"""
|
||||
# Simulate real-world complex data structure
|
||||
real_world_data = {
|
||||
"user_profile": {
|
||||
"id": uuid4(),
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"created_at": datetime(2024, 1, 1, 12, 0, 0, tzinfo=pytz.UTC),
|
||||
"last_login": datetime(2024, 1, 15, 14, 30, 0, tzinfo=pytz.UTC),
|
||||
"preferences": {"theme": "dark", "language": "en", "timezone": "UTC"},
|
||||
},
|
||||
"analytics": {
|
||||
"session_count": 42,
|
||||
"total_time": Decimal("123.45"),
|
||||
"metrics": np.array([1.1, 2.2, 3.3, 4.4, 5.5]),
|
||||
"events": [
|
||||
{
|
||||
"timestamp": datetime(2024, 1, 1, 10, 0, 0, tzinfo=pytz.UTC),
|
||||
"action": "login",
|
||||
"duration": Decimal("5.67"),
|
||||
},
|
||||
{
|
||||
"timestamp": datetime(2024, 1, 1, 11, 0, 0, tzinfo=pytz.UTC),
|
||||
"action": "logout",
|
||||
"duration": Decimal("3600.0"),
|
||||
},
|
||||
],
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"id": uuid4(),
|
||||
"name": "document.pdf",
|
||||
"size": 1024,
|
||||
"uploaded_at": datetime(2024, 1, 1, 9, 0, 0, tzinfo=pytz.UTC),
|
||||
"checksum": b"abc123def456",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
# Step 1: Create ToolInvokeMessage
|
||||
json_message = ToolInvokeMessage.JsonMessage(json_object=real_world_data)
|
||||
message = ToolInvokeMessage(type=ToolInvokeMessage.MessageType.JSON, message=json_message)
|
||||
|
||||
# Step 2: Apply safe_json_value transformation
|
||||
transformed_data = safe_json_value(message.message.json_object)
|
||||
|
||||
# Step 3: Serialize to JSON string
|
||||
json_string = json.dumps(transformed_data, ensure_ascii=False)
|
||||
|
||||
# Step 4: Verify the string is valid JSON
|
||||
assert isinstance(json_string, str)
|
||||
assert json_string.startswith("{")
|
||||
assert json_string.endswith("}")
|
||||
|
||||
# Step 5: Deserialize back to Python object
|
||||
deserialized_data = json.loads(json_string)
|
||||
|
||||
# Step 6: Verify data integrity
|
||||
assert deserialized_data["user_profile"]["name"] == "John Doe"
|
||||
assert deserialized_data["user_profile"]["email"] == "john@example.com"
|
||||
assert isinstance(deserialized_data["user_profile"]["created_at"], str)
|
||||
assert isinstance(deserialized_data["analytics"]["total_time"], float)
|
||||
assert deserialized_data["analytics"]["total_time"] == 123.45
|
||||
assert isinstance(deserialized_data["analytics"]["metrics"], list)
|
||||
assert deserialized_data["analytics"]["metrics"] == [1.1, 2.2, 3.3, 4.4, 5.5]
|
||||
assert isinstance(deserialized_data["files"][0]["checksum"], str)
|
||||
|
||||
# Step 7: Verify all complex types were properly converted
|
||||
self._verify_all_complex_types_converted(deserialized_data)
|
||||
|
||||
def _verify_all_complex_types_converted(self, data):
|
||||
"""Helper method to verify all complex types were properly converted"""
|
||||
if isinstance(data, dict):
|
||||
for key, value in data.items():
|
||||
if key in ["id", "checksum"]:
|
||||
# These should be strings (UUID/bytes converted)
|
||||
assert isinstance(value, str)
|
||||
elif key in ["created_at", "last_login", "timestamp", "uploaded_at"]:
|
||||
# These should be strings (datetime converted)
|
||||
assert isinstance(value, str)
|
||||
elif key in ["total_time", "duration"]:
|
||||
# These should be floats (Decimal converted)
|
||||
assert isinstance(value, float)
|
||||
elif key == "metrics":
|
||||
# This should be a list (ndarray converted)
|
||||
assert isinstance(value, list)
|
||||
else:
|
||||
# Recursively check nested structures
|
||||
self._verify_all_complex_types_converted(value)
|
||||
elif isinstance(data, list):
|
||||
for item in data:
|
||||
self._verify_all_complex_types_converted(item)
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
"""Tests for Celery SSL configuration."""
|
||||
|
||||
import ssl
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
class TestCelerySSLConfiguration:
|
||||
"""Test suite for Celery SSL configuration."""
|
||||
|
||||
def test_get_celery_ssl_options_when_ssl_disabled(self):
|
||||
"""Test SSL options when REDIS_USE_SSL is False."""
|
||||
mock_config = MagicMock()
|
||||
mock_config.REDIS_USE_SSL = False
|
||||
|
||||
with patch("extensions.ext_celery.dify_config", mock_config):
|
||||
from extensions.ext_celery import _get_celery_ssl_options
|
||||
|
||||
result = _get_celery_ssl_options()
|
||||
assert result is None
|
||||
|
||||
def test_get_celery_ssl_options_when_broker_not_redis(self):
|
||||
"""Test SSL options when broker is not Redis."""
|
||||
mock_config = MagicMock()
|
||||
mock_config.REDIS_USE_SSL = True
|
||||
mock_config.CELERY_BROKER_URL = "amqp://localhost:5672"
|
||||
|
||||
with patch("extensions.ext_celery.dify_config", mock_config):
|
||||
from extensions.ext_celery import _get_celery_ssl_options
|
||||
|
||||
result = _get_celery_ssl_options()
|
||||
assert result is None
|
||||
|
||||
def test_get_celery_ssl_options_with_cert_none(self):
|
||||
"""Test SSL options with CERT_NONE requirement."""
|
||||
mock_config = MagicMock()
|
||||
mock_config.REDIS_USE_SSL = True
|
||||
mock_config.CELERY_BROKER_URL = "redis://localhost:6379/0"
|
||||
mock_config.REDIS_SSL_CERT_REQS = "CERT_NONE"
|
||||
mock_config.REDIS_SSL_CA_CERTS = None
|
||||
mock_config.REDIS_SSL_CERTFILE = None
|
||||
mock_config.REDIS_SSL_KEYFILE = None
|
||||
|
||||
with patch("extensions.ext_celery.dify_config", mock_config):
|
||||
from extensions.ext_celery import _get_celery_ssl_options
|
||||
|
||||
result = _get_celery_ssl_options()
|
||||
assert result is not None
|
||||
assert result["ssl_cert_reqs"] == ssl.CERT_NONE
|
||||
assert result["ssl_ca_certs"] is None
|
||||
assert result["ssl_certfile"] is None
|
||||
assert result["ssl_keyfile"] is None
|
||||
|
||||
def test_get_celery_ssl_options_with_cert_required(self):
|
||||
"""Test SSL options with CERT_REQUIRED and certificates."""
|
||||
mock_config = MagicMock()
|
||||
mock_config.REDIS_USE_SSL = True
|
||||
mock_config.CELERY_BROKER_URL = "rediss://localhost:6380/0"
|
||||
mock_config.REDIS_SSL_CERT_REQS = "CERT_REQUIRED"
|
||||
mock_config.REDIS_SSL_CA_CERTS = "/path/to/ca.crt"
|
||||
mock_config.REDIS_SSL_CERTFILE = "/path/to/client.crt"
|
||||
mock_config.REDIS_SSL_KEYFILE = "/path/to/client.key"
|
||||
|
||||
with patch("extensions.ext_celery.dify_config", mock_config):
|
||||
from extensions.ext_celery import _get_celery_ssl_options
|
||||
|
||||
result = _get_celery_ssl_options()
|
||||
assert result is not None
|
||||
assert result["ssl_cert_reqs"] == ssl.CERT_REQUIRED
|
||||
assert result["ssl_ca_certs"] == "/path/to/ca.crt"
|
||||
assert result["ssl_certfile"] == "/path/to/client.crt"
|
||||
assert result["ssl_keyfile"] == "/path/to/client.key"
|
||||
|
||||
def test_get_celery_ssl_options_with_cert_optional(self):
|
||||
"""Test SSL options with CERT_OPTIONAL requirement."""
|
||||
mock_config = MagicMock()
|
||||
mock_config.REDIS_USE_SSL = True
|
||||
mock_config.CELERY_BROKER_URL = "redis://localhost:6379/0"
|
||||
mock_config.REDIS_SSL_CERT_REQS = "CERT_OPTIONAL"
|
||||
mock_config.REDIS_SSL_CA_CERTS = "/path/to/ca.crt"
|
||||
mock_config.REDIS_SSL_CERTFILE = None
|
||||
mock_config.REDIS_SSL_KEYFILE = None
|
||||
|
||||
with patch("extensions.ext_celery.dify_config", mock_config):
|
||||
from extensions.ext_celery import _get_celery_ssl_options
|
||||
|
||||
result = _get_celery_ssl_options()
|
||||
assert result is not None
|
||||
assert result["ssl_cert_reqs"] == ssl.CERT_OPTIONAL
|
||||
assert result["ssl_ca_certs"] == "/path/to/ca.crt"
|
||||
|
||||
def test_get_celery_ssl_options_with_invalid_cert_reqs(self):
|
||||
"""Test SSL options with invalid cert requirement defaults to CERT_NONE."""
|
||||
mock_config = MagicMock()
|
||||
mock_config.REDIS_USE_SSL = True
|
||||
mock_config.CELERY_BROKER_URL = "redis://localhost:6379/0"
|
||||
mock_config.REDIS_SSL_CERT_REQS = "INVALID_VALUE"
|
||||
mock_config.REDIS_SSL_CA_CERTS = None
|
||||
mock_config.REDIS_SSL_CERTFILE = None
|
||||
mock_config.REDIS_SSL_KEYFILE = None
|
||||
|
||||
with patch("extensions.ext_celery.dify_config", mock_config):
|
||||
from extensions.ext_celery import _get_celery_ssl_options
|
||||
|
||||
result = _get_celery_ssl_options()
|
||||
assert result is not None
|
||||
assert result["ssl_cert_reqs"] == ssl.CERT_NONE # Should default to CERT_NONE
|
||||
|
||||
def test_celery_init_applies_ssl_to_broker_and_backend(self):
|
||||
"""Test that SSL options are applied to both broker and backend when using Redis."""
|
||||
mock_config = MagicMock()
|
||||
mock_config.REDIS_USE_SSL = True
|
||||
mock_config.CELERY_BROKER_URL = "redis://localhost:6379/0"
|
||||
mock_config.CELERY_BACKEND = "redis"
|
||||
mock_config.CELERY_RESULT_BACKEND = "redis://localhost:6379/0"
|
||||
mock_config.REDIS_SSL_CERT_REQS = "CERT_NONE"
|
||||
mock_config.REDIS_SSL_CA_CERTS = None
|
||||
mock_config.REDIS_SSL_CERTFILE = None
|
||||
mock_config.REDIS_SSL_KEYFILE = None
|
||||
mock_config.CELERY_USE_SENTINEL = False
|
||||
mock_config.LOG_FORMAT = "%(message)s"
|
||||
mock_config.LOG_TZ = "UTC"
|
||||
mock_config.LOG_FILE = None
|
||||
|
||||
# Mock all the scheduler configs
|
||||
mock_config.CELERY_BEAT_SCHEDULER_TIME = 1
|
||||
mock_config.ENABLE_CLEAN_EMBEDDING_CACHE_TASK = False
|
||||
mock_config.ENABLE_CLEAN_UNUSED_DATASETS_TASK = False
|
||||
mock_config.ENABLE_CREATE_TIDB_SERVERLESS_TASK = False
|
||||
mock_config.ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK = False
|
||||
mock_config.ENABLE_CLEAN_MESSAGES = False
|
||||
mock_config.ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK = False
|
||||
mock_config.ENABLE_DATASETS_QUEUE_MONITOR = False
|
||||
mock_config.ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK = False
|
||||
|
||||
with patch("extensions.ext_celery.dify_config", mock_config):
|
||||
from dify_app import DifyApp
|
||||
from extensions.ext_celery import init_app
|
||||
|
||||
app = DifyApp(__name__)
|
||||
celery_app = init_app(app)
|
||||
|
||||
# Check that SSL options were applied
|
||||
assert "broker_use_ssl" in celery_app.conf
|
||||
assert celery_app.conf["broker_use_ssl"] is not None
|
||||
assert celery_app.conf["broker_use_ssl"]["ssl_cert_reqs"] == ssl.CERT_NONE
|
||||
|
||||
# Check that SSL is also applied to Redis backend
|
||||
assert "redis_backend_use_ssl" in celery_app.conf
|
||||
assert celery_app.conf["redis_backend_use_ssl"] is not None
|
||||
15
api/uv.lock
15
api/uv.lock
|
|
@ -1253,6 +1253,7 @@ dependencies = [
|
|||
{ name = "flask-cors" },
|
||||
{ name = "flask-login" },
|
||||
{ name = "flask-migrate" },
|
||||
{ name = "flask-orjson" },
|
||||
{ name = "flask-restful" },
|
||||
{ name = "flask-sqlalchemy" },
|
||||
{ name = "gevent" },
|
||||
|
|
@ -1440,6 +1441,7 @@ requires-dist = [
|
|||
{ name = "flask-cors", specifier = "~=6.0.0" },
|
||||
{ name = "flask-login", specifier = "~=0.6.3" },
|
||||
{ name = "flask-migrate", specifier = "~=4.0.7" },
|
||||
{ name = "flask-orjson", specifier = "~=2.0.0" },
|
||||
{ name = "flask-restful", specifier = "~=0.3.10" },
|
||||
{ name = "flask-sqlalchemy", specifier = "~=3.1.1" },
|
||||
{ name = "gevent", specifier = "~=24.11.1" },
|
||||
|
|
@ -1859,6 +1861,19 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/93/01/587023575286236f95d2ab8a826c320375ed5ea2102bb103ed89704ffa6b/Flask_Migrate-4.0.7-py3-none-any.whl", hash = "sha256:5c532be17e7b43a223b7500d620edae33795df27c75811ddf32560f7d48ec617", size = 21127, upload-time = "2024-03-11T18:42:59.462Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flask-orjson"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "flask" },
|
||||
{ name = "orjson" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/49/575796f6ddca171d82dbb12762e33166c8b8f8616c946f0a6dfbb9bc3cd6/flask_orjson-2.0.0.tar.gz", hash = "sha256:6df6631437f9bc52cf9821735f896efa5583b5f80712f7d29d9ef69a79986a9c", size = 2974, upload-time = "2024-01-15T00:03:22.236Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/ca/53e14be018a2284acf799830e8cd8e0b263c0fd3dff1ad7b35f8417e7067/flask_orjson-2.0.0-py3-none-any.whl", hash = "sha256:5d15f2ba94b8d6c02aee88fc156045016e83db9eda2c30545fabd640aebaec9d", size = 3622, upload-time = "2024-01-15T00:03:17.511Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flask-restful"
|
||||
version = "0.3.10"
|
||||
|
|
|
|||
|
|
@ -264,6 +264,15 @@ REDIS_PORT=6379
|
|||
REDIS_USERNAME=
|
||||
REDIS_PASSWORD=difyai123456
|
||||
REDIS_USE_SSL=false
|
||||
# SSL configuration for Redis (when REDIS_USE_SSL=true)
|
||||
REDIS_SSL_CERT_REQS=CERT_NONE
|
||||
# Options: CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED
|
||||
REDIS_SSL_CA_CERTS=
|
||||
# Path to CA certificate file for SSL verification
|
||||
REDIS_SSL_CERTFILE=
|
||||
# Path to client certificate file for SSL authentication
|
||||
REDIS_SSL_KEYFILE=
|
||||
# Path to client private key file for SSL authentication
|
||||
REDIS_DB=0
|
||||
|
||||
# Whether to use Redis Sentinel mode.
|
||||
|
|
@ -861,7 +870,7 @@ WORKFLOW_NODE_EXECUTION_STORAGE=rdbms
|
|||
|
||||
# Repository configuration
|
||||
# Core workflow execution repository implementation
|
||||
# Options:
|
||||
# Options:
|
||||
# - core.repositories.sqlalchemy_workflow_execution_repository.SQLAlchemyWorkflowExecutionRepository (default)
|
||||
# - core.repositories.celery_workflow_execution_repository.CeleryWorkflowExecutionRepository
|
||||
CORE_WORKFLOW_EXECUTION_REPOSITORY=core.repositories.sqlalchemy_workflow_execution_repository.SQLAlchemyWorkflowExecutionRepository
|
||||
|
|
@ -1157,6 +1166,9 @@ MARKETPLACE_API_URL=https://marketplace.dify.ai
|
|||
|
||||
FORCE_VERIFYING_SIGNATURE=true
|
||||
|
||||
PLUGIN_STDIO_BUFFER_SIZE=1024
|
||||
PLUGIN_STDIO_MAX_BUFFER_SIZE=5242880
|
||||
|
||||
PLUGIN_PYTHON_ENV_INIT_TIMEOUT=120
|
||||
PLUGIN_MAX_EXECUTION_TIMEOUT=600
|
||||
# PIP_MIRROR_URL=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
|
|
|||
|
|
@ -181,6 +181,8 @@ services:
|
|||
FORCE_VERIFYING_SIGNATURE: ${FORCE_VERIFYING_SIGNATURE:-true}
|
||||
PYTHON_ENV_INIT_TIMEOUT: ${PLUGIN_PYTHON_ENV_INIT_TIMEOUT:-120}
|
||||
PLUGIN_MAX_EXECUTION_TIMEOUT: ${PLUGIN_MAX_EXECUTION_TIMEOUT:-600}
|
||||
PLUGIN_STDIO_BUFFER_SIZE: ${PLUGIN_STDIO_BUFFER_SIZE:-1024}
|
||||
PLUGIN_STDIO_MAX_BUFFER_SIZE: ${PLUGIN_STDIO_MAX_BUFFER_SIZE:-5242880}
|
||||
PIP_MIRROR_URL: ${PIP_MIRROR_URL:-}
|
||||
PLUGIN_STORAGE_TYPE: ${PLUGIN_STORAGE_TYPE:-local}
|
||||
PLUGIN_STORAGE_LOCAL_ROOT: ${PLUGIN_STORAGE_LOCAL_ROOT:-/app/storage}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,10 @@ x-shared-env: &shared-api-worker-env
|
|||
REDIS_USERNAME: ${REDIS_USERNAME:-}
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD:-difyai123456}
|
||||
REDIS_USE_SSL: ${REDIS_USE_SSL:-false}
|
||||
REDIS_SSL_CERT_REQS: ${REDIS_SSL_CERT_REQS:-CERT_NONE}
|
||||
REDIS_SSL_CA_CERTS: ${REDIS_SSL_CA_CERTS:-}
|
||||
REDIS_SSL_CERTFILE: ${REDIS_SSL_CERTFILE:-}
|
||||
REDIS_SSL_KEYFILE: ${REDIS_SSL_KEYFILE:-}
|
||||
REDIS_DB: ${REDIS_DB:-0}
|
||||
REDIS_USE_SENTINEL: ${REDIS_USE_SENTINEL:-false}
|
||||
REDIS_SENTINELS: ${REDIS_SENTINELS:-}
|
||||
|
|
@ -506,6 +510,8 @@ x-shared-env: &shared-api-worker-env
|
|||
MARKETPLACE_ENABLED: ${MARKETPLACE_ENABLED:-true}
|
||||
MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-https://marketplace.dify.ai}
|
||||
FORCE_VERIFYING_SIGNATURE: ${FORCE_VERIFYING_SIGNATURE:-true}
|
||||
PLUGIN_STDIO_BUFFER_SIZE: ${PLUGIN_STDIO_BUFFER_SIZE:-1024}
|
||||
PLUGIN_STDIO_MAX_BUFFER_SIZE: ${PLUGIN_STDIO_MAX_BUFFER_SIZE:-5242880}
|
||||
PLUGIN_PYTHON_ENV_INIT_TIMEOUT: ${PLUGIN_PYTHON_ENV_INIT_TIMEOUT:-120}
|
||||
PLUGIN_MAX_EXECUTION_TIMEOUT: ${PLUGIN_MAX_EXECUTION_TIMEOUT:-600}
|
||||
PIP_MIRROR_URL: ${PIP_MIRROR_URL:-}
|
||||
|
|
@ -747,6 +753,8 @@ services:
|
|||
FORCE_VERIFYING_SIGNATURE: ${FORCE_VERIFYING_SIGNATURE:-true}
|
||||
PYTHON_ENV_INIT_TIMEOUT: ${PLUGIN_PYTHON_ENV_INIT_TIMEOUT:-120}
|
||||
PLUGIN_MAX_EXECUTION_TIMEOUT: ${PLUGIN_MAX_EXECUTION_TIMEOUT:-600}
|
||||
PLUGIN_STDIO_BUFFER_SIZE: ${PLUGIN_STDIO_BUFFER_SIZE:-1024}
|
||||
PLUGIN_STDIO_MAX_BUFFER_SIZE: ${PLUGIN_STDIO_MAX_BUFFER_SIZE:-5242880}
|
||||
PIP_MIRROR_URL: ${PIP_MIRROR_URL:-}
|
||||
PLUGIN_STORAGE_TYPE: ${PLUGIN_STORAGE_TYPE:-local}
|
||||
PLUGIN_STORAGE_LOCAL_ROOT: ${PLUGIN_STORAGE_LOCAL_ROOT:-/app/storage}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ LABEL maintainer="takatost@gmail.com"
|
|||
# RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
|
||||
RUN apk add --no-cache tzdata
|
||||
RUN npm install -g pnpm@10.13.1
|
||||
RUN corepack enable
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
|
|
@ -19,6 +19,9 @@ WORKDIR /app/web
|
|||
COPY package.json .
|
||||
COPY pnpm-lock.yaml .
|
||||
|
||||
# Use packageManager from package.json
|
||||
RUN corepack install
|
||||
|
||||
# if you located in China, you can use taobao registry to speed up
|
||||
# RUN pnpm install --frozen-lockfile --registry https://registry.npmmirror.com/
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {
|
|||
} from '@heroicons/react/24/outline'
|
||||
import { RiCloseLine, RiEditFill } from '@remixicon/react'
|
||||
import { get } from 'lodash-es'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import dayjs from 'dayjs'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
|
|
@ -111,7 +110,8 @@ const statusTdRender = (statusCount: StatusCount) => {
|
|||
|
||||
const getFormattedChatList = (messages: ChatMessage[], conversationId: string, timezone: string, format: string) => {
|
||||
const newChatList: IChatItem[] = []
|
||||
messages.forEach((item: ChatMessage) => {
|
||||
try {
|
||||
messages.forEach((item: ChatMessage) => {
|
||||
const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
|
||||
newChatList.push({
|
||||
id: `question-${item.id}`,
|
||||
|
|
@ -178,7 +178,13 @@ const getFormattedChatList = (messages: ChatMessage[], conversationId: string, t
|
|||
parentMessageId: `question-${item.id}`,
|
||||
})
|
||||
})
|
||||
return newChatList
|
||||
|
||||
return newChatList
|
||||
}
|
||||
catch (error) {
|
||||
console.error('getFormattedChatList processing failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
type IDetailPanel = {
|
||||
|
|
@ -188,6 +194,9 @@ type IDetailPanel = {
|
|||
}
|
||||
|
||||
function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
const MIN_ITEMS_FOR_SCROLL_LOADING = 8
|
||||
const SCROLL_THRESHOLD_PX = 50
|
||||
const SCROLL_DEBOUNCE_MS = 200
|
||||
const { userProfile: { timezone } } = useAppContext()
|
||||
const { formatTime } = useTimestamp()
|
||||
const { onClose, appDetail } = useContext(DrawerContext)
|
||||
|
|
@ -204,13 +213,19 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
|||
const { t } = useTranslation()
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [varValues, setVarValues] = useState<Record<string, string>>({})
|
||||
const isLoadingRef = useRef(false)
|
||||
|
||||
const [allChatItems, setAllChatItems] = useState<IChatItem[]>([])
|
||||
const [chatItemTree, setChatItemTree] = useState<ChatItemInTree[]>([])
|
||||
const [threadChatItems, setThreadChatItems] = useState<IChatItem[]>([])
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (isLoadingRef.current)
|
||||
return
|
||||
|
||||
try {
|
||||
isLoadingRef.current = true
|
||||
|
||||
if (!hasMore)
|
||||
return
|
||||
|
||||
|
|
@ -218,8 +233,11 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
|||
conversation_id: detail.id,
|
||||
limit: 10,
|
||||
}
|
||||
if (allChatItems[0]?.id)
|
||||
params.first_id = allChatItems[0]?.id.replace('question-', '')
|
||||
// Use the oldest answer item ID for pagination
|
||||
const answerItems = allChatItems.filter(item => item.isAnswer)
|
||||
const oldestAnswerItem = answerItems[answerItems.length - 1]
|
||||
if (oldestAnswerItem?.id)
|
||||
params.first_id = oldestAnswerItem.id
|
||||
const messageRes = await fetchChatMessages({
|
||||
url: `/apps/${appDetail?.id}/chat-messages`,
|
||||
params,
|
||||
|
|
@ -249,15 +267,20 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
|||
}
|
||||
setChatItemTree(tree)
|
||||
|
||||
setThreadChatItems(getThreadMessages(tree, newAllChatItems.at(-1)?.id))
|
||||
const lastMessageId = newAllChatItems.length > 0 ? newAllChatItems[newAllChatItems.length - 1].id : undefined
|
||||
setThreadChatItems(getThreadMessages(tree, lastMessageId))
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
console.error('fetchData execution failed:', err)
|
||||
}
|
||||
finally {
|
||||
isLoadingRef.current = false
|
||||
}
|
||||
}, [allChatItems, detail.id, hasMore, timezone, t, appDetail, detail?.model_config?.configs?.introduction])
|
||||
|
||||
const switchSibling = useCallback((siblingMessageId: string) => {
|
||||
setThreadChatItems(getThreadMessages(chatItemTree, siblingMessageId))
|
||||
const newThreadChatItems = getThreadMessages(chatItemTree, siblingMessageId)
|
||||
setThreadChatItems(newThreadChatItems)
|
||||
}, [chatItemTree])
|
||||
|
||||
const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => {
|
||||
|
|
@ -344,13 +367,217 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
|||
|
||||
const fetchInitiated = useRef(false)
|
||||
|
||||
// Only load initial messages, don't auto-load more
|
||||
useEffect(() => {
|
||||
if (appDetail?.id && detail.id && appDetail?.mode !== 'completion' && !fetchInitiated.current) {
|
||||
// Mark as initialized, but don't auto-load more messages
|
||||
fetchInitiated.current = true
|
||||
// Still call fetchData to get initial messages
|
||||
fetchData()
|
||||
}
|
||||
}, [appDetail?.id, detail.id, appDetail?.mode, fetchData])
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const loadMoreMessages = useCallback(async () => {
|
||||
if (isLoading || !hasMore || !appDetail?.id || !detail.id)
|
||||
return
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const params: ChatMessagesRequest = {
|
||||
conversation_id: detail.id,
|
||||
limit: 10,
|
||||
}
|
||||
|
||||
// Use the earliest response item as the first_id
|
||||
const answerItems = allChatItems.filter(item => item.isAnswer)
|
||||
const oldestAnswerItem = answerItems[answerItems.length - 1]
|
||||
if (oldestAnswerItem?.id) {
|
||||
params.first_id = oldestAnswerItem.id
|
||||
}
|
||||
else if (allChatItems.length > 0 && allChatItems[0]?.id) {
|
||||
const firstId = allChatItems[0].id.replace('question-', '').replace('answer-', '')
|
||||
params.first_id = firstId
|
||||
}
|
||||
|
||||
const messageRes = await fetchChatMessages({
|
||||
url: `/apps/${appDetail.id}/chat-messages`,
|
||||
params,
|
||||
})
|
||||
|
||||
if (!messageRes.data || messageRes.data.length === 0) {
|
||||
setHasMore(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (messageRes.data.length > 0) {
|
||||
const varValues = messageRes.data.at(-1)!.inputs
|
||||
setVarValues(varValues)
|
||||
}
|
||||
|
||||
setHasMore(messageRes.has_more)
|
||||
|
||||
const newItems = getFormattedChatList(
|
||||
messageRes.data,
|
||||
detail.id,
|
||||
timezone!,
|
||||
t('appLog.dateTimeFormat') as string,
|
||||
)
|
||||
|
||||
// Check for duplicate messages
|
||||
const existingIds = new Set(allChatItems.map(item => item.id))
|
||||
const uniqueNewItems = newItems.filter(item => !existingIds.has(item.id))
|
||||
|
||||
if (uniqueNewItems.length === 0) {
|
||||
if (allChatItems.length > 1) {
|
||||
const nextId = allChatItems[1].id.replace('question-', '').replace('answer-', '')
|
||||
|
||||
const retryParams = {
|
||||
...params,
|
||||
first_id: nextId,
|
||||
}
|
||||
|
||||
const retryRes = await fetchChatMessages({
|
||||
url: `/apps/${appDetail.id}/chat-messages`,
|
||||
params: retryParams,
|
||||
})
|
||||
|
||||
if (retryRes.data && retryRes.data.length > 0) {
|
||||
const retryItems = getFormattedChatList(
|
||||
retryRes.data,
|
||||
detail.id,
|
||||
timezone!,
|
||||
t('appLog.dateTimeFormat') as string,
|
||||
)
|
||||
|
||||
const retryUniqueItems = retryItems.filter(item => !existingIds.has(item.id))
|
||||
if (retryUniqueItems.length > 0) {
|
||||
const newAllChatItems = [
|
||||
...retryUniqueItems,
|
||||
...allChatItems,
|
||||
]
|
||||
|
||||
setAllChatItems(newAllChatItems)
|
||||
|
||||
let tree = buildChatItemTree(newAllChatItems)
|
||||
if (retryRes.has_more === false && detail?.model_config?.configs?.introduction) {
|
||||
tree = [{
|
||||
id: 'introduction',
|
||||
isAnswer: true,
|
||||
isOpeningStatement: true,
|
||||
content: detail?.model_config?.configs?.introduction ?? 'hello',
|
||||
feedbackDisabled: true,
|
||||
children: tree,
|
||||
}]
|
||||
}
|
||||
setChatItemTree(tree)
|
||||
setHasMore(retryRes.has_more)
|
||||
setThreadChatItems(getThreadMessages(tree, newAllChatItems.at(-1)?.id))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newAllChatItems = [
|
||||
...uniqueNewItems,
|
||||
...allChatItems,
|
||||
]
|
||||
|
||||
setAllChatItems(newAllChatItems)
|
||||
|
||||
let tree = buildChatItemTree(newAllChatItems)
|
||||
if (messageRes.has_more === false && detail?.model_config?.configs?.introduction) {
|
||||
tree = [{
|
||||
id: 'introduction',
|
||||
isAnswer: true,
|
||||
isOpeningStatement: true,
|
||||
content: detail?.model_config?.configs?.introduction ?? 'hello',
|
||||
feedbackDisabled: true,
|
||||
children: tree,
|
||||
}]
|
||||
}
|
||||
setChatItemTree(tree)
|
||||
|
||||
setThreadChatItems(getThreadMessages(tree, newAllChatItems.at(-1)?.id))
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
setHasMore(false)
|
||||
}
|
||||
finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [allChatItems, detail.id, hasMore, isLoading, timezone, t, appDetail])
|
||||
|
||||
useEffect(() => {
|
||||
const scrollableDiv = document.getElementById('scrollableDiv')
|
||||
const outerDiv = scrollableDiv?.parentElement
|
||||
const chatContainer = document.querySelector('.mx-1.mb-1.grow.overflow-auto') as HTMLElement
|
||||
|
||||
let scrollContainer: HTMLElement | null = null
|
||||
|
||||
if (outerDiv && outerDiv.scrollHeight > outerDiv.clientHeight) {
|
||||
scrollContainer = outerDiv
|
||||
}
|
||||
else if (scrollableDiv && scrollableDiv.scrollHeight > scrollableDiv.clientHeight) {
|
||||
scrollContainer = scrollableDiv
|
||||
}
|
||||
else if (chatContainer && chatContainer.scrollHeight > chatContainer.clientHeight) {
|
||||
scrollContainer = chatContainer
|
||||
}
|
||||
else {
|
||||
const possibleContainers = document.querySelectorAll('.overflow-auto, .overflow-y-auto')
|
||||
for (let i = 0; i < possibleContainers.length; i++) {
|
||||
const container = possibleContainers[i] as HTMLElement
|
||||
if (container.scrollHeight > container.clientHeight) {
|
||||
scrollContainer = container
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!scrollContainer)
|
||||
return
|
||||
|
||||
let lastLoadTime = 0
|
||||
const throttleDelay = 200
|
||||
|
||||
const handleScroll = () => {
|
||||
const currentScrollTop = scrollContainer!.scrollTop
|
||||
const scrollHeight = scrollContainer!.scrollHeight
|
||||
const clientHeight = scrollContainer!.clientHeight
|
||||
|
||||
const distanceFromTop = currentScrollTop
|
||||
const distanceFromBottom = scrollHeight - currentScrollTop - clientHeight
|
||||
|
||||
const now = Date.now()
|
||||
|
||||
const isNearTop = distanceFromTop < 30
|
||||
// eslint-disable-next-line sonarjs/no-unused-vars
|
||||
const _distanceFromBottom = distanceFromBottom < 30
|
||||
if (isNearTop && hasMore && !isLoading && (now - lastLoadTime > throttleDelay)) {
|
||||
lastLoadTime = now
|
||||
loadMoreMessages()
|
||||
}
|
||||
}
|
||||
|
||||
scrollContainer.addEventListener('scroll', handleScroll, { passive: true })
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
if (e.deltaY < 0)
|
||||
handleScroll()
|
||||
}
|
||||
scrollContainer.addEventListener('wheel', handleWheel, { passive: true })
|
||||
|
||||
return () => {
|
||||
scrollContainer!.removeEventListener('scroll', handleScroll)
|
||||
scrollContainer!.removeEventListener('wheel', handleWheel)
|
||||
}
|
||||
}, [hasMore, isLoading, loadMoreMessages])
|
||||
|
||||
const isChatMode = appDetail?.mode !== 'completion'
|
||||
const isAdvanced = appDetail?.mode === 'advanced-chat'
|
||||
|
||||
|
|
@ -378,6 +605,36 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
|||
return () => cancelAnimationFrame(raf)
|
||||
}, [])
|
||||
|
||||
// Add scroll listener to ensure loading is triggered
|
||||
useEffect(() => {
|
||||
if (threadChatItems.length >= MIN_ITEMS_FOR_SCROLL_LOADING && hasMore) {
|
||||
const scrollableDiv = document.getElementById('scrollableDiv')
|
||||
|
||||
if (scrollableDiv) {
|
||||
let loadingTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
const handleScroll = () => {
|
||||
const { scrollTop } = scrollableDiv
|
||||
|
||||
// Trigger loading when scrolling near the top
|
||||
if (scrollTop < SCROLL_THRESHOLD_PX && !isLoadingRef.current) {
|
||||
if (loadingTimeout)
|
||||
clearTimeout(loadingTimeout)
|
||||
|
||||
loadingTimeout = setTimeout(fetchData, SCROLL_DEBOUNCE_MS) // 200ms debounce
|
||||
}
|
||||
}
|
||||
|
||||
scrollableDiv.addEventListener('scroll', handleScroll)
|
||||
return () => {
|
||||
scrollableDiv.removeEventListener('scroll', handleScroll)
|
||||
if (loadingTimeout)
|
||||
clearTimeout(loadingTimeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [threadChatItems.length, hasMore, fetchData])
|
||||
|
||||
return (
|
||||
<div ref={ref} className='flex h-full flex-col rounded-xl border-[0.5px] border-components-panel-border'>
|
||||
{/* Panel Header */}
|
||||
|
|
@ -439,8 +696,8 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
|||
siteInfo={null}
|
||||
/>
|
||||
</div>
|
||||
: threadChatItems.length < 8
|
||||
? <div className="mb-4 pt-4">
|
||||
: threadChatItems.length < MIN_ITEMS_FOR_SCROLL_LOADING ? (
|
||||
<div className="mb-4 pt-4">
|
||||
<Chat
|
||||
config={{
|
||||
appId: appDetail?.id,
|
||||
|
|
@ -466,35 +723,27 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
|||
switchSibling={switchSibling}
|
||||
/>
|
||||
</div>
|
||||
: <div
|
||||
) : (
|
||||
<div
|
||||
className="py-4"
|
||||
id="scrollableDiv"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column-reverse',
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
}}>
|
||||
{/* Put the scroll bar always on the bottom */}
|
||||
<InfiniteScroll
|
||||
scrollableTarget="scrollableDiv"
|
||||
dataLength={threadChatItems.length}
|
||||
next={fetchData}
|
||||
hasMore={hasMore}
|
||||
loader={<div className='system-xs-regular text-center text-text-tertiary'>{t('appLog.detail.loading')}...</div>}
|
||||
// endMessage={<div className='text-center'>Nothing more to show</div>}
|
||||
// below props only if you need pull down functionality
|
||||
refreshFunction={fetchData}
|
||||
pullDownToRefresh
|
||||
pullDownToRefreshThreshold={50}
|
||||
// pullDownToRefreshContent={
|
||||
// <div className='text-center'>Pull down to refresh</div>
|
||||
// }
|
||||
// releaseToRefreshContent={
|
||||
// <div className='text-center'>Release to refresh</div>
|
||||
// }
|
||||
// To put endMessage and loader to the top.
|
||||
style={{ display: 'flex', flexDirection: 'column-reverse' }}
|
||||
inverse={true}
|
||||
>
|
||||
<div className="flex w-full flex-col-reverse" style={{ position: 'relative' }}>
|
||||
{/* Loading state indicator - only shown when loading */}
|
||||
{hasMore && isLoading && (
|
||||
<div className="sticky left-0 right-0 top-0 z-10 bg-primary-50/40 py-3 text-center">
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{t('appLog.detail.loading')}...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Chat
|
||||
config={{
|
||||
appId: appDetail?.id,
|
||||
|
|
@ -519,8 +768,9 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
|||
chatContainerInnerClassName='px-3'
|
||||
switchSibling={switchSibling}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{showMessageLogModal && (
|
||||
|
|
|
|||
|
|
@ -407,8 +407,8 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||
}
|
||||
btnClassName={open =>
|
||||
cn(
|
||||
open ? '!bg-black/5 !shadow-none' : '!bg-transparent',
|
||||
'h-8 w-8 rounded-md border-none !p-2 hover:!bg-black/5',
|
||||
open ? '!bg-state-base-hover !shadow-none' : '!bg-transparent',
|
||||
'h-8 w-8 rounded-md border-none !p-2 hover:!bg-state-base-hover',
|
||||
)
|
||||
}
|
||||
popupClassName={
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import s from './style.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type ILoadingAnimProps = {
|
||||
type: 'text' | 'avatar'
|
||||
|
|
@ -11,7 +12,7 @@ const LoadingAnim: FC<ILoadingAnimProps> = ({
|
|||
type,
|
||||
}) => {
|
||||
return (
|
||||
<div className={`${s['dot-flashing']} ${s[type]}`}></div>
|
||||
<div className={cn(s['dot-flashing'], s[type])} />
|
||||
)
|
||||
}
|
||||
export default React.memo(LoadingAnim)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import Modal from '@/app/components/base/modal'
|
|||
import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import ConfirmAddVar from '@/app/components/app/configuration/config-prompt/confirm-add-var'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import type { OpeningStatement } from '@/app/components/base/features/types'
|
||||
import { getInputKeys } from '@/app/components/base/block-input'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
|
|
@ -101,7 +102,7 @@ const OpeningSettingModal = ({
|
|||
<div>·</div>
|
||||
<div>{tempSuggestedQuestions.length}/{MAX_QUESTION_NUM}</div>
|
||||
</div>
|
||||
<Divider bgStyle='gradient' className='ml-3 h-px w-0 grow'/>
|
||||
<Divider bgStyle='gradient' className='ml-3 h-px w-0 grow' />
|
||||
</div>
|
||||
<ReactSortable
|
||||
className="space-y-1"
|
||||
|
|
@ -178,19 +179,32 @@ const OpeningSettingModal = ({
|
|||
>
|
||||
<div className='mb-6 flex items-center justify-between'>
|
||||
<div className='title-2xl-semi-bold text-text-primary'>{t('appDebug.feature.conversationOpener.title')}</div>
|
||||
<div className='cursor-pointer p-1' onClick={onCancel}><RiCloseLine className='h-4 w-4 text-text-tertiary'/></div>
|
||||
<div className='cursor-pointer p-1' onClick={onCancel}><RiCloseLine className='h-4 w-4 text-text-tertiary' /></div>
|
||||
</div>
|
||||
<div className='mb-8 flex gap-2'>
|
||||
<div className='mt-1.5 h-8 w-8 shrink-0 rounded-lg border-components-panel-border bg-util-colors-orange-dark-orange-dark-500 p-1.5'>
|
||||
<RiAsterisk className='h-5 w-5 text-text-primary-on-surface' />
|
||||
</div>
|
||||
<div className='grow rounded-2xl border-t border-divider-subtle bg-chat-bubble-bg p-3 shadow-xs'>
|
||||
<textarea
|
||||
<PromptEditor
|
||||
value={tempValue}
|
||||
rows={3}
|
||||
onChange={e => setTempValue(e.target.value)}
|
||||
className="system-md-regular w-full border-0 bg-transparent px-0 text-text-secondary focus:outline-none"
|
||||
onChange={setTempValue}
|
||||
placeholder={t('appDebug.openingStatement.placeholder') as string}
|
||||
variableBlock={{
|
||||
show: true,
|
||||
variables: [
|
||||
// Prompt variables
|
||||
...promptVariables.map(item => ({
|
||||
name: item.name || item.key,
|
||||
value: item.key,
|
||||
})),
|
||||
// Workflow variables
|
||||
...workflowVariables.map(item => ({
|
||||
name: item.variable,
|
||||
value: item.variable,
|
||||
})),
|
||||
],
|
||||
}}
|
||||
/>
|
||||
{renderQuestions()}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ const HitTestingPage: FC<Props> = ({ datasetId }: Props) => {
|
|||
<>
|
||||
<div className='grow overflow-y-auto'>
|
||||
<table className={'w-full border-collapse border-0 text-[13px] leading-4 text-text-secondary '}>
|
||||
<thead className='sticky top-0 h-7 text-xs font-medium uppercase leading-7 text-text-tertiary'>
|
||||
<thead className='sticky top-0 h-7 text-xs font-medium uppercase leading-7 text-text-tertiary backdrop-blur-[5px]'>
|
||||
<tr>
|
||||
<td className='w-[128px] rounded-l-lg bg-background-section-burn pl-3'>{t('datasetHitTesting.table.header.source')}</td>
|
||||
<td className='bg-background-section-burn'>{t('datasetHitTesting.table.header.text')}</td>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import type { GithubRepo } from '@/models/common'
|
|||
import { RiLoader2Line } from '@remixicon/react'
|
||||
|
||||
const defaultData = {
|
||||
stargazers_count: 98570,
|
||||
stargazers_count: 110918,
|
||||
}
|
||||
|
||||
const getStar = async () => {
|
||||
|
|
|
|||
|
|
@ -79,6 +79,9 @@ const SwrInitializer = ({
|
|||
<SWRConfig value={{
|
||||
shouldRetryOnError: false,
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 60000,
|
||||
focusThrottleInterval: 5000,
|
||||
provider: () => new Map(),
|
||||
}}>
|
||||
{children}
|
||||
</SWRConfig>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
import { create } from 'zustand'
|
||||
import type { Label } from './constant'
|
||||
|
||||
type State = {
|
||||
labelList: Label[]
|
||||
}
|
||||
|
||||
type Action = {
|
||||
setLabelList: (labelList?: Label[]) => void
|
||||
}
|
||||
|
||||
export const useStore = create<State & Action>(set => ({
|
||||
labelList: [],
|
||||
setLabelList: labelList => set(() => ({ labelList })),
|
||||
}))
|
||||
|
|
@ -246,11 +246,11 @@ export const useWorkflow = () => {
|
|||
|
||||
const handleOutVarRenameChange = useCallback((nodeId: string, oldValeSelector: ValueSelector, newVarSelector: ValueSelector) => {
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const afterNodes = getAfterNodesInSameBranch(nodeId)
|
||||
const effectNodes = findUsedVarNodes(oldValeSelector, afterNodes)
|
||||
if (effectNodes.length > 0) {
|
||||
const newNodes = getNodes().map((node) => {
|
||||
if (effectNodes.find(n => n.id === node.id))
|
||||
const allNodes = getNodes()
|
||||
const affectedNodes = findUsedVarNodes(oldValeSelector, allNodes)
|
||||
if (affectedNodes.length > 0) {
|
||||
const newNodes = allNodes.map((node) => {
|
||||
if (affectedNodes.find(n => n.id === node.id))
|
||||
return updateNodeVars(node, oldValeSelector, newVarSelector)
|
||||
|
||||
return node
|
||||
|
|
|
|||
|
|
@ -1101,7 +1101,15 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
|
|||
res = (data as IfElseNodeType).conditions?.map((c) => {
|
||||
return c.variable_selector || []
|
||||
}) || []
|
||||
res.push(...((data as IfElseNodeType).cases || []).flatMap(c => (c.conditions || [])).map(c => c.variable_selector || []))
|
||||
res.push(...((data as IfElseNodeType).cases || []).flatMap(c => (c.conditions || [])).flatMap((c) => {
|
||||
const selectors: ValueSelector[] = []
|
||||
if (c.variable_selector)
|
||||
selectors.push(c.variable_selector)
|
||||
// Handle sub-variable conditions
|
||||
if (c.sub_variable_condition && c.sub_variable_condition.conditions)
|
||||
selectors.push(...c.sub_variable_condition.conditions.map(subC => subC.variable_selector || []).filter(sel => sel.length > 0))
|
||||
return selectors
|
||||
}))
|
||||
break
|
||||
}
|
||||
case BlockEnum.Code: {
|
||||
|
|
@ -1345,6 +1353,26 @@ export const updateNodeVars = (oldNode: Node, oldVarSelector: ValueSelector, new
|
|||
return c
|
||||
})
|
||||
}
|
||||
if (payload.cases) {
|
||||
payload.cases = payload.cases.map((caseItem) => {
|
||||
if (caseItem.conditions) {
|
||||
caseItem.conditions = caseItem.conditions.map((c) => {
|
||||
if (c.variable_selector?.join('.') === oldVarSelector.join('.'))
|
||||
c.variable_selector = newVarSelector
|
||||
// Handle sub-variable conditions
|
||||
if (c.sub_variable_condition && c.sub_variable_condition.conditions) {
|
||||
c.sub_variable_condition.conditions = c.sub_variable_condition.conditions.map((subC) => {
|
||||
if (subC.variable_selector?.join('.') === oldVarSelector.join('.'))
|
||||
subC.variable_selector = newVarSelector
|
||||
return subC
|
||||
})
|
||||
}
|
||||
return c
|
||||
})
|
||||
}
|
||||
return caseItem
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case BlockEnum.Code: {
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ const DebugAndPreview = () => {
|
|||
<div
|
||||
ref={containerRef}
|
||||
className={cn(
|
||||
'relative flex h-full flex-col rounded-l-2xl border border-r-0 border-components-panel-border bg-components-panel-bg shadow-xl',
|
||||
'relative flex h-full flex-col rounded-l-2xl border border-r-0 border-components-panel-border bg-chatbot-bg shadow-xl',
|
||||
)}
|
||||
style={{ width: `${panelWidth}px` }}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export type AppContextProviderProps = {
|
|||
}
|
||||
|
||||
export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => {
|
||||
const { data: userProfileResponse, mutate: mutateUserProfile } = useSWR({ url: '/account/profile', params: {} }, fetchUserProfile)
|
||||
const { data: userProfileResponse, mutate: mutateUserProfile, error: userProfileError } = useSWR({ url: '/account/profile', params: {} }, fetchUserProfile)
|
||||
const { data: currentWorkspaceResponse, mutate: mutateCurrentWorkspace, isLoading: isLoadingCurrentWorkspace } = useSWR({ url: '/workspaces/current', params: {} }, fetchCurrentWorkspace)
|
||||
|
||||
const [userProfile, setUserProfile] = useState<UserProfileResponse>(userProfilePlaceholder)
|
||||
|
|
@ -86,15 +86,26 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
|
|||
const isCurrentWorkspaceEditor = useMemo(() => ['owner', 'admin', 'editor'].includes(currentWorkspace.role), [currentWorkspace.role])
|
||||
const isCurrentWorkspaceDatasetOperator = useMemo(() => currentWorkspace.role === 'dataset_operator', [currentWorkspace.role])
|
||||
const updateUserProfileAndVersion = useCallback(async () => {
|
||||
if (userProfileResponse && !userProfileResponse.bodyUsed) {
|
||||
const result = await userProfileResponse.json()
|
||||
setUserProfile(result)
|
||||
const current_version = userProfileResponse.headers.get('x-version')
|
||||
const current_env = process.env.NODE_ENV === 'development' ? 'DEVELOPMENT' : userProfileResponse.headers.get('x-env')
|
||||
const versionData = await fetchLangGeniusVersion({ url: '/version', params: { current_version } })
|
||||
setLangGeniusVersionInfo({ ...versionData, current_version, latest_version: versionData.version, current_env })
|
||||
if (userProfileResponse) {
|
||||
try {
|
||||
const clonedResponse = (userProfileResponse as Response).clone()
|
||||
const result = await clonedResponse.json()
|
||||
setUserProfile(result)
|
||||
const current_version = userProfileResponse.headers.get('x-version')
|
||||
const current_env = process.env.NODE_ENV === 'development' ? 'DEVELOPMENT' : userProfileResponse.headers.get('x-env')
|
||||
const versionData = await fetchLangGeniusVersion({ url: '/version', params: { current_version } })
|
||||
setLangGeniusVersionInfo({ ...versionData, current_version, latest_version: versionData.version, current_env })
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to update user profile:', error)
|
||||
if (userProfile.id === '')
|
||||
setUserProfile(userProfilePlaceholder)
|
||||
}
|
||||
}
|
||||
}, [userProfileResponse])
|
||||
else if (userProfileError && userProfile.id === '') {
|
||||
setUserProfile(userProfilePlaceholder)
|
||||
}
|
||||
}, [userProfileResponse, userProfileError, userProfile.id])
|
||||
|
||||
useEffect(() => {
|
||||
updateUserProfileAndVersion()
|
||||
|
|
|
|||
|
|
@ -23,19 +23,14 @@ const translation = {
|
|||
contractSales: 'تماس با فروش',
|
||||
contractOwner: 'تماس با مدیر تیم',
|
||||
startForFree: 'رایگان شروع کنید',
|
||||
getStartedWith: 'شروع کنید با ',
|
||||
contactSales: 'تماس با فروش',
|
||||
talkToSales: 'صحبت با فروش',
|
||||
modelProviders: 'ارائهدهندگان مدل',
|
||||
teamMembers: 'اعضای تیم',
|
||||
annotationQuota: 'سهمیه حاشیهنویسی',
|
||||
buildApps: 'ساخت اپلیکیشنها',
|
||||
vectorSpace: 'فضای وکتور',
|
||||
vectorSpaceBillingTooltip: 'هر 1 مگابایت میتواند حدود 1.2 میلیون کاراکتر از دادههای وکتور شده را ذخیره کند (براساس تخمین با استفاده از OpenAI Embeddings، متفاوت بر اساس مدلها).',
|
||||
vectorSpaceTooltip: 'فضای وکتور سیستم حافظه بلند مدت است که برای درک دادههای شما توسط LLMها مورد نیاز است.',
|
||||
documentsUploadQuota: 'سهمیه بارگذاری مستندات',
|
||||
documentProcessingPriority: 'اولویت پردازش مستندات',
|
||||
documentProcessingPriorityTip: 'برای اولویت پردازش بالاتر مستندات، لطفاً طرح خود را ارتقاء دهید.',
|
||||
documentProcessingPriorityUpgrade: 'دادههای بیشتری را با دقت بالاتر و سرعت بیشتر پردازش کنید.',
|
||||
priority: {
|
||||
'standard': 'استاندارد',
|
||||
|
|
@ -103,19 +98,16 @@ const translation = {
|
|||
sandbox: {
|
||||
name: 'محیط آزمایشی',
|
||||
description: '200 بار آزمایش رایگان GPT',
|
||||
includesTitle: 'شامل:',
|
||||
for: 'دوره آزمایشی رایگان قابلیتهای اصلی',
|
||||
},
|
||||
professional: {
|
||||
name: 'حرفهای',
|
||||
description: 'برای افراد و تیمهای کوچک برای باز کردن قدرت بیشتر به طور مقرون به صرفه.',
|
||||
includesTitle: 'همه چیز در طرح رایگان، به علاوه:',
|
||||
for: 'برای توسعهدهندگان مستقل/تیمهای کوچک',
|
||||
},
|
||||
team: {
|
||||
name: 'تیم',
|
||||
description: 'همکاری بدون محدودیت و لذت بردن از عملکرد برتر.',
|
||||
includesTitle: 'همه چیز در طرح حرفهای، به علاوه:',
|
||||
for: 'برای تیمهای متوسط',
|
||||
},
|
||||
enterprise: {
|
||||
|
|
@ -123,15 +115,15 @@ const translation = {
|
|||
description: 'دریافت کاملترین قابلیتها و پشتیبانی برای سیستمهای بزرگ و بحرانی.',
|
||||
includesTitle: 'همه چیز در طرح تیم، به علاوه:',
|
||||
features: {
|
||||
0: 'راهکارهای استقرار مقیاسپذیر در سطح سازمانی',
|
||||
8: 'پشتیبانی فنی حرفهای',
|
||||
3: 'چندین فضای کاری و مدیریت سازمانی',
|
||||
5: 'SLA های توافق شده توسط شرکای Dify',
|
||||
4: 'SSO',
|
||||
2: 'ویژگیهای انحصاری سازمانی',
|
||||
1: 'مجوز صدور مجوز تجاری',
|
||||
6: 'امنیت و کنترلهای پیشرفته',
|
||||
7: 'بهروزرسانیها و نگهداری توسط دیفی بهطور رسمی',
|
||||
4: 'Sso',
|
||||
1: 'مجوز جواز تجاری',
|
||||
2: 'ویژگی های انحصاری سازمانی',
|
||||
8: 'پشتیبانی فنی حرفه ای',
|
||||
5: 'SLA های مذاکره شده توسط Dify Partners',
|
||||
6: 'امنیت و کنترل پیشرفته',
|
||||
3: 'فضاهای کاری چندگانه و مدیریت سازمانی',
|
||||
7: 'به روز رسانی و نگهداری توسط Dify به طور رسمی',
|
||||
0: 'راه حل های استقرار مقیاس پذیر در سطح سازمانی',
|
||||
},
|
||||
price: 'سفارشی',
|
||||
btnText: 'تماس با فروش',
|
||||
|
|
@ -140,9 +132,9 @@ const translation = {
|
|||
},
|
||||
community: {
|
||||
features: {
|
||||
0: 'تمام ویژگیهای اصلی منتشر شده در مخزن عمومی',
|
||||
2: 'با رعایت مجوز منبع باز دیفی',
|
||||
1: 'فضای کاری واحد',
|
||||
2: 'با مجوز منبع باز Dify مطابقت دارد',
|
||||
0: 'تمام ویژگی های اصلی در مخزن عمومی منتشر شده است',
|
||||
},
|
||||
btnText: 'شروع کنید با جامعه',
|
||||
price: 'رایگان',
|
||||
|
|
@ -153,10 +145,10 @@ const translation = {
|
|||
},
|
||||
premium: {
|
||||
features: {
|
||||
1: 'محل کار واحد',
|
||||
0: 'قابل اطمینان خودمدیریتی توسط ارائهدهندگان مختلف ابر',
|
||||
2: 'شعار و سفارشیسازی برند وباپلیکیشن',
|
||||
3: 'پشتیبانی اولویت ایمیل و چت',
|
||||
1: 'فضای کاری واحد',
|
||||
3: 'پشتیبانی از ایمیل و چت اولویت دار',
|
||||
2: 'لوگوی وب اپلیکیشن و سفارشی سازی برندینگ',
|
||||
0: 'قابلیت اطمینان خود مدیریت شده توسط ارائه دهندگان مختلف ابر',
|
||||
},
|
||||
btnText: 'گرفتن نسخه پریمیوم در',
|
||||
description: 'برای سازمانها و تیمهای میانرده',
|
||||
|
|
@ -173,8 +165,6 @@ const translation = {
|
|||
fullSolution: 'طرح خود را ارتقاء دهید تا فضای بیشتری دریافت کنید.',
|
||||
},
|
||||
apps: {
|
||||
fullTipLine1: 'طرح خود را ارتقاء دهید تا',
|
||||
fullTipLine2: 'اپلیکیشنهای بیشتری بسازید.',
|
||||
fullTip2: 'محدودیت طرح به پایان رسید',
|
||||
contactUs: 'با ما تماس بگیرید',
|
||||
fullTip1: 'بهروزرسانی کنید تا برنامههای بیشتری ایجاد کنید',
|
||||
|
|
|
|||
|
|
@ -23,18 +23,13 @@ const translation = {
|
|||
contractSales: 'Contactez les ventes',
|
||||
contractOwner: 'Contacter le chef d\'équipe',
|
||||
startForFree: 'Commencez gratuitement',
|
||||
getStartedWith: 'Commencez avec',
|
||||
contactSales: 'Contacter les ventes',
|
||||
talkToSales: 'Parlez aux Ventes',
|
||||
modelProviders: 'Fournisseurs de Modèles',
|
||||
teamMembers: 'Membres de l\'équipe',
|
||||
buildApps: 'Construire des Applications',
|
||||
vectorSpace: 'Espace Vectoriel',
|
||||
vectorSpaceBillingTooltip: 'Chaque 1MB peut stocker environ 1,2 million de caractères de données vectorisées (estimé en utilisant les embeddings OpenAI, varie selon les modèles).',
|
||||
vectorSpaceTooltip: 'L\'espace vectoriel est le système de mémoire à long terme nécessaire pour que les LLMs comprennent vos données.',
|
||||
documentsUploadQuota: 'Quota de téléchargement de documents',
|
||||
documentProcessingPriority: 'Priorité de Traitement de Document',
|
||||
documentProcessingPriorityTip: 'Pour une priorité de traitement de documents plus élevée, veuillez mettre à niveau votre plan.',
|
||||
documentProcessingPriorityUpgrade: 'Traitez plus de données avec une précision plus élevée à des vitesses plus rapides.',
|
||||
priority: {
|
||||
'standard': 'Standard',
|
||||
|
|
@ -103,19 +98,16 @@ const translation = {
|
|||
sandbox: {
|
||||
name: 'Bac à sable',
|
||||
description: '200 essais gratuits de GPT',
|
||||
includesTitle: 'Inclus :',
|
||||
for: 'Essai gratuit des fonctionnalités principales',
|
||||
},
|
||||
professional: {
|
||||
name: 'Professionnel',
|
||||
description: 'Pour les individus et les petites équipes afin de débloquer plus de puissance à un prix abordable.',
|
||||
includesTitle: 'Tout ce qui est dans le plan gratuit, plus :',
|
||||
for: 'Pour les développeurs indépendants / petites équipes',
|
||||
},
|
||||
team: {
|
||||
name: 'Équipe',
|
||||
description: 'Collaborez sans limites et profitez d\'une performance de premier ordre.',
|
||||
includesTitle: 'Tout ce qui est inclus dans le plan Professionnel, plus :',
|
||||
for: 'Pour les équipes de taille moyenne',
|
||||
},
|
||||
enterprise: {
|
||||
|
|
@ -123,14 +115,14 @@ const translation = {
|
|||
description: 'Obtenez toutes les capacités et le support pour les systèmes à grande échelle et critiques pour la mission.',
|
||||
includesTitle: 'Tout ce qui est inclus dans le plan Équipe, plus :',
|
||||
features: {
|
||||
5: 'SLA négociés par Dify Partners',
|
||||
1: 'Autorisation de Licence Commerciale',
|
||||
2: 'Fonctionnalités exclusives pour les entreprises',
|
||||
4: 'SSO',
|
||||
8: 'Support Technique Professionnel',
|
||||
3: 'Gestion de plusieurs espaces de travail et d\'entreprise',
|
||||
6: 'Sécurité et contrôles avancés',
|
||||
7: 'Mises à jour et maintenance par Dify Officiellement',
|
||||
3: 'Espaces de travail multiples et gestion d’entreprise',
|
||||
4: 'SSO',
|
||||
1: 'Autorisation de licence commerciale',
|
||||
2: 'Fonctionnalités exclusives à l’entreprise',
|
||||
5: 'SLA négociés par les partenaires Dify',
|
||||
8: 'Assistance technique professionnelle',
|
||||
7: 'Mises à jour et maintenance par Dify officiellement',
|
||||
0: 'Solutions de déploiement évolutives de niveau entreprise',
|
||||
},
|
||||
for: 'Pour les équipes de grande taille',
|
||||
|
|
@ -140,9 +132,9 @@ const translation = {
|
|||
},
|
||||
community: {
|
||||
features: {
|
||||
2: 'Conforme à la licence open source de Dify',
|
||||
1: 'Espace de travail unique',
|
||||
0: 'Toutes les fonctionnalités principales publiées dans le référentiel public',
|
||||
0: 'Toutes les fonctionnalités de base publiées dans le dépôt public',
|
||||
2: 'Conforme à la licence Open Source Dify',
|
||||
},
|
||||
name: 'Communauté',
|
||||
btnText: 'Commencez avec la communauté',
|
||||
|
|
@ -153,10 +145,10 @@ const translation = {
|
|||
},
|
||||
premium: {
|
||||
features: {
|
||||
3: 'Support par e-mail et chat prioritaire',
|
||||
2: 'Personnalisation du logo et de l’image de marque WebApp',
|
||||
1: 'Espace de travail unique',
|
||||
0: 'Fiabilité autogérée par divers fournisseurs de cloud',
|
||||
2: 'Personnalisation du logo et de la marque de l\'application Web',
|
||||
3: 'Assistance prioritaire par e-mail et chat',
|
||||
0: 'Fiabilité autogérée par différents fournisseurs de cloud',
|
||||
},
|
||||
for: 'Pour les organisations et les équipes de taille moyenne',
|
||||
includesTitle: 'Tout de la communauté, en plus :',
|
||||
|
|
@ -173,8 +165,6 @@ const translation = {
|
|||
fullSolution: 'Mettez à niveau votre plan pour obtenir plus d\'espace.',
|
||||
},
|
||||
apps: {
|
||||
fullTipLine1: 'Mettez à jour votre plan pour',
|
||||
fullTipLine2: 'construire plus d\'applications.',
|
||||
fullTip2: 'Limite de plan atteinte',
|
||||
contactUs: 'Contactez-nous',
|
||||
fullTip1: 'Mettez à niveau pour créer plus d\'applications',
|
||||
|
|
|
|||
|
|
@ -24,22 +24,15 @@ const translation = {
|
|||
contractSales: 'बिक्री से संपर्क करें',
|
||||
contractOwner: 'टीम प्रबंधक से संपर्क करें',
|
||||
startForFree: 'मुफ्त में शुरू करें',
|
||||
getStartedWith: 'इसके साथ शुरू करें ',
|
||||
contactSales: 'बिक्री से संपर्क करें',
|
||||
talkToSales: 'बिक्री से बात करें',
|
||||
modelProviders: 'मॉडल प्रदाता',
|
||||
teamMembers: 'टीम के सदस्य',
|
||||
annotationQuota: 'एनोटेशन कोटा',
|
||||
buildApps: 'ऐप्स बनाएं',
|
||||
vectorSpace: 'वेक्टर स्पेस',
|
||||
vectorSpaceBillingTooltip:
|
||||
'प्रत्येक 1MB लगभग 1.2 मिलियन वर्णों के वेक्टराइज्ड डेटा को संग्रहीत कर सकता है (OpenAI एम्बेडिंग का उपयोग करके अनुमानित, मॉडल में भिन्नता होती है)।',
|
||||
vectorSpaceTooltip:
|
||||
'वेक्टर स्पेस वह दीर्घकालिक स्मृति प्रणाली है जिसकी आवश्यकता LLMs को आपके डेटा को समझने के लिए होती है।',
|
||||
documentsUploadQuota: 'दस्तावेज़ अपलोड कोटा',
|
||||
documentProcessingPriority: 'दस्तावेज़ प्रसंस्करण प्राथमिकता',
|
||||
documentProcessingPriorityTip:
|
||||
'उच्च दस्तावेज़ प्रसंस्करण प्राथमिकता के लिए, कृपया अपनी योजना अपग्रेड करें।',
|
||||
documentProcessingPriorityUpgrade:
|
||||
'तेजी से गति पर उच्च सटीकता के साथ अधिक डेटा संसाधित करें।',
|
||||
priority: {
|
||||
|
|
@ -113,21 +106,18 @@ const translation = {
|
|||
sandbox: {
|
||||
name: 'सैंडबॉक्स',
|
||||
description: '200 बार GPT मुफ्त ट्रायल',
|
||||
includesTitle: 'शामिल हैं:',
|
||||
for: 'कोर क्षमताओं का मुफ्त परीक्षण',
|
||||
},
|
||||
professional: {
|
||||
name: 'प्रोफेशनल',
|
||||
description:
|
||||
'व्यक्तियों और छोटे टीमों के लिए अधिक शक्ति सस्ती दर पर खोलें।',
|
||||
includesTitle: 'मुफ्त योजना में सब कुछ, साथ में:',
|
||||
for: 'स्वतंत्र डेवलपर्स/छोटे टीमों के लिए',
|
||||
},
|
||||
team: {
|
||||
name: 'टीम',
|
||||
description:
|
||||
'बिना सीमा के सहयोग करें और शीर्ष स्तरीय प्रदर्शन का आनंद लें।',
|
||||
includesTitle: 'प्रोफेशनल योजना में सब कुछ, साथ में:',
|
||||
for: 'मध्यम आकार की टीमों के लिए',
|
||||
},
|
||||
enterprise: {
|
||||
|
|
@ -136,15 +126,15 @@ const translation = {
|
|||
'बड़े पैमाने पर मिशन-क्रिटिकल सिस्टम के लिए पूर्ण क्षमताएं और समर्थन प्राप्त करें।',
|
||||
includesTitle: 'टीम योजना में सब कुछ, साथ में:',
|
||||
features: {
|
||||
0: 'उद्योग स्तर के बड़े पैमाने पर वितरण समाधान',
|
||||
3: 'अनेक कार्यक्षेत्र और उद्यम प्रबंधक',
|
||||
8: 'प्रोफेशनल तकनीकी समर्थन',
|
||||
6: 'उन्नत सुरक्षा और नियंत्रण',
|
||||
2: 'विशेष उद्यम सुविधाएँ',
|
||||
1: 'Commercial License Authorization',
|
||||
4: 'SSO',
|
||||
6: 'उन्नत सुरक्षा और नियंत्रण',
|
||||
2: 'विशेष उद्यम सुविधाएँ',
|
||||
3: 'अनेक कार्यक्षेत्र और उद्यम प्रबंधक',
|
||||
5: 'डिफाई पार्टनर्स द्वारा बातचीत किए गए एसएलए',
|
||||
8: 'प्रोफेशनल तकनीकी समर्थन',
|
||||
7: 'डीफाई द्वारा आधिकारिक रूप से अपडेट और रखरखाव',
|
||||
0: 'उद्योग स्तर के बड़े पैमाने पर वितरण समाधान',
|
||||
},
|
||||
price: 'कस्टम',
|
||||
btnText: 'बिक्री से संपर्क करें',
|
||||
|
|
@ -153,9 +143,9 @@ const translation = {
|
|||
},
|
||||
community: {
|
||||
features: {
|
||||
1: 'एकल कार्यक्षेत्र',
|
||||
2: 'डिफी ओपन सोर्स लाइसेंस के अनुपालन में',
|
||||
0: 'सभी मुख्य सुविधाएं सार्वजनिक संग्रह के तहत जारी की गई हैं।',
|
||||
1: 'एकल कार्यक्षेत्र',
|
||||
},
|
||||
description: 'व्यक्तिगत उपयोगकर्ताओं, छोटे टीमों, या गैर-व्यावसायिक परियोजनाओं के लिए',
|
||||
for: 'व्यक्तिगत उपयोगकर्ताओं, छोटे टीमों, या गैर-व्यावसायिक परियोजनाओं के लिए',
|
||||
|
|
@ -166,9 +156,9 @@ const translation = {
|
|||
},
|
||||
premium: {
|
||||
features: {
|
||||
1: 'एकल कार्यक्षेत्र',
|
||||
2: 'वेब ऐप लोगो और ब्रांडिंग कस्टमाइजेशन',
|
||||
3: 'प्राथमिकता ईमेल और चैट समर्थन',
|
||||
1: 'एकल कार्यक्षेत्र',
|
||||
0: 'विभिन्न क्लाउड प्रदाताओं द्वारा आत्म-प्रबंधित विश्वसनीयता',
|
||||
},
|
||||
priceTip: 'क्लाउड मार्केटप्लेस के आधार पर',
|
||||
|
|
@ -186,8 +176,6 @@ const translation = {
|
|||
fullSolution: 'अधिक स्थान प्राप्त करने के लिए अपनी योजना अपग्रेड करें।',
|
||||
},
|
||||
apps: {
|
||||
fullTipLine1: 'अधिक ऐप्स बनाने के लिए',
|
||||
fullTipLine2: 'अपनी योजना अपग्रेड करें।',
|
||||
fullTip1: 'अधिक ऐप्स बनाने के लिए अपग्रेड करें',
|
||||
fullTip2: 'योजना की सीमा पहुँच गई',
|
||||
contactUs: 'हमसे संपर्क करें',
|
||||
|
|
|
|||
|
|
@ -24,22 +24,15 @@ const translation = {
|
|||
contractSales: 'Contatta vendite',
|
||||
contractOwner: 'Contatta il responsabile del team',
|
||||
startForFree: 'Inizia gratis',
|
||||
getStartedWith: 'Inizia con ',
|
||||
contactSales: 'Contatta le vendite',
|
||||
talkToSales: 'Parla con le vendite',
|
||||
modelProviders: 'Fornitori di Modelli',
|
||||
teamMembers: 'Membri del Team',
|
||||
annotationQuota: 'Quota di Annotazione',
|
||||
buildApps: 'Crea App',
|
||||
vectorSpace: 'Spazio Vettoriale',
|
||||
vectorSpaceBillingTooltip:
|
||||
'Ogni 1MB può memorizzare circa 1,2 milioni di caratteri di dati vettoriali (stimato utilizzando OpenAI Embeddings, varia tra i modelli).',
|
||||
vectorSpaceTooltip:
|
||||
'Lo Spazio Vettoriale è il sistema di memoria a lungo termine necessario per permettere agli LLM di comprendere i tuoi dati.',
|
||||
documentsUploadQuota: 'Quota di Caricamento Documenti',
|
||||
documentProcessingPriority: 'Priorità di Elaborazione Documenti',
|
||||
documentProcessingPriorityTip:
|
||||
'Per una maggiore priorità di elaborazione dei documenti, aggiorna il tuo piano.',
|
||||
documentProcessingPriorityUpgrade:
|
||||
'Elabora più dati con maggiore precisione a velocità più elevate.',
|
||||
priority: {
|
||||
|
|
@ -113,21 +106,18 @@ const translation = {
|
|||
sandbox: {
|
||||
name: 'Sandbox',
|
||||
description: '200 prove gratuite di GPT',
|
||||
includesTitle: 'Include:',
|
||||
for: 'Prova gratuita delle capacità principali',
|
||||
},
|
||||
professional: {
|
||||
name: 'Professional',
|
||||
description:
|
||||
'Per individui e piccoli team per sbloccare più potenza a prezzi accessibili.',
|
||||
includesTitle: 'Tutto nel piano gratuito, più:',
|
||||
for: 'Per sviluppatori indipendenti / piccoli team',
|
||||
},
|
||||
team: {
|
||||
name: 'Team',
|
||||
description:
|
||||
'Collabora senza limiti e goditi prestazioni di alto livello.',
|
||||
includesTitle: 'Tutto nel piano Professional, più:',
|
||||
for: 'Per team di medie dimensioni',
|
||||
},
|
||||
enterprise: {
|
||||
|
|
@ -136,15 +126,15 @@ const translation = {
|
|||
'Ottieni tutte le capacità e il supporto per sistemi mission-critical su larga scala.',
|
||||
includesTitle: 'Tutto nel piano Team, più:',
|
||||
features: {
|
||||
6: 'Sicurezza e Controlli Avanzati',
|
||||
2: 'Funzionalità esclusive per le imprese',
|
||||
3: 'Spazi di lavoro multipli e gestione aziendale',
|
||||
2: 'Funzionalità esclusive per le aziende',
|
||||
1: 'Autorizzazione Licenza Commerciale',
|
||||
5: 'SLA negoziati dai partner Dify',
|
||||
4: 'SSO',
|
||||
8: 'Supporto Tecnico Professionale',
|
||||
5: 'SLA negoziati da Dify Partners',
|
||||
0: 'Soluzioni di distribuzione scalabili di livello enterprise',
|
||||
7: 'Aggiornamenti e manutenzione di Dify ufficialmente',
|
||||
1: 'Autorizzazione alla Licenza Commerciale',
|
||||
3: 'Gestione di più spazi di lavoro e imprese',
|
||||
6: 'Sicurezza e controlli avanzati',
|
||||
8: 'Supporto tecnico professionale',
|
||||
7: 'Aggiornamenti e manutenzione da parte di Dify ufficialmente',
|
||||
0: 'Soluzioni di distribuzione scalabili di livello aziendale',
|
||||
},
|
||||
price: 'Personalizzato',
|
||||
for: 'Per team di grandi dimensioni',
|
||||
|
|
@ -153,9 +143,9 @@ const translation = {
|
|||
},
|
||||
community: {
|
||||
features: {
|
||||
1: 'Spazio di Lavoro Unico',
|
||||
2: 'Rispetta la Licenza Open Source di Dify',
|
||||
0: 'Tutte le funzionalità principali rilasciate sotto il repository pubblico',
|
||||
1: 'Area di lavoro singola',
|
||||
2: 'Conforme alla licenza Open Source Dify',
|
||||
0: 'Tutte le funzionalità principali rilasciate nel repository pubblico',
|
||||
},
|
||||
name: 'Comunità',
|
||||
btnText: 'Inizia con la comunità',
|
||||
|
|
@ -166,10 +156,10 @@ const translation = {
|
|||
},
|
||||
premium: {
|
||||
features: {
|
||||
0: 'Affidabilità autogestita da vari fornitori di cloud',
|
||||
3: 'Supporto prioritario via Email e Chat',
|
||||
2: 'Personalizzazione del logo e del marchio dell\'app web',
|
||||
1: 'Spazio di Lavoro Unico',
|
||||
3: 'Supporto prioritario via e-mail e chat',
|
||||
1: 'Area di lavoro singola',
|
||||
2: 'Personalizzazione del logo e del marchio WebApp',
|
||||
0: 'Affidabilità autogestita da vari fornitori di servizi cloud',
|
||||
},
|
||||
name: 'Premium',
|
||||
priceTip: 'Basato su Cloud Marketplace',
|
||||
|
|
@ -186,8 +176,6 @@ const translation = {
|
|||
fullSolution: 'Aggiorna il tuo piano per ottenere più spazio.',
|
||||
},
|
||||
apps: {
|
||||
fullTipLine1: 'Aggiorna il tuo piano per',
|
||||
fullTipLine2: 'creare più app.',
|
||||
fullTip1des: 'Hai raggiunto il limite di costruzione delle app su questo piano.',
|
||||
fullTip2des: 'Si consiglia di disinstallare le applicazioni inattive per liberare spazio, o contattarci.',
|
||||
contactUs: 'Contattaci',
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const translation = {
|
|||
bulkImport: '一括インポート',
|
||||
bulkExport: '一括エクスポート',
|
||||
clearAll: 'すべて削除',
|
||||
clearAllConfirm: 'すべての寸法を削除?',
|
||||
clearAllConfirm: 'すべての注釈を削除しますか?',
|
||||
},
|
||||
},
|
||||
editModal: {
|
||||
|
|
|
|||
|
|
@ -565,7 +565,7 @@ const translation = {
|
|||
overview: '監視',
|
||||
promptEng: 'オーケストレート',
|
||||
apiAccess: 'API アクセス',
|
||||
logAndAnn: 'ログ&アナウンス',
|
||||
logAndAnn: 'ログ&注釈',
|
||||
logs: 'ログ',
|
||||
},
|
||||
environment: {
|
||||
|
|
|
|||
|
|
@ -995,6 +995,7 @@ const translation = {
|
|||
noLastRunFound: '以前の実行が見つかりませんでした。',
|
||||
copyLastRunError: '最後の実行の入力をコピーできませんでした',
|
||||
noMatchingInputsFound: '前回の実行から一致する入力が見つかりませんでした。',
|
||||
lastRunInputsCopied: '前回の実行から{{count}}個の入力をコピーしました',
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,21 +24,14 @@ const translation = {
|
|||
contractSales: 'Skontaktuj się z działem sprzedaży',
|
||||
contractOwner: 'Skontaktuj się z zarządcą zespołu',
|
||||
startForFree: 'Zacznij za darmo',
|
||||
getStartedWith: 'Rozpocznij z ',
|
||||
contactSales: 'Kontakt z działem sprzedaży',
|
||||
talkToSales: 'Porozmawiaj z działem sprzedaży',
|
||||
modelProviders: 'Dostawcy modeli',
|
||||
teamMembers: 'Członkowie zespołu',
|
||||
buildApps: 'Twórz aplikacje',
|
||||
vectorSpace: 'Przestrzeń wektorowa',
|
||||
vectorSpaceBillingTooltip:
|
||||
'Każdy 1MB może przechowywać około 1,2 miliona znaków z wektoryzowanych danych (szacowane na podstawie OpenAI Embeddings, różni się w zależności od modelu).',
|
||||
vectorSpaceTooltip:
|
||||
'Przestrzeń wektorowa jest systemem pamięci długoterminowej wymaganym dla LLM, aby zrozumieć Twoje dane.',
|
||||
documentsUploadQuota: 'Limit przesyłanych dokumentów',
|
||||
documentProcessingPriority: 'Priorytet przetwarzania dokumentów',
|
||||
documentProcessingPriorityTip:
|
||||
'Dla wyższego priorytetu przetwarzania dokumentów, ulepsz swój plan.',
|
||||
documentProcessingPriorityUpgrade:
|
||||
'Przetwarzaj więcej danych z większą dokładnością i w szybszym tempie.',
|
||||
priority: {
|
||||
|
|
@ -112,21 +105,18 @@ const translation = {
|
|||
sandbox: {
|
||||
name: 'Sandbox',
|
||||
description: '200 razy darmowa próba GPT',
|
||||
includesTitle: 'Zawiera:',
|
||||
for: 'Darmowy okres próbny podstawowych funkcji',
|
||||
},
|
||||
professional: {
|
||||
name: 'Profesjonalny',
|
||||
description:
|
||||
'Dla osób fizycznych i małych zespołów, aby odblokować więcej mocy w przystępnej cenie.',
|
||||
includesTitle: 'Wszystko w darmowym planie, plus:',
|
||||
for: 'Dla niezależnych deweloperów/małych zespołów',
|
||||
},
|
||||
team: {
|
||||
name: 'Zespół',
|
||||
description:
|
||||
'Współpracuj bez ograniczeń i ciesz się najwyższą wydajnością.',
|
||||
includesTitle: 'Wszystko w planie Profesjonalnym, plus:',
|
||||
for: 'Dla średniej wielkości zespołów',
|
||||
},
|
||||
enterprise: {
|
||||
|
|
@ -135,15 +125,15 @@ const translation = {
|
|||
'Uzyskaj pełne możliwości i wsparcie dla systemów o kluczowym znaczeniu dla misji.',
|
||||
includesTitle: 'Wszystko w planie Zespołowym, plus:',
|
||||
features: {
|
||||
3: 'Wiele przestrzeni roboczych i zarządzanie przedsiębiorstwem',
|
||||
5: 'Wynegocjowane SLA przez Dify Partners',
|
||||
0: 'Rozwiązania do wdrożeń na dużą skalę klasy przedsiębiorstw',
|
||||
8: 'Profesjonalne wsparcie techniczne',
|
||||
2: 'Ekskluzywne funkcje przedsiębiorstwa',
|
||||
6: 'Zaawansowane zabezpieczenia i kontrola',
|
||||
7: 'Aktualizacje i konserwacja przez Dify Oficjalnie',
|
||||
4: 'SSO',
|
||||
2: 'Wyjątkowe funkcje dla przedsiębiorstw',
|
||||
7: 'Aktualizacje i konserwacja przez Dify oficjalnie',
|
||||
4: 'Usługi rejestracji jednokrotnej',
|
||||
1: 'Autoryzacja licencji komercyjnej',
|
||||
0: 'Skalowalne rozwiązania wdrożeniowe klasy korporacyjnej',
|
||||
5: 'Umowy SLA wynegocjowane przez Dify Partners',
|
||||
8: 'Profesjonalne wsparcie techniczne',
|
||||
3: 'Wiele przestrzeni roboczych i zarządzanie przedsiębiorstwem',
|
||||
6: 'Zaawansowane zabezpieczenia i kontrola',
|
||||
},
|
||||
priceTip: 'Tylko roczne fakturowanie',
|
||||
btnText: 'Skontaktuj się z działem sprzedaży',
|
||||
|
|
@ -152,9 +142,9 @@ const translation = {
|
|||
},
|
||||
community: {
|
||||
features: {
|
||||
0: 'Wszystkie funkcje podstawowe wydane w publicznym repozytorium',
|
||||
1: 'Jedno Miejsce Pracy',
|
||||
2: 'Zgodne z licencją Dify Open Source',
|
||||
1: 'Pojedyncza przestrzeń robocza',
|
||||
2: 'Zgodny z licencją Dify Open Source',
|
||||
0: 'Wszystkie podstawowe funkcje udostępnione w repozytorium publicznym',
|
||||
},
|
||||
includesTitle: 'Darmowe funkcje:',
|
||||
name: 'Społeczność',
|
||||
|
|
@ -165,10 +155,10 @@ const translation = {
|
|||
},
|
||||
premium: {
|
||||
features: {
|
||||
0: 'Samozarządzana niezawodność różnych dostawców chmury',
|
||||
1: 'Jedno miejsce pracy',
|
||||
3: 'Priorytetowe wsparcie przez e-mail i czat',
|
||||
2: 'Logo aplikacji internetowej i dostosowanie marki',
|
||||
1: 'Pojedyncza przestrzeń robocza',
|
||||
2: 'Personalizacja logo i brandingu aplikacji internetowej',
|
||||
3: 'Priorytetowa pomoc techniczna przez e-mail i czat',
|
||||
0: 'Niezawodność samodzielnego zarządzania przez różnych dostawców usług w chmurze',
|
||||
},
|
||||
description: 'Dla średnich organizacji i zespołów',
|
||||
for: 'Dla średnich organizacji i zespołów',
|
||||
|
|
@ -185,8 +175,6 @@ const translation = {
|
|||
fullSolution: 'Ulepsz swój plan, aby uzyskać więcej miejsca.',
|
||||
},
|
||||
apps: {
|
||||
fullTipLine1: 'Ulepsz swój plan, aby',
|
||||
fullTipLine2: 'tworzyć więcej aplikacji.',
|
||||
fullTip1des: 'Osiągnąłeś limit tworzenia aplikacji w tym planie.',
|
||||
fullTip1: 'Zaktualizuj, aby stworzyć więcej aplikacji',
|
||||
fullTip2: 'Osiągnięto limit planu',
|
||||
|
|
|
|||
|
|
@ -22,17 +22,13 @@ const translation = {
|
|||
currentPlan: 'Plano Atual',
|
||||
contractOwner: 'Entre em contato com o gerente da equipe',
|
||||
startForFree: 'Comece de graça',
|
||||
getStartedWith: 'Comece com',
|
||||
contactSales: 'Fale com a equipe de Vendas',
|
||||
talkToSales: 'Fale com a equipe de Vendas',
|
||||
modelProviders: 'Fornecedores de Modelos',
|
||||
teamMembers: 'Membros da Equipe',
|
||||
buildApps: 'Construir Aplicações',
|
||||
vectorSpace: 'Espaço Vetorial',
|
||||
vectorSpaceBillingTooltip: 'Cada 1MB pode armazenar cerca de 1,2 milhão de caracteres de dados vetorizados (estimado usando OpenAI Embeddings, varia entre os modelos).',
|
||||
vectorSpaceTooltip: 'O Espaço Vetorial é o sistema de memória de longo prazo necessário para que LLMs compreendam seus dados.',
|
||||
documentProcessingPriority: 'Prioridade no Processamento de Documentos',
|
||||
documentProcessingPriorityTip: 'Para maior prioridade no processamento de documentos, faça o upgrade do seu plano.',
|
||||
documentProcessingPriorityUpgrade: 'Processe mais dados com maior precisão e velocidade.',
|
||||
priority: {
|
||||
'standard': 'Padrão',
|
||||
|
|
@ -53,7 +49,6 @@ const translation = {
|
|||
dedicatedAPISupport: 'Suporte dedicado à API',
|
||||
customIntegration: 'Integração e suporte personalizados',
|
||||
ragAPIRequest: 'Solicitações API RAG',
|
||||
agentModel: 'Modelo de Agente',
|
||||
workflow: 'Fluxo de trabalho',
|
||||
llmLoadingBalancing: 'Balanceamento de carga LLM',
|
||||
bulkUpload: 'Upload em massa de documentos',
|
||||
|
|
@ -75,7 +70,6 @@ const translation = {
|
|||
ragAPIRequestTooltip: 'Refere-se ao número de chamadas de API que invocam apenas as capacidades de processamento da base de conhecimento do Dify.',
|
||||
receiptInfo: 'Somente proprietários e administradores de equipe podem se inscrever e visualizar informações de cobrança',
|
||||
customTools: 'Ferramentas personalizadas',
|
||||
documentsUploadQuota: 'Cota de upload de documentos',
|
||||
annotationQuota: 'Cota de anotação',
|
||||
contractSales: 'Entre em contato com a equipe de vendas',
|
||||
unavailable: 'Indisponível',
|
||||
|
|
@ -104,19 +98,16 @@ const translation = {
|
|||
sandbox: {
|
||||
name: 'Sandbox',
|
||||
description: '200 vezes GPT de teste gratuito',
|
||||
includesTitle: 'Inclui:',
|
||||
for: 'Teste gratuito das capacidades principais',
|
||||
},
|
||||
professional: {
|
||||
name: 'Profissional',
|
||||
description: 'Para indivíduos e pequenas equipes desbloquearem mais poder de forma acessível.',
|
||||
includesTitle: 'Tudo no plano gratuito, além de:',
|
||||
for: 'Para Desenvolvedores Independentes/Pequenas Equipes',
|
||||
},
|
||||
team: {
|
||||
name: 'Equipe',
|
||||
description: 'Colabore sem limites e aproveite o desempenho de primeira linha.',
|
||||
includesTitle: 'Tudo no plano Profissional, além de:',
|
||||
for: 'Para Equipes de Médio Porte',
|
||||
},
|
||||
enterprise: {
|
||||
|
|
@ -124,15 +115,15 @@ const translation = {
|
|||
description: 'Obtenha capacidades completas e suporte para sistemas críticos em larga escala.',
|
||||
includesTitle: 'Tudo no plano Equipe, além de:',
|
||||
features: {
|
||||
6: 'Segurança e Controles Avançados',
|
||||
7: 'Atualizações e Manutenção por Dify Oficialmente',
|
||||
5: 'Acordos de Nível de Serviço negociados pelos Parceiros Dify',
|
||||
1: 'Autorização de Licença Comercial',
|
||||
8: 'Suporte Técnico Profissional',
|
||||
3: 'Vários espaços de trabalho e gerenciamento corporativo',
|
||||
2: 'Recursos exclusivos da empresa',
|
||||
6: 'Segurança e controles avançados',
|
||||
4: 'SSO',
|
||||
2: 'Recursos Exclusivos da Empresa',
|
||||
3: 'Múltiplos Espaços de Trabalho e Gestão Empresarial',
|
||||
0: 'Soluções de Implantação Escaláveis de Nível Empresarial',
|
||||
8: 'Suporte Técnico Profissional',
|
||||
0: 'Soluções de implantação escaláveis de nível empresarial',
|
||||
7: 'Atualizações e manutenção por Dify oficialmente',
|
||||
1: 'Autorização de Licença Comercial',
|
||||
5: 'SLAs negociados pela Dify Partners',
|
||||
},
|
||||
btnText: 'Contate Vendas',
|
||||
priceTip: 'Faturamento Anual Apenas',
|
||||
|
|
@ -141,9 +132,9 @@ const translation = {
|
|||
},
|
||||
community: {
|
||||
features: {
|
||||
1: 'Espaço de Trabalho Único',
|
||||
0: 'Todos os recursos principais lançados sob o repositório público',
|
||||
2: 'Cumpre a Licença de Código Aberto Dify',
|
||||
0: 'Todos os principais recursos lançados no repositório público',
|
||||
2: 'Está em conformidade com a licença de código aberto Dify',
|
||||
1: 'Espaço de trabalho individual',
|
||||
},
|
||||
name: 'Comunidade',
|
||||
description: 'Para Usuários Individuais, Pequenas Equipes ou Projetos Não Comerciais',
|
||||
|
|
@ -154,10 +145,10 @@ const translation = {
|
|||
},
|
||||
premium: {
|
||||
features: {
|
||||
1: 'Espaço de Trabalho Único',
|
||||
3: 'Suporte prioritário por e-mail e chat',
|
||||
2: 'Customização de Logo e Branding do WebApp',
|
||||
2: 'Personalização do logotipo e da marca do WebApp',
|
||||
1: 'Espaço de trabalho individual',
|
||||
0: 'Confiabilidade autogerenciada por vários provedores de nuvem',
|
||||
3: 'Suporte prioritário por e-mail e bate-papo',
|
||||
},
|
||||
includesTitle: 'Tudo da Comunidade, além de:',
|
||||
for: 'Para organizações e equipes de médio porte',
|
||||
|
|
@ -174,8 +165,6 @@ const translation = {
|
|||
fullSolution: 'Faça o upgrade do seu plano para obter mais espaço.',
|
||||
},
|
||||
apps: {
|
||||
fullTipLine1: 'Faça o upgrade do seu plano para',
|
||||
fullTipLine2: 'construir mais aplicativos.',
|
||||
fullTip1: 'Atualize para criar mais aplicativos',
|
||||
fullTip2: 'Limite do plano alcançado',
|
||||
fullTip1des: 'Você atingiu o limite de criar aplicativos neste plano.',
|
||||
|
|
|
|||
|
|
@ -23,18 +23,13 @@ const translation = {
|
|||
contractSales: 'Contactați vânzările',
|
||||
contractOwner: 'Contactați managerul echipei',
|
||||
startForFree: 'Începe gratuit',
|
||||
getStartedWith: 'Începe cu ',
|
||||
contactSales: 'Contactați vânzările',
|
||||
talkToSales: 'Vorbiți cu vânzările',
|
||||
modelProviders: 'Furnizori de modele',
|
||||
teamMembers: 'Membri ai echipei',
|
||||
buildApps: 'Construiește aplicații',
|
||||
vectorSpace: 'Spațiu vectorial',
|
||||
vectorSpaceBillingTooltip: 'Fiecare 1MB poate stoca aproximativ 1,2 milioane de caractere de date vectorizate (estimat folosind OpenAI Embeddings, variază în funcție de modele).',
|
||||
vectorSpaceTooltip: 'Spațiul vectorial este sistemul de memorie pe termen lung necesar pentru ca LLM-urile să înțeleagă datele dvs.',
|
||||
documentsUploadQuota: 'Cotă de încărcare a documentelor',
|
||||
documentProcessingPriority: 'Prioritatea procesării documentelor',
|
||||
documentProcessingPriorityTip: 'Pentru o prioritate mai mare a procesării documentelor, vă rugăm să actualizați planul.',
|
||||
documentProcessingPriorityUpgrade: 'Procesați mai multe date cu o acuratețe mai mare și la viteze mai rapide.',
|
||||
priority: {
|
||||
'standard': 'Standard',
|
||||
|
|
@ -103,19 +98,16 @@ const translation = {
|
|||
sandbox: {
|
||||
name: 'Sandbox',
|
||||
description: '200 de încercări gratuite GPT',
|
||||
includesTitle: 'Include:',
|
||||
for: 'Proba gratuită a capacităților de bază',
|
||||
},
|
||||
professional: {
|
||||
name: 'Professional',
|
||||
description: 'Pentru persoane fizice și echipe mici pentru a debloca mai multă putere la un preț accesibil.',
|
||||
includesTitle: 'Tot ce este în planul gratuit, plus:',
|
||||
for: 'Pentru dezvoltatori independenți / echipe mici',
|
||||
},
|
||||
team: {
|
||||
name: 'Echipă',
|
||||
description: 'Colaborați fără limite și bucurați-vă de performanțe de top.',
|
||||
includesTitle: 'Tot ce este în planul Professional, plus:',
|
||||
for: 'Pentru echipe de dimensiuni medii',
|
||||
},
|
||||
enterprise: {
|
||||
|
|
@ -123,15 +115,15 @@ const translation = {
|
|||
description: 'Obțineți capacități și asistență complete pentru sisteme critice la scară largă.',
|
||||
includesTitle: 'Tot ce este în planul Echipă, plus:',
|
||||
features: {
|
||||
3: 'Multiple Spații de lucru și Management Enterprise',
|
||||
6: 'Securitate avansată și control',
|
||||
6: 'Securitate și controale avansate',
|
||||
1: 'Autorizare licență comercială',
|
||||
2: 'Funcții exclusive pentru întreprinderi',
|
||||
0: 'Soluții de implementare scalabile la nivel de întreprindere',
|
||||
5: 'SLA-uri negociate de partenerii Dify',
|
||||
3: 'Mai multe spații de lucru și managementul întreprinderii',
|
||||
7: 'Actualizări și întreținere de către Dify oficial',
|
||||
8: 'Asistență tehnică profesională',
|
||||
4: 'SSO',
|
||||
7: 'Actualizări și întreținere de către Dify Oficial',
|
||||
1: 'Autorizare pentru licență comercială',
|
||||
5: 'SLA-uri negociate de partenerii Dify',
|
||||
0: 'Soluții de desfășurare scalabile de nivel enterprise',
|
||||
},
|
||||
for: 'Pentru echipe de mari dimensiuni',
|
||||
price: 'Personalizat',
|
||||
|
|
@ -140,9 +132,9 @@ const translation = {
|
|||
},
|
||||
community: {
|
||||
features: {
|
||||
2: 'Se conformează Licenței Open Source Dify',
|
||||
0: 'Toate caracteristicile de bază lansate în depozitul public',
|
||||
2: 'Respectă licența Dify Open Source',
|
||||
1: 'Spațiu de lucru unic',
|
||||
0: 'Toate funcțiile de bază lansate sub depozitul public',
|
||||
},
|
||||
description: 'Pentru utilizatori individuali, echipe mici sau proiecte necomerciale',
|
||||
btnText: 'Începe cu Comunitatea',
|
||||
|
|
@ -153,10 +145,10 @@ const translation = {
|
|||
},
|
||||
premium: {
|
||||
features: {
|
||||
3: 'Asistență prioritară prin e-mail și chat',
|
||||
1: 'Spațiu de lucru unic',
|
||||
0: 'Fiabilitate autogestionată de diferiți furnizori de cloud',
|
||||
2: 'Personalizarea logo-ului și branding-ului aplicației web',
|
||||
3: 'Suport prioritar prin email și chat',
|
||||
0: 'Fiabilitate autogestionată de diverși furnizori de cloud',
|
||||
2: 'Personalizarea logo-ului și brandingului WebApp',
|
||||
},
|
||||
btnText: 'Obține Premium în',
|
||||
description: 'Pentru organizații și echipe de dimensiuni medii',
|
||||
|
|
@ -173,8 +165,6 @@ const translation = {
|
|||
fullSolution: 'Actualizați-vă planul pentru a obține mai mult spațiu.',
|
||||
},
|
||||
apps: {
|
||||
fullTipLine1: 'Actualizați-vă planul pentru a',
|
||||
fullTipLine2: 'construi mai multe aplicații.',
|
||||
fullTip2des: 'Se recomandă curățarea aplicațiilor inactive pentru a elibera resurse, sau contactați-ne.',
|
||||
fullTip2: 'Limita planului a fost atinsă',
|
||||
fullTip1des: 'Ați atins limita de aplicații construite pe acest plan',
|
||||
|
|
|
|||
|
|
@ -23,19 +23,14 @@ const translation = {
|
|||
contractSales: 'Связаться с отделом продаж',
|
||||
contractOwner: 'Связаться с руководителем команды',
|
||||
startForFree: 'Начать бесплатно',
|
||||
getStartedWith: 'Начать с ',
|
||||
contactSales: 'Связаться с отделом продаж',
|
||||
talkToSales: 'Поговорить с отделом продаж',
|
||||
modelProviders: 'Поставщики моделей',
|
||||
teamMembers: 'Участники команды',
|
||||
annotationQuota: 'Квота аннотаций',
|
||||
buildApps: 'Создать приложения',
|
||||
vectorSpace: 'Векторное пространство',
|
||||
vectorSpaceBillingTooltip: 'Каждый 1 МБ может хранить около 1,2 миллиона символов векторизованных данных (оценка с использованием Embeddings OpenAI, варьируется в зависимости от модели).',
|
||||
vectorSpaceTooltip: 'Векторное пространство - это система долговременной памяти, необходимая LLM для понимания ваших данных.',
|
||||
documentsUploadQuota: 'Квота загрузки документов',
|
||||
documentProcessingPriority: 'Приоритет обработки документов',
|
||||
documentProcessingPriorityTip: 'Для более высокого приоритета обработки документов, пожалуйста, обновите свой тарифный план.',
|
||||
documentProcessingPriorityUpgrade: 'Обрабатывайте больше данных с большей точностью и на более высоких скоростях.',
|
||||
priority: {
|
||||
'standard': 'Стандартный',
|
||||
|
|
@ -103,19 +98,16 @@ const translation = {
|
|||
sandbox: {
|
||||
name: 'Песочница',
|
||||
description: '200 бесплатных пробных использований GPT',
|
||||
includesTitle: 'Включает:',
|
||||
for: 'Бесплатная пробная версия основных возможностей',
|
||||
},
|
||||
professional: {
|
||||
name: 'Профессиональный',
|
||||
description: 'Для частных лиц и небольших команд, чтобы разблокировать больше возможностей по доступной цене.',
|
||||
includesTitle: 'Все в бесплатном плане, плюс:',
|
||||
for: 'Для независимых разработчиков/малых команд',
|
||||
},
|
||||
team: {
|
||||
name: 'Команда',
|
||||
description: 'Сотрудничайте без ограничений и наслаждайтесь высочайшей производительностью.',
|
||||
includesTitle: 'Все в профессиональном плане, плюс:',
|
||||
for: 'Для команд среднего размера',
|
||||
},
|
||||
enterprise: {
|
||||
|
|
@ -123,15 +115,15 @@ const translation = {
|
|||
description: 'Получите полный набор возможностей и поддержку для крупномасштабных критически важных систем.',
|
||||
includesTitle: 'Все в командном плане, плюс:',
|
||||
features: {
|
||||
7: 'Обновления и обслуживание от Dify официально',
|
||||
4: 'ССО',
|
||||
5: 'Согласованные SLA от Dify Partners',
|
||||
8: 'Профессиональная техническая поддержка',
|
||||
6: 'Современная безопасность и контроль',
|
||||
2: 'Эксклюзивные функции для предприятий',
|
||||
1: 'Коммерческая лицензия',
|
||||
3: 'Множественные рабочие области и управление предприятием',
|
||||
0: 'Решения для масштабируемого развертывания корпоративного уровня',
|
||||
5: 'Согласованные Соглашения об Уровне Услуг от Dify Partners',
|
||||
2: 'Эксклюзивные корпоративные функции',
|
||||
6: 'Расширенная безопасность и контроль',
|
||||
7: 'Обновления и обслуживание от Dify официально',
|
||||
3: 'Несколько рабочих пространств и управление предприятием',
|
||||
0: 'Масштабируемые решения для развертывания корпоративного уровня',
|
||||
1: 'Разрешение на коммерческую лицензию',
|
||||
},
|
||||
price: 'Пользовательский',
|
||||
priceTip: 'Только годовая подписка',
|
||||
|
|
@ -140,9 +132,9 @@ const translation = {
|
|||
},
|
||||
community: {
|
||||
features: {
|
||||
0: 'Все основные функции выпущены в публичном репозитории',
|
||||
1: 'Единое рабочее пространство',
|
||||
2: 'Соблюдает Лицензию на открытое программное обеспечение Dify',
|
||||
2: 'Соответствует лицензии Dify с открытым исходным кодом',
|
||||
0: 'Все основные функции выпущены в общедоступном репозитории',
|
||||
},
|
||||
name: 'Сообщество',
|
||||
btnText: 'Начните с сообщества',
|
||||
|
|
@ -153,10 +145,10 @@ const translation = {
|
|||
},
|
||||
premium: {
|
||||
features: {
|
||||
3: 'Приоритетная поддержка по электронной почте и чату',
|
||||
2: 'Настройка логотипа и брендинга WebApp',
|
||||
1: 'Единое рабочее пространство',
|
||||
2: 'Настройка логотипа и брендинга веб-приложения',
|
||||
0: 'Самостоятельное управление надежностью различными облачными провайдерами',
|
||||
3: 'Приоритетная поддержка по электронной почте и в чате',
|
||||
0: 'Самостоятельное управление надежностью от различных поставщиков облачных услуг',
|
||||
},
|
||||
description: 'Для средних организаций и команд',
|
||||
includesTitle: 'Всё из Сообщества, плюс:',
|
||||
|
|
@ -173,8 +165,6 @@ const translation = {
|
|||
fullSolution: 'Обновите свой тарифный план, чтобы получить больше места.',
|
||||
},
|
||||
apps: {
|
||||
fullTipLine1: 'Обновите свой тарифный план, чтобы',
|
||||
fullTipLine2: 'создавать больше приложений.',
|
||||
fullTip2des: 'Рекомендуется удалить неактивные приложения, чтобы освободить место, или свяжитесь с нами.',
|
||||
fullTip2: 'Достигнут лимит плана',
|
||||
contactUs: 'Свяжитесь с нами',
|
||||
|
|
|
|||
|
|
@ -23,19 +23,14 @@ const translation = {
|
|||
contractSales: 'Kontaktirajte prodajo',
|
||||
contractOwner: 'Kontaktirajte upravitelja ekipe',
|
||||
startForFree: 'Začnite brezplačno',
|
||||
getStartedWith: 'Začnite z ',
|
||||
contactSales: 'Kontaktirajte prodajo',
|
||||
talkToSales: 'Pogovorite se s prodajo',
|
||||
modelProviders: 'Ponudniki modelov',
|
||||
teamMembers: 'Člani ekipe',
|
||||
annotationQuota: 'Kvote za označevanje',
|
||||
buildApps: 'Gradite aplikacije',
|
||||
vectorSpace: 'Prostor za vektorje',
|
||||
vectorSpaceBillingTooltip: 'Vsak 1 MB lahko shrani približno 1,2 milijona znakov vektoriziranih podatkov (ocenjeno z uporabo OpenAI Embeddings, odvisno od modelov).',
|
||||
vectorSpaceTooltip: 'Prostor za vektorje je dolgoročni pomnilniški sistem, potreben za to, da LLM-ji razumejo vaše podatke.',
|
||||
documentsUploadQuota: 'Kvote za nalaganje dokumentov',
|
||||
documentProcessingPriority: 'Prioriteta obdelave dokumentov',
|
||||
documentProcessingPriorityTip: 'Za višjo prioriteto obdelave dokumentov nadgradite svoj načrt.',
|
||||
documentProcessingPriorityUpgrade: 'Obdelujte več podatkov z večjo natančnostjo in hitrostjo.',
|
||||
priority: {
|
||||
'standard': 'Standard',
|
||||
|
|
@ -103,19 +98,16 @@ const translation = {
|
|||
sandbox: {
|
||||
name: 'Peskovnik',
|
||||
description: '200 brezplačnih poskusov GPT',
|
||||
includesTitle: 'Vključuje:',
|
||||
for: 'Brezplačno preizkušanje osnovnih zmogljivosti',
|
||||
},
|
||||
professional: {
|
||||
name: 'Profesionalni',
|
||||
description: 'Za posameznike in male ekipe, da odklenete več zmogljivosti po ugodni ceni.',
|
||||
includesTitle: 'Vse v brezplačnem načrtu, plus:',
|
||||
for: 'Za neodvisne razvijalce/male ekipe',
|
||||
},
|
||||
team: {
|
||||
name: 'Ekipa',
|
||||
description: 'Sodelujte brez omejitev in uživajte v vrhunski zmogljivosti.',
|
||||
includesTitle: 'Vse v profesionalnem načrtu, plus:',
|
||||
for: 'Za srednje velike ekipe',
|
||||
},
|
||||
enterprise: {
|
||||
|
|
@ -123,15 +115,15 @@ const translation = {
|
|||
description: 'Pridobite vse zmogljivosti in podporo za velike sisteme kritične za misijo.',
|
||||
includesTitle: 'Vse v načrtu Ekipa, plus:',
|
||||
features: {
|
||||
5: 'Pogajali smo se o SLAs s partnerji Dify',
|
||||
4: 'SSO',
|
||||
0: 'Rešitve za razširljivo uvedbo na ravni podjetij',
|
||||
1: 'Avtorizacija za komercialno licenco',
|
||||
0: 'Prilagodljive rešitve za uvajanje na ravni podjetij',
|
||||
2: 'Ekskluzivne funkcije za podjetja',
|
||||
7: 'Posodobitve in vzdrževanje s strani Dify uradno',
|
||||
3: 'Več delovnih prostorov in upravljanje podjetij',
|
||||
7: 'Posodobitve in vzdrževanje s strani Dify Official',
|
||||
8: 'Strokovna tehnična podpora',
|
||||
1: 'Dovoljenje za komercialno licenco',
|
||||
3: 'Več delovnih prostorov in upravljanje podjetja',
|
||||
5: 'Dogovorjene pogodbe o ravni storitev s strani Dify Partners',
|
||||
6: 'Napredna varnost in nadzor',
|
||||
8: 'Profesionalna tehnična podpora',
|
||||
4: 'SSO',
|
||||
},
|
||||
priceTip: 'Letno zaračunavanje samo',
|
||||
price: 'Po meri',
|
||||
|
|
@ -140,9 +132,9 @@ const translation = {
|
|||
},
|
||||
community: {
|
||||
features: {
|
||||
2: 'Upošteva Dify odprtokodno licenco',
|
||||
0: 'Vse ključne funkcije so bile objavljene v javnem repozitoriju',
|
||||
1: 'Enotno delovno okolje',
|
||||
1: 'En delovni prostor',
|
||||
0: 'Vse osnovne funkcije, izdane v javnem repozitoriju',
|
||||
2: 'Skladen z odprtokodno licenco Dify',
|
||||
},
|
||||
includesTitle: 'Brezplačne funkcije:',
|
||||
price: 'Brezplačno',
|
||||
|
|
@ -153,10 +145,10 @@ const translation = {
|
|||
},
|
||||
premium: {
|
||||
features: {
|
||||
2: 'Prilagoditev logotipa in blagovne znamke spletne aplikacije',
|
||||
1: 'Enotno delovno okolje',
|
||||
0: 'Samoobvladovana zanesljivost različnih ponudnikov oblačnih storitev',
|
||||
3: 'Prednostna e-pošta in podpora za klepet',
|
||||
1: 'En delovni prostor',
|
||||
3: 'Prednostna podpora po e-pošti in klepetu',
|
||||
2: 'Prilagajanje logotipa in blagovne znamke WebApp',
|
||||
0: 'Samostojna zanesljivost različnih ponudnikov storitev v oblaku',
|
||||
},
|
||||
name: 'Premium',
|
||||
priceTip: 'Na podlagi oblaka Marketplace',
|
||||
|
|
@ -173,8 +165,6 @@ const translation = {
|
|||
fullSolution: 'Nadgradite svoj načrt za več prostora.',
|
||||
},
|
||||
apps: {
|
||||
fullTipLine1: 'Nadgradite svoj načrt, da',
|
||||
fullTipLine2: 'gradite več aplikacij.',
|
||||
fullTip1des: 'Dosegli ste omejitev za izdelavo aplikacij v tem načrtu.',
|
||||
fullTip1: 'Nadgradite za ustvarjanje več aplikacij',
|
||||
fullTip2: 'Dosežena meja načrta',
|
||||
|
|
|
|||
|
|
@ -23,19 +23,14 @@ const translation = {
|
|||
contractSales: 'ติดต่อฝ่ายขาย',
|
||||
contractOwner: 'ติดต่อผู้จัดการทีม',
|
||||
startForFree: 'เริ่มฟรี',
|
||||
getStartedWith: 'เริ่มต้นใช้งาน',
|
||||
contactSales: 'ติดต่อฝ่ายขาย',
|
||||
talkToSales: 'พูดคุยกับฝ่ายขาย',
|
||||
modelProviders: 'ผู้ให้บริการโมเดล',
|
||||
teamMembers: 'สมาชิกในทีม',
|
||||
annotationQuota: 'โควต้าคําอธิบายประกอบ',
|
||||
buildApps: 'สร้างแอพ',
|
||||
vectorSpace: 'พื้นที่เวกเตอร์',
|
||||
vectorSpaceBillingTooltip: 'แต่ละ 1MB สามารถจัดเก็บข้อมูลแบบเวกเตอร์ได้ประมาณ 1.2 ล้านอักขระ (โดยประมาณโดยใช้ OpenAI Embeddings แตกต่างกันไปตามรุ่น)',
|
||||
vectorSpaceTooltip: 'Vector Space เป็นระบบหน่วยความจําระยะยาวที่จําเป็นสําหรับ LLM ในการทําความเข้าใจข้อมูลของคุณ',
|
||||
documentsUploadQuota: 'โควต้าการอัปโหลดเอกสาร',
|
||||
documentProcessingPriority: 'ลําดับความสําคัญในการประมวลผลเอกสาร',
|
||||
documentProcessingPriorityTip: 'สําหรับลําดับความสําคัญในการประมวลผลเอกสารที่สูงขึ้น โปรดอัปเกรดแผนของคุณ',
|
||||
documentProcessingPriorityUpgrade: 'ประมวลผลข้อมูลได้มากขึ้นด้วยความแม่นยําที่สูงขึ้นด้วยความเร็วที่เร็วขึ้น',
|
||||
priority: {
|
||||
'standard': 'มาตรฐาน',
|
||||
|
|
@ -103,19 +98,16 @@ const translation = {
|
|||
sandbox: {
|
||||
name: 'กระบะทราย',
|
||||
description: 'ทดลองใช้ GPT ฟรี 200 ครั้ง',
|
||||
includesTitle: 'มี:',
|
||||
for: 'ทดลองใช้ฟรีของความสามารถหลัก',
|
||||
},
|
||||
professional: {
|
||||
name: 'มืออาชีพ',
|
||||
description: 'สําหรับบุคคลและทีมขนาดเล็กเพื่อปลดล็อกพลังงานมากขึ้นในราคาย่อมเยา',
|
||||
includesTitle: 'ทุกอย่างในแผนฟรี รวมถึง:',
|
||||
for: 'สำหรับนักพัฒนาที่เป็นอิสระ/ทีมขนาดเล็ก',
|
||||
},
|
||||
team: {
|
||||
name: 'ทีม',
|
||||
description: 'ทํางานร่วมกันอย่างไร้ขีดจํากัดและเพลิดเพลินไปกับประสิทธิภาพระดับสูงสุด',
|
||||
includesTitle: 'ทุกอย่างในแผน Professional รวมถึง:',
|
||||
for: 'สำหรับทีมขนาดกลาง',
|
||||
},
|
||||
enterprise: {
|
||||
|
|
@ -123,15 +115,15 @@ const translation = {
|
|||
description: 'รับความสามารถและการสนับสนุนเต็มรูปแบบสําหรับระบบที่สําคัญต่อภารกิจขนาดใหญ่',
|
||||
includesTitle: 'ทุกอย่างในแผนทีม รวมถึง:',
|
||||
features: {
|
||||
8: 'การสนับสนุนทางเทคนิคระดับมืออาชีพ',
|
||||
2: 'คุณสมบัติพิเศษขององค์กร',
|
||||
3: 'หลายพื้นที่ทำงานและการบริหารจัดการองค์กร',
|
||||
4: 'SSO',
|
||||
6: 'ความปลอดภัยและการควบคุมขั้นสูง',
|
||||
5: 'เจรจาข้อตกลงบริการ (SLA) โดย Dify Partners',
|
||||
7: 'การอัปเดตและการบำรุงรักษาโดย Dify อย่างเป็นทางการ',
|
||||
1: 'ใบอนุญาตการใช้เชิงพาณิชย์',
|
||||
0: 'โซลูชันการปรับใช้ที่มีขนาดใหญ่และมีคุณภาพระดับองค์กร',
|
||||
2: 'คุณสมบัติพิเศษสําหรับองค์กร',
|
||||
5: 'SLA ที่เจรจาโดย Dify Partners',
|
||||
1: 'การอนุญาตใบอนุญาตเชิงพาณิชย์',
|
||||
8: 'การสนับสนุนด้านเทคนิคอย่างมืออาชีพ',
|
||||
0: 'โซลูชันการปรับใช้ที่ปรับขนาดได้ระดับองค์กร',
|
||||
7: 'การอัปเดตและบํารุงรักษาโดย Dify อย่างเป็นทางการ',
|
||||
3: 'พื้นที่ทํางานหลายแห่งและการจัดการองค์กร',
|
||||
6: 'การรักษาความปลอดภัยและการควบคุมขั้นสูง',
|
||||
},
|
||||
btnText: 'ติดต่อฝ่ายขาย',
|
||||
price: 'ที่กำหนดเอง',
|
||||
|
|
@ -140,9 +132,9 @@ const translation = {
|
|||
},
|
||||
community: {
|
||||
features: {
|
||||
2: 'ปฏิบัติตามใบอนุญาตโอเพ่นซอร์สของ Dify',
|
||||
0: 'ฟีเจอร์หลักทั้งหมดถูกปล่อยออกภายใต้ที่เก็บสาธารณะ',
|
||||
1: 'พื้นที่ทำงานเดียว',
|
||||
1: 'พื้นที่ทํางานเดียว',
|
||||
2: 'สอดคล้องกับใบอนุญาตโอเพ่นซอร์ส Dify',
|
||||
0: 'คุณสมบัติหลักทั้งหมดที่เผยแพร่ภายใต้ที่เก็บสาธารณะ',
|
||||
},
|
||||
name: 'ชุมชน',
|
||||
price: 'ฟรี',
|
||||
|
|
@ -153,10 +145,10 @@ const translation = {
|
|||
},
|
||||
premium: {
|
||||
features: {
|
||||
3: 'การสนับสนุนทางอีเมลและแชทที่มีความสำคัญ',
|
||||
1: 'พื้นที่ทำงานเดียว',
|
||||
2: 'การปรับแต่งโลโก้และแบรนดิ้งของเว็บแอป',
|
||||
0: 'การจัดการความน่าเชื่อถือด้วยตนเองโดยผู้ให้บริการคลาวด์ต่าง ๆ',
|
||||
2: 'โลโก้ WebApp และการปรับแต่งแบรนด์',
|
||||
3: 'การสนับสนุนทางอีเมลและแชทลําดับความสําคัญ',
|
||||
1: 'พื้นที่ทํางานเดียว',
|
||||
0: 'ความน่าเชื่อถือที่จัดการด้วยตนเองโดยผู้ให้บริการคลาวด์ต่างๆ',
|
||||
},
|
||||
priceTip: 'อิงตามตลาดคลาวด์',
|
||||
for: 'สำหรับองค์กรและทีมขนาดกลาง',
|
||||
|
|
@ -173,8 +165,6 @@ const translation = {
|
|||
fullSolution: 'อัปเกรดแผนของคุณเพื่อเพิ่มพื้นที่',
|
||||
},
|
||||
apps: {
|
||||
fullTipLine1: 'อัปเกรดแผนของคุณเป็น',
|
||||
fullTipLine2: 'สร้างแอปเพิ่มเติม',
|
||||
contactUs: 'ติดต่อเรา',
|
||||
fullTip2: 'ถึงขีดจำกัดของแผนแล้ว',
|
||||
fullTip1: 'อัปเกรดเพื่อสร้างแอปเพิ่มเติม',
|
||||
|
|
|
|||
|
|
@ -23,19 +23,14 @@ const translation = {
|
|||
contractSales: 'Satışla iletişime geçin',
|
||||
contractOwner: 'Takım yöneticisine başvurun',
|
||||
startForFree: 'Ücretsiz Başla',
|
||||
getStartedWith: 'ile başlayın',
|
||||
contactSales: 'Satışlarla İletişime Geçin',
|
||||
talkToSales: 'Satışlarla Konuşun',
|
||||
modelProviders: 'Model Sağlayıcılar',
|
||||
teamMembers: 'Takım Üyeleri',
|
||||
annotationQuota: 'Ek Açıklama Kotası',
|
||||
buildApps: 'Uygulamalar Oluştur',
|
||||
vectorSpace: 'Vektör Alanı',
|
||||
vectorSpaceBillingTooltip: 'Her 1MB yaklaşık 1.2 milyon karakter vektörize veri depolayabilir (OpenAI Embeddings ile tahmin edilmiştir, modellere göre farklılık gösterebilir).',
|
||||
vectorSpaceTooltip: 'Vektör Alanı, LLM\'lerin verilerinizi anlaması için gerekli uzun süreli hafıza sistemidir.',
|
||||
documentsUploadQuota: 'Doküman Yükleme Kotası',
|
||||
documentProcessingPriority: 'Doküman İşleme Önceliği',
|
||||
documentProcessingPriorityTip: 'Daha yüksek doküman işleme önceliği için planınızı yükseltin.',
|
||||
documentProcessingPriorityUpgrade: 'Daha fazla veriyi daha yüksek doğrulukla ve daha hızlı işleyin.',
|
||||
priority: {
|
||||
'standard': 'Standart',
|
||||
|
|
@ -103,19 +98,16 @@ const translation = {
|
|||
sandbox: {
|
||||
name: 'Sandbox',
|
||||
description: '200 kez GPT ücretsiz deneme',
|
||||
includesTitle: 'İçerdikleri:',
|
||||
for: 'Temel Yeteneklerin Ücretsiz Denemesi',
|
||||
},
|
||||
professional: {
|
||||
name: 'Profesyonel',
|
||||
description: 'Bireyler ve küçük takımlar için daha fazla güç açın.',
|
||||
includesTitle: 'Ücretsiz plandaki her şey, artı:',
|
||||
for: 'Bağımsız Geliştiriciler/Küçük Takımlar için',
|
||||
},
|
||||
team: {
|
||||
name: 'Takım',
|
||||
description: 'Sınırsız işbirliği ve en üst düzey performans.',
|
||||
includesTitle: 'Profesyonel plandaki her şey, artı:',
|
||||
for: 'Orta Boyutlu Takımlar İçin',
|
||||
},
|
||||
enterprise: {
|
||||
|
|
@ -123,15 +115,15 @@ const translation = {
|
|||
description: 'Büyük ölçekli kritik sistemler için tam yetenekler ve destek.',
|
||||
includesTitle: 'Takım plandaki her şey, artı:',
|
||||
features: {
|
||||
3: 'Birden Fazla Çalışma Alanı ve Kurumsal Yönetim',
|
||||
8: 'Profesyonel Teknik Destek',
|
||||
4: 'SSO',
|
||||
2: 'Özel Şirket Özellikleri',
|
||||
1: 'Ticari Lisans Yetkilendirmesi',
|
||||
7: 'Dify Tarafından Resmi Güncellemeler ve Bakım',
|
||||
5: 'Dify Ortakları tarafından müzakere edilen SLA\'lar',
|
||||
6: 'Gelişmiş Güvenlik ve Kontroller',
|
||||
5: 'Dify Partners tarafından müzakere edilen SLA\'lar',
|
||||
4: 'SSO',
|
||||
2: 'Özel Kurumsal Özellikler',
|
||||
0: 'Kurumsal Düzeyde Ölçeklenebilir Dağıtım Çözümleri',
|
||||
7: 'Resmi olarak Dify tarafından Güncellemeler ve Bakım',
|
||||
3: 'Çoklu Çalışma Alanları ve Kurumsal Yönetim',
|
||||
},
|
||||
priceTip: 'Yıllık Faturalama Sadece',
|
||||
for: 'Büyük boyutlu Takımlar için',
|
||||
|
|
@ -140,9 +132,9 @@ const translation = {
|
|||
},
|
||||
community: {
|
||||
features: {
|
||||
1: 'Tek İş Alanı',
|
||||
0: 'Tüm Temel Özellikler Kamu Deposu Altında Yayınlandı',
|
||||
2: 'Dify Açık Kaynak Lisansına uyar',
|
||||
1: 'Tek Çalışma Alanı',
|
||||
0: 'Genel depo altında yayınlanan tüm temel özellikler',
|
||||
2: 'Dify Açık Kaynak Lisansı ile uyumludur',
|
||||
},
|
||||
price: 'Ücretsiz',
|
||||
includesTitle: 'Ücretsiz Özellikler:',
|
||||
|
|
@ -153,10 +145,10 @@ const translation = {
|
|||
},
|
||||
premium: {
|
||||
features: {
|
||||
1: 'Tek İş Alanı',
|
||||
0: 'Çeşitli Bulut Sağlayıcıları Tarafından Kendiliğinden Yönetilen Güvenilirlik',
|
||||
3: 'Öncelikli Email ve Sohbet Desteği',
|
||||
2: 'Web Uygulaması Logo ve Markalaşma Özelleştirmesi',
|
||||
1: 'Tek Çalışma Alanı',
|
||||
0: 'Çeşitli Bulut Sağlayıcıları Tarafından Kendi Kendini Yöneten Güvenilirlik',
|
||||
2: 'WebApp Logosu ve Marka Özelleştirmesi',
|
||||
3: 'Öncelikli E-posta ve Sohbet Desteği',
|
||||
},
|
||||
name: 'Premium',
|
||||
includesTitle: 'Topluluktan her şey, artı:',
|
||||
|
|
@ -173,8 +165,6 @@ const translation = {
|
|||
fullSolution: 'Daha fazla alan için planınızı yükseltin.',
|
||||
},
|
||||
apps: {
|
||||
fullTipLine1: 'Daha fazla uygulama oluşturmak için',
|
||||
fullTipLine2: 'planınızı yükseltin.',
|
||||
contactUs: 'Bizimle iletişime geçin',
|
||||
fullTip2des: 'Kullanımı serbest bırakmak için etkisiz uygulamaların temizlenmesi önerilir veya bizimle iletişime geçin.',
|
||||
fullTip1des: 'Bu planda uygulama oluşturma limitine ulaştınız.',
|
||||
|
|
|
|||
|
|
@ -23,17 +23,13 @@ const translation = {
|
|||
contractSales: 'Зв\'язатися з відділом продажів',
|
||||
contractOwner: 'Зв\'язатися з керівником команди',
|
||||
startForFree: 'Почніть безкоштовно',
|
||||
getStartedWith: 'Почніть роботу з ',
|
||||
contactSales: 'Зв\'язатися з відділом продажів',
|
||||
talkToSales: 'Поговоріть зі службою продажів',
|
||||
modelProviders: 'Постачальники моделей',
|
||||
teamMembers: 'Члени команди',
|
||||
buildApps: 'Створювати додатки',
|
||||
vectorSpace: 'Векторний простір',
|
||||
vectorSpaceBillingTooltip: 'Кожен 1 МБ може зберігати близько 1,2 мільйона символів векторизованих даних (оцінка з використанням OpenAI Embeddings, відрізняється в залежності від моделей).',
|
||||
vectorSpaceTooltip: 'Векторний простір – це система довгострокової пам\'яті, необхідна LLM для розуміння ваших даних.',
|
||||
documentProcessingPriority: 'Пріоритет обробки документів',
|
||||
documentProcessingPriorityTip: 'Для вищого пріоритету обробки документів оновіть свій план.',
|
||||
documentProcessingPriorityUpgrade: 'Обробляйте більше даних із вищою точністю та на більших швидкостях.',
|
||||
priority: {
|
||||
'standard': 'Стандартний',
|
||||
|
|
@ -77,7 +73,6 @@ const translation = {
|
|||
ragAPIRequestTooltip: 'Відноситься до кількості викликів API, що викликають лише можливості обробки бази знань Dify.',
|
||||
receiptInfo: 'Лише власник команди та адміністратор команди можуть підписуватися та переглядати інформацію про виставлення рахунків',
|
||||
annotationQuota: 'Квота анотацій',
|
||||
documentsUploadQuota: 'Квота завантаження документів',
|
||||
teamMember_one: '{{count,number}} член команди',
|
||||
teamWorkspace: '{{count,number}} Командний Простір',
|
||||
apiRateLimit: 'Обмеження швидкості API',
|
||||
|
|
@ -103,19 +98,16 @@ const translation = {
|
|||
sandbox: {
|
||||
name: 'Пісочниця',
|
||||
description: '200 безкоштовних пробних версій GPT',
|
||||
includesTitle: 'Включає в себе:',
|
||||
for: 'Безкоштовна пробна версія основних функцій',
|
||||
},
|
||||
professional: {
|
||||
name: 'Професійний',
|
||||
description: 'Щоб окремі особи та невеликі команди могли отримати більше можливостей за доступною ціною.',
|
||||
includesTitle: 'Все у безкоштовному плані, плюс:',
|
||||
for: 'Для незалежних розробників/малих команд',
|
||||
},
|
||||
team: {
|
||||
name: 'Команда',
|
||||
description: 'Співпрацюйте без обмежень і користуйтеся продуктивністю найвищого рівня.',
|
||||
includesTitle: 'Все, що входить до плану Professional, плюс:',
|
||||
for: 'Для середніх команд',
|
||||
},
|
||||
enterprise: {
|
||||
|
|
@ -123,15 +115,15 @@ const translation = {
|
|||
description: 'Отримайте повні можливості та підтримку для масштабних критично важливих систем.',
|
||||
includesTitle: 'Все, що входить до плану Team, плюс:',
|
||||
features: {
|
||||
5: 'Угоди про рівень обслуговування, узгоджені партнерами Dify',
|
||||
2: 'Ексклюзивні підприємницькі функції',
|
||||
6: 'Розвинена безпека та контроль',
|
||||
4: 'Єдиний вхід',
|
||||
7: 'Оновлення та обслуговування від Dify Official',
|
||||
1: 'Авторизація комерційної ліцензії',
|
||||
8: 'Професійна технічна підтримка',
|
||||
1: 'Комерційна ліцензія на авторизацію',
|
||||
3: 'Кілька робочих просторів та управління підприємством',
|
||||
4: 'ССО',
|
||||
0: 'Рішення для масштабованого розгортання підприємств',
|
||||
7: 'Оновлення та обслуговування від Dify Офіційно',
|
||||
2: 'Ексклюзивні функції підприємства',
|
||||
6: 'Розширені функції безпеки та керування',
|
||||
3: 'Кілька робочих областей і управління підприємством',
|
||||
5: 'Угода про рівень обслуговування за домовленістю від Dify Partners',
|
||||
0: 'Масштабовані рішення для розгортання корпоративного рівня',
|
||||
},
|
||||
btnText: 'Зв\'язатися з відділом продажу',
|
||||
priceTip: 'Тільки річна оплата',
|
||||
|
|
@ -140,9 +132,9 @@ const translation = {
|
|||
},
|
||||
community: {
|
||||
features: {
|
||||
2: 'Відповідає ліцензії Dify Open Source',
|
||||
1: 'Єдине робоче місце',
|
||||
0: 'Усі основні функції випущені під публічним репозиторієм',
|
||||
2: 'Відповідає ліцензії Dify з відкритим вихідним кодом',
|
||||
0: 'Усі основні функції випущено в загальнодоступному репозиторії',
|
||||
},
|
||||
btnText: 'Розпочніть з громади',
|
||||
includesTitle: 'Безкоштовні можливості:',
|
||||
|
|
@ -153,10 +145,10 @@ const translation = {
|
|||
},
|
||||
premium: {
|
||||
features: {
|
||||
2: 'Логотип веб-додатку та налаштування брендингу',
|
||||
1: 'Єдине робоче місце',
|
||||
3: 'Пріоритетна email та чат підтримка',
|
||||
0: 'Самостійно керовані надійність різних хмарних постачальників',
|
||||
2: 'Налаштування логотипу WebApp та брендингу',
|
||||
3: 'Пріоритетна підтримка електронною поштою та в чаті',
|
||||
0: 'Самокерована надійність різними хмарними провайдерами',
|
||||
},
|
||||
description: 'Для середніх підприємств та команд',
|
||||
btnText: 'Отримайте Преміум у',
|
||||
|
|
@ -173,8 +165,6 @@ const translation = {
|
|||
fullSolution: 'Оновіть свій план, щоб отримати більше місця.',
|
||||
},
|
||||
apps: {
|
||||
fullTipLine1: 'Оновіть свій план, щоб',
|
||||
fullTipLine2: 'створити більше програм.',
|
||||
fullTip1des: 'Ви досягли межі створення додатків за цим планом.',
|
||||
fullTip2: 'Досягнуто ліміту плану',
|
||||
fullTip1: 'Оновіть, щоб створити більше додатків',
|
||||
|
|
|
|||
|
|
@ -23,18 +23,13 @@ const translation = {
|
|||
contractSales: 'Liên hệ bộ phận bán hàng',
|
||||
contractOwner: 'Liên hệ quản lý nhóm',
|
||||
startForFree: 'Bắt đầu miễn phí',
|
||||
getStartedWith: 'Bắt đầu với ',
|
||||
contactSales: 'Liên hệ Bán hàng',
|
||||
talkToSales: 'Nói chuyện với Bộ phận Bán hàng',
|
||||
modelProviders: 'Nhà cung cấp Mô hình',
|
||||
teamMembers: 'Thành viên Nhóm',
|
||||
buildApps: 'Xây dựng Ứng dụng',
|
||||
vectorSpace: 'Không gian Vector',
|
||||
vectorSpaceBillingTooltip: 'Mỗi 1MB có thể lưu trữ khoảng 1.2 triệu ký tự dữ liệu vector hóa (ước tính sử dụng OpenAI Embeddings, thay đổi tùy theo các mô hình).',
|
||||
vectorSpaceTooltip: 'Không gian Vector là hệ thống bộ nhớ dài hạn cần thiết cho LLMs để hiểu dữ liệu của bạn.',
|
||||
documentsUploadQuota: 'Hạn mức Tải lên Tài liệu',
|
||||
documentProcessingPriority: 'Ưu tiên Xử lý Tài liệu',
|
||||
documentProcessingPriorityTip: 'Để có ưu tiên xử lý tài liệu cao hơn, vui lòng nâng cấp kế hoạch của bạn.',
|
||||
documentProcessingPriorityUpgrade: 'Xử lý nhiều dữ liệu với độ chính xác cao và tốc độ nhanh hơn.',
|
||||
priority: {
|
||||
'standard': 'Tiêu chuẩn',
|
||||
|
|
@ -103,19 +98,16 @@ const translation = {
|
|||
sandbox: {
|
||||
name: 'Hộp Cát',
|
||||
description: 'Thử nghiệm miễn phí 200 lần GPT',
|
||||
includesTitle: 'Bao gồm:',
|
||||
for: 'Dùng thử miễn phí các tính năng cốt lõi',
|
||||
},
|
||||
professional: {
|
||||
name: 'Chuyên nghiệp',
|
||||
description: 'Dành cho cá nhân và các nhóm nhỏ để mở khóa nhiều sức mạnh với giá cả phải chăng.',
|
||||
includesTitle: 'Tất cả trong kế hoạch miễn phí, cộng thêm:',
|
||||
for: 'Dành cho các nhà phát triển độc lập/nhóm nhỏ',
|
||||
},
|
||||
team: {
|
||||
name: 'Nhóm',
|
||||
description: 'Hợp tác mà không giới hạn và tận hưởng hiệu suất hạng nhất.',
|
||||
includesTitle: 'Tất cả trong kế hoạch Chuyên nghiệp, cộng thêm:',
|
||||
for: 'Dành cho các đội nhóm vừa',
|
||||
},
|
||||
enterprise: {
|
||||
|
|
@ -123,15 +115,15 @@ const translation = {
|
|||
description: 'Nhận toàn bộ khả năng và hỗ trợ cho các hệ thống quan trọng cho nhiệm vụ quy mô lớn.',
|
||||
includesTitle: 'Tất cả trong kế hoạch Nhóm, cộng thêm:',
|
||||
features: {
|
||||
2: 'Tính năng Doanh nghiệp Độc quyền',
|
||||
1: 'Giấy phép kinh doanh',
|
||||
8: 'Hỗ trợ kỹ thuật chuyên nghiệp',
|
||||
7: 'Cập nhật và Bảo trì bởi Dify Chính thức',
|
||||
5: 'Thỏa thuận SLA bởi các đối tác Dify',
|
||||
6: 'An ninh nâng cao và kiểm soát',
|
||||
3: 'Nhiều không gian làm việc & Quản lý doanh nghiệp',
|
||||
0: 'Giải pháp triển khai mở rộng quy mô cấp doanh nghiệp',
|
||||
2: 'Các tính năng dành riêng cho doanh nghiệp',
|
||||
3: 'Nhiều không gian làm việc & quản lý doanh nghiệp',
|
||||
7: 'Cập nhật và bảo trì bởi Dify chính thức',
|
||||
4: 'SSO',
|
||||
8: 'Hỗ trợ kỹ thuật chuyên nghiệp',
|
||||
5: 'SLA được đàm phán bởi Dify Partners',
|
||||
1: 'Ủy quyền giấy phép thương mại',
|
||||
6: 'Bảo mật & Kiểm soát nâng cao',
|
||||
0: 'Giải pháp triển khai có thể mở rộng cấp doanh nghiệp',
|
||||
},
|
||||
price: 'Tùy chỉnh',
|
||||
for: 'Dành cho các đội lớn',
|
||||
|
|
@ -140,9 +132,9 @@ const translation = {
|
|||
},
|
||||
community: {
|
||||
features: {
|
||||
2: 'Tuân thủ Giấy phép Mã nguồn Mở Dify',
|
||||
0: 'Tất cả các tính năng cốt lõi được phát hành dưới Kho lưu trữ công khai',
|
||||
1: 'Không gian làm việc đơn',
|
||||
0: 'Tất cả các tính năng cốt lõi được phát hành trong kho lưu trữ công cộng',
|
||||
2: 'Tuân thủ Giấy phép nguồn mở Dify',
|
||||
},
|
||||
description: 'Dành cho người dùng cá nhân, nhóm nhỏ hoặc các dự án phi thương mại',
|
||||
name: 'Cộng đồng',
|
||||
|
|
@ -153,10 +145,10 @@ const translation = {
|
|||
},
|
||||
premium: {
|
||||
features: {
|
||||
3: 'Hỗ trợ qua Email & Chat Ưu tiên',
|
||||
2: 'Tùy chỉnh Logo & Thương hiệu Ứng dụng Web',
|
||||
1: 'Không gian làm việc đơn',
|
||||
0: 'Độ tin cậy tự quản lý bởi các nhà cung cấp đám mây khác nhau',
|
||||
2: 'Logo WebApp & Tùy chỉnh thương hiệu',
|
||||
3: 'Hỗ trợ email & trò chuyện ưu tiên',
|
||||
0: 'Độ tin cậy tự quản lý của các nhà cung cấp đám mây khác nhau',
|
||||
},
|
||||
comingSoon: 'Hỗ trợ Microsoft Azure & Google Cloud Sẽ Đến Sớm',
|
||||
priceTip: 'Dựa trên Thị trường Đám mây',
|
||||
|
|
@ -173,8 +165,6 @@ const translation = {
|
|||
fullSolution: 'Nâng cấp kế hoạch của bạn để có thêm không gian.',
|
||||
},
|
||||
apps: {
|
||||
fullTipLine1: 'Nâng cấp kế hoạch của bạn để',
|
||||
fullTipLine2: 'xây dựng thêm ứng dụng.',
|
||||
contactUs: 'Liên hệ với chúng tôi',
|
||||
fullTip2: 'Đã đạt giới hạn kế hoạch',
|
||||
fullTip1des: 'Bạn đã đạt đến giới hạn xây dựng ứng dụng trên kế hoạch này.',
|
||||
|
|
|
|||
|
|
@ -115,6 +115,15 @@ const translation = {
|
|||
description: '獲得大規模關鍵任務系統的完整功能和支援。',
|
||||
includesTitle: 'Team 計劃中的一切,加上:',
|
||||
features: {
|
||||
8: '專業技術支持',
|
||||
3: '多個工作區和企業管理',
|
||||
0: '企業級可擴展部署解決方案',
|
||||
1: '商業許可證授權',
|
||||
7: 'Dify 官方更新和維護',
|
||||
6: '進階安全與控制',
|
||||
4: '單一登入',
|
||||
5: 'Dify 合作夥伴協商的 SLA',
|
||||
2: '獨家企業功能',
|
||||
},
|
||||
price: '自訂',
|
||||
btnText: '聯繫銷售',
|
||||
|
|
@ -123,6 +132,9 @@ const translation = {
|
|||
},
|
||||
community: {
|
||||
features: {
|
||||
0: '所有核心功能在公共存儲庫下發布',
|
||||
1: '單一工作區',
|
||||
2: '符合 Dify 開源許可證',
|
||||
},
|
||||
includesTitle: '免費功能:',
|
||||
btnText: '開始使用社區',
|
||||
|
|
@ -133,6 +145,10 @@ const translation = {
|
|||
},
|
||||
premium: {
|
||||
features: {
|
||||
3: '優先電子郵件和聊天支持',
|
||||
2: 'WebApp 標誌和品牌定制',
|
||||
0: '各種雲端供應商的自我管理可靠性',
|
||||
1: '單一工作區',
|
||||
},
|
||||
for: '適用於中型組織和團隊',
|
||||
comingSoon: '微軟 Azure 與 Google Cloud 支持即將推出',
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"name": "dify-web",
|
||||
"version": "1.7.2",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.14.0",
|
||||
"engines": {
|
||||
"node": ">=v22.11.0"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue