Merge branch 'main' into feat/rag-2

This commit is contained in:
twwu 2025-08-14 15:03:48 +08:00
commit 02720c9b95
54 changed files with 1474 additions and 306 deletions

View File

@ -1,5 +1,7 @@
from werkzeug.exceptions import HTTPException
from libs.exception import BaseHTTPException
class FilenameNotExistsError(HTTPException):
code = 400
@ -9,3 +11,27 @@ class FilenameNotExistsError(HTTPException):
class RemoteFileUploadError(HTTPException):
code = 400
description = "Error uploading remote file."
class FileTooLargeError(BaseHTTPException):
error_code = "file_too_large"
description = "File size exceeded. {message}"
code = 413
class UnsupportedFileTypeError(BaseHTTPException):
error_code = "unsupported_file_type"
description = "File type not allowed."
code = 415
class TooManyFilesError(BaseHTTPException):
error_code = "too_many_files"
description = "Only one file is allowed."
code = 400
class NoFileUploadedError(BaseHTTPException):
error_code = "no_file_uploaded"
description = "Please upload your file."
code = 400

View File

@ -3,9 +3,8 @@ from flask_login import current_user
from flask_restful import Resource, marshal, marshal_with, reqparse
from werkzeug.exceptions import Forbidden
from controllers.common.errors import NoFileUploadedError, TooManyFilesError
from controllers.console import api
from controllers.console.app.error import NoFileUploadedError
from controllers.console.datasets.error import TooManyFilesError
from controllers.console.wraps import (
account_initialization_required,
cloud_edition_billing_resource_check,

View File

@ -79,18 +79,6 @@ class ProviderNotSupportSpeechToTextError(BaseHTTPException):
code = 400
class NoFileUploadedError(BaseHTTPException):
error_code = "no_file_uploaded"
description = "Please upload your file."
code = 400
class TooManyFilesError(BaseHTTPException):
error_code = "too_many_files"
description = "Only one file is allowed."
code = 400
class DraftWorkflowNotExist(BaseHTTPException):
error_code = "draft_workflow_not_exist"
description = "Draft workflow need to be initialized."

View File

@ -27,7 +27,7 @@ from fields.conversation_fields import annotation_fields, message_detail_fields
from libs.helper import uuid_value
from libs.infinite_scroll_pagination import InfiniteScrollPagination
from libs.login import login_required
from models.model import AppMode, Conversation, Message, MessageAnnotation
from models.model import AppMode, Conversation, Message, MessageAnnotation, MessageFeedback
from services.annotation_service import AppAnnotationService
from services.errors.conversation import ConversationNotExistsError
from services.errors.message import MessageNotExistsError, SuggestedQuestionsAfterAnswerDisabledError
@ -124,17 +124,34 @@ class MessageFeedbackApi(Resource):
parser.add_argument("rating", type=str, choices=["like", "dislike", None], location="json")
args = parser.parse_args()
try:
MessageService.create_feedback(
app_model=app_model,
message_id=str(args["message_id"]),
user=current_user,
rating=args.get("rating"),
content=None,
)
except MessageNotExistsError:
message_id = str(args["message_id"])
message = db.session.query(Message).filter(Message.id == message_id, Message.app_id == app_model.id).first()
if not message:
raise NotFound("Message Not Exists.")
feedback = message.admin_feedback
if not args["rating"] and feedback:
db.session.delete(feedback)
elif args["rating"] and feedback:
feedback.rating = args["rating"]
elif not args["rating"] and not feedback:
raise ValueError("rating cannot be None when feedback not exists")
else:
feedback = MessageFeedback(
app_id=app_model.id,
conversation_id=message.conversation_id,
message_id=message.id,
rating=args["rating"],
from_source="admin",
from_account_id=current_user.id,
)
db.session.add(feedback)
db.session.commit()
return {"result": "success"}

View File

@ -1,30 +1,6 @@
from libs.exception import BaseHTTPException
class NoFileUploadedError(BaseHTTPException):
error_code = "no_file_uploaded"
description = "Please upload your file."
code = 400
class TooManyFilesError(BaseHTTPException):
error_code = "too_many_files"
description = "Only one file is allowed."
code = 400
class FileTooLargeError(BaseHTTPException):
error_code = "file_too_large"
description = "File size exceeded. {message}"
code = 413
class UnsupportedFileTypeError(BaseHTTPException):
error_code = "unsupported_file_type"
description = "File type not allowed."
code = 415
class DatasetNotInitializedError(BaseHTTPException):
error_code = "dataset_not_initialized"
description = "The dataset is still being initialized or indexing. Please wait a moment."

View File

@ -76,30 +76,6 @@ class EmailSendIpLimitError(BaseHTTPException):
code = 429
class FileTooLargeError(BaseHTTPException):
error_code = "file_too_large"
description = "File size exceeded. {message}"
code = 413
class UnsupportedFileTypeError(BaseHTTPException):
error_code = "unsupported_file_type"
description = "File type not allowed."
code = 415
class TooManyFilesError(BaseHTTPException):
error_code = "too_many_files"
description = "Only one file is allowed."
code = 400
class NoFileUploadedError(BaseHTTPException):
error_code = "no_file_uploaded"
description = "Please upload your file."
code = 400
class UnauthorizedAndForceLogout(BaseHTTPException):
error_code = "unauthorized_and_force_logout"
description = "Unauthorized and force logout."

View File

@ -8,7 +8,13 @@ from werkzeug.exceptions import Forbidden
import services
from configs import dify_config
from constants import DOCUMENT_EXTENSIONS
from controllers.common.errors import FilenameNotExistsError
from controllers.common.errors import (
FilenameNotExistsError,
FileTooLargeError,
NoFileUploadedError,
TooManyFilesError,
UnsupportedFileTypeError,
)
from controllers.console.wraps import (
account_initialization_required,
cloud_edition_billing_resource_check,
@ -18,13 +24,6 @@ from fields.file_fields import file_fields, upload_config_fields
from libs.login import login_required
from services.file_service import FileService
from .error import (
FileTooLargeError,
NoFileUploadedError,
TooManyFilesError,
UnsupportedFileTypeError,
)
PREVIEW_WORDS_LIMIT = 3000

View File

@ -7,18 +7,17 @@ from flask_restful import Resource, marshal_with, reqparse
import services
from controllers.common import helpers
from controllers.common.errors import RemoteFileUploadError
from controllers.common.errors import (
FileTooLargeError,
RemoteFileUploadError,
UnsupportedFileTypeError,
)
from core.file import helpers as file_helpers
from core.helper import ssrf_proxy
from fields.file_fields import file_fields_with_signed_url, remote_file_info_fields
from models.account import Account
from services.file_service import FileService
from .error import (
FileTooLargeError,
UnsupportedFileTypeError,
)
class RemoteFileInfoApi(Resource):
@marshal_with(remote_file_info_fields)

View File

@ -7,15 +7,15 @@ from sqlalchemy import select
from werkzeug.exceptions import Unauthorized
import services
from controllers.common.errors import FilenameNotExistsError
from controllers.console import api
from controllers.console.admin import admin_required
from controllers.console.datasets.error import (
from controllers.common.errors import (
FilenameNotExistsError,
FileTooLargeError,
NoFileUploadedError,
TooManyFilesError,
UnsupportedFileTypeError,
)
from controllers.console import api
from controllers.console.admin import admin_required
from controllers.console.error import AccountNotLinkTenantError
from controllers.console.wraps import (
account_initialization_required,

View File

@ -1,7 +0,0 @@
from libs.exception import BaseHTTPException
class UnsupportedFileTypeError(BaseHTTPException):
error_code = "unsupported_file_type"
description = "File type not allowed."
code = 415

View File

@ -5,8 +5,8 @@ from flask_restful import Resource, reqparse
from werkzeug.exceptions import NotFound
import services
from controllers.common.errors import UnsupportedFileTypeError
from controllers.files import api
from controllers.files.error import UnsupportedFileTypeError
from services.account_service import TenantService
from services.file_service import FileService

View File

@ -4,8 +4,8 @@ from flask import Response
from flask_restful import Resource, reqparse
from werkzeug.exceptions import Forbidden, NotFound
from controllers.common.errors import UnsupportedFileTypeError
from controllers.files import api
from controllers.files.error import UnsupportedFileTypeError
from core.tools.signature import verify_tool_file_signature
from core.tools.tool_file_manager import ToolFileManager
from models import db as global_db

View File

@ -5,11 +5,13 @@ from flask_restful import Resource, marshal_with
from werkzeug.exceptions import Forbidden
import services
from controllers.common.errors import (
FileTooLargeError,
UnsupportedFileTypeError,
)
from controllers.console.wraps import setup_required
from controllers.files import api
from controllers.files.error import UnsupportedFileTypeError
from controllers.inner_api.plugin.wraps import get_user
from controllers.service_api.app.error import FileTooLargeError
from core.file.helpers import verify_plugin_file_signature
from core.tools.tool_file_manager import ToolFileManager
from fields.file_fields import file_fields

View File

@ -85,30 +85,6 @@ class ProviderNotSupportSpeechToTextError(BaseHTTPException):
code = 400
class NoFileUploadedError(BaseHTTPException):
error_code = "no_file_uploaded"
description = "Please upload your file."
code = 400
class TooManyFilesError(BaseHTTPException):
error_code = "too_many_files"
description = "Only one file is allowed."
code = 400
class FileTooLargeError(BaseHTTPException):
error_code = "file_too_large"
description = "File size exceeded. {message}"
code = 413
class UnsupportedFileTypeError(BaseHTTPException):
error_code = "unsupported_file_type"
description = "File type not allowed."
code = 415
class FileNotFoundError(BaseHTTPException):
error_code = "file_not_found"
description = "The requested file was not found."

View File

@ -2,14 +2,14 @@ from flask import request
from flask_restful import Resource, marshal_with
import services
from controllers.common.errors import FilenameNotExistsError
from controllers.service_api import api
from controllers.service_api.app.error import (
from controllers.common.errors import (
FilenameNotExistsError,
FileTooLargeError,
NoFileUploadedError,
TooManyFilesError,
UnsupportedFileTypeError,
)
from controllers.service_api import api
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
from fields.file_fields import file_fields
from models.model import App, EndUser

View File

@ -6,15 +6,15 @@ from sqlalchemy import desc, select
from werkzeug.exceptions import Forbidden, NotFound
import services
from controllers.common.errors import FilenameNotExistsError
from controllers.service_api import api
from controllers.service_api.app.error import (
from controllers.common.errors import (
FilenameNotExistsError,
FileTooLargeError,
NoFileUploadedError,
ProviderNotInitializeError,
TooManyFilesError,
UnsupportedFileTypeError,
)
from controllers.service_api import api
from controllers.service_api.app.error import ProviderNotInitializeError
from controllers.service_api.dataset.error import (
ArchivedDocumentImmutableError,
DocumentIndexingError,

View File

@ -1,30 +1,6 @@
from libs.exception import BaseHTTPException
class NoFileUploadedError(BaseHTTPException):
error_code = "no_file_uploaded"
description = "Please upload your file."
code = 400
class TooManyFilesError(BaseHTTPException):
error_code = "too_many_files"
description = "Only one file is allowed."
code = 400
class FileTooLargeError(BaseHTTPException):
error_code = "file_too_large"
description = "File size exceeded. {message}"
code = 413
class UnsupportedFileTypeError(BaseHTTPException):
error_code = "unsupported_file_type"
description = "File type not allowed."
code = 415
class DatasetNotInitializedError(BaseHTTPException):
error_code = "dataset_not_initialized"
description = "The dataset is still being initialized or indexing. Please wait a moment."

View File

@ -97,30 +97,6 @@ class ProviderNotSupportSpeechToTextError(BaseHTTPException):
code = 400
class NoFileUploadedError(BaseHTTPException):
error_code = "no_file_uploaded"
description = "Please upload your file."
code = 400
class TooManyFilesError(BaseHTTPException):
error_code = "too_many_files"
description = "Only one file is allowed."
code = 400
class FileTooLargeError(BaseHTTPException):
error_code = "file_too_large"
description = "File size exceeded. {message}"
code = 413
class UnsupportedFileTypeError(BaseHTTPException):
error_code = "unsupported_file_type"
description = "File type not allowed."
code = 415
class WebAppAuthRequiredError(BaseHTTPException):
error_code = "web_sso_auth_required"
description = "Web app authentication required."

View File

@ -2,8 +2,13 @@ from flask import request
from flask_restful import marshal_with
import services
from controllers.common.errors import FilenameNotExistsError
from controllers.web.error import FileTooLargeError, NoFileUploadedError, TooManyFilesError, UnsupportedFileTypeError
from controllers.common.errors import (
FilenameNotExistsError,
FileTooLargeError,
NoFileUploadedError,
TooManyFilesError,
UnsupportedFileTypeError,
)
from controllers.web.wraps import WebApiResource
from fields.file_fields import file_fields
from services.file_service import FileService

View File

@ -5,15 +5,17 @@ from flask_restful import marshal_with, reqparse
import services
from controllers.common import helpers
from controllers.common.errors import RemoteFileUploadError
from controllers.common.errors import (
FileTooLargeError,
RemoteFileUploadError,
UnsupportedFileTypeError,
)
from controllers.web.wraps import WebApiResource
from core.file import helpers as file_helpers
from core.helper import ssrf_proxy
from fields.file_fields import file_fields_with_signed_url, remote_file_info_fields
from services.file_service import FileService
from .error import FileTooLargeError, UnsupportedFileTypeError
class RemoteFileInfoApi(WebApiResource):
@marshal_with(remote_file_info_fields)

View File

@ -74,6 +74,7 @@ from core.workflow.system_variable import SystemVariable
from core.workflow.workflow_cycle_manager import CycleManagerWorkflowInfo, WorkflowCycleManager
from events.message_event import message_was_created
from extensions.ext_database import db
from libs.datetime_utils import naive_utc_now
from models import Conversation, EndUser, Message, MessageFile
from models.account import Account
from models.enums import CreatorUserRole
@ -896,6 +897,7 @@ class AdvancedChatAppGenerateTaskPipeline:
def _save_message(self, *, session: Session, graph_runtime_state: Optional[GraphRuntimeState] = None) -> None:
message = self._get_message(session=session)
message.answer = self._task_state.answer
message.updated_at = naive_utc_now()
message.provider_response_latency = time.perf_counter() - self._base_task_pipeline._start_at
message.message_metadata = self._task_state.metadata.model_dump_json()
message_files = [

View File

@ -57,6 +57,7 @@ from core.prompt.utils.prompt_message_util import PromptMessageUtil
from core.prompt.utils.prompt_template_parser import PromptTemplateParser
from events.message_event import message_was_created
from extensions.ext_database import db
from libs.datetime_utils import naive_utc_now
from models.model import AppMode, Conversation, Message, MessageAgentThought
logger = logging.getLogger(__name__)
@ -389,6 +390,7 @@ class EasyUIBasedGenerateTaskPipeline(BasedGenerateTaskPipeline):
if llm_result.message.content
else ""
)
message.updated_at = naive_utc_now()
message.answer_tokens = usage.completion_tokens
message.answer_unit_price = usage.completion_unit_price
message.answer_price_unit = usage.completion_price_unit

View File

@ -276,17 +276,26 @@ class Executor:
encoded_credentials = credentials
headers[authorization.config.header] = f"Basic {encoded_credentials}"
elif self.auth.config.type == "custom":
headers[authorization.config.header] = authorization.config.api_key or ""
if authorization.config.header and authorization.config.api_key:
headers[authorization.config.header] = authorization.config.api_key
# Handle Content-Type for multipart/form-data requests
# Fix for issue #22880: Missing boundary when using multipart/form-data
# Fix for issue #23829: Missing boundary when using multipart/form-data
body = self.node_data.body
if body and body.type == "form-data":
# For multipart/form-data with files, let httpx handle the boundary automatically
# by not setting Content-Type header when files are present
if not self.files or all(f[0] == "__multipart_placeholder__" for f in self.files):
# Only set Content-Type when there are no actual files
# This ensures httpx generates the correct boundary
# For multipart/form-data with files (including placeholder files),
# remove any manually set Content-Type header to let httpx handle
# For multipart/form-data, if any files are present (including placeholder files),
# we must remove any manually set Content-Type header. This is because httpx needs to
# automatically set the Content-Type and boundary for multipart encoding whenever files
# are included, even if they are placeholders, to avoid boundary issues and ensure correct
# file upload behaviour. Manually setting Content-Type can cause httpx to fail to set the
# boundary, resulting in invalid requests.
if self.files:
# Remove Content-Type if it was manually set to avoid boundary issues
headers = {k: v for k, v in headers.items() if k.lower() != "content-type"}
else:
# No files at all, set Content-Type manually
if "content-type" not in (k.lower() for k in headers):
headers["Content-Type"] = "multipart/form-data"
elif body and body.type in BODY_TYPE_TO_CONTENT_TYPE:

View File

@ -249,6 +249,8 @@ def _get_remote_file_info(url: str):
# Initialize mime_type from filename as fallback
mime_type, _ = mimetypes.guess_type(filename)
if mime_type is None:
mime_type = ""
resp = ssrf_proxy.head(url, follow_redirects=True)
resp = cast(httpx.Response, resp)
@ -257,7 +259,12 @@ def _get_remote_file_info(url: str):
filename = str(content_disposition.split("filename=")[-1].strip('"'))
# Re-guess mime_type from updated filename
mime_type, _ = mimetypes.guess_type(filename)
if mime_type is None:
mime_type = ""
file_size = int(resp.headers.get("Content-Length", file_size))
# Fallback to Content-Type header if mime_type is still empty
if not mime_type:
mime_type = resp.headers.get("Content-Type", "").split(";")[0].strip()
return mime_type, filename, file_size

View File

@ -160,6 +160,177 @@ def test_custom_authorization_header(setup_http_mock):
assert "?A=b" in data
assert "X-Header: 123" in data
# Custom authorization header should be set (may be masked)
assert "X-Auth:" in data
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
def test_custom_auth_with_empty_api_key_does_not_set_header(setup_http_mock):
"""Test: In custom authentication mode, when the api_key is empty, no header should be set."""
from core.workflow.entities.variable_pool import VariablePool
from core.workflow.nodes.http_request.entities import (
HttpRequestNodeAuthorization,
HttpRequestNodeData,
HttpRequestNodeTimeout,
)
from core.workflow.nodes.http_request.executor import Executor
from core.workflow.system_variable import SystemVariable
# Create variable pool
variable_pool = VariablePool(
system_variables=SystemVariable(user_id="test", files=[]),
user_inputs={},
environment_variables=[],
conversation_variables=[],
)
# Create node data with custom auth and empty api_key
node_data = HttpRequestNodeData(
title="http",
desc="",
url="http://example.com",
method="get",
authorization=HttpRequestNodeAuthorization(
type="api-key",
config={
"type": "custom",
"api_key": "", # Empty api_key
"header": "X-Custom-Auth",
},
),
headers="",
params="",
body=None,
ssl_verify=True,
)
# Create executor
executor = Executor(
node_data=node_data, timeout=HttpRequestNodeTimeout(connect=10, read=30, write=10), variable_pool=variable_pool
)
# Get assembled headers
headers = executor._assembling_headers()
# When api_key is empty, the custom header should NOT be set
assert "X-Custom-Auth" not in headers
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
def test_bearer_authorization_with_custom_header_ignored(setup_http_mock):
"""
Test that when switching from custom to bearer authorization,
the custom header settings don't interfere with bearer token.
This test verifies the fix for issue #23554.
"""
node = init_http_node(
config={
"id": "1",
"data": {
"title": "http",
"desc": "",
"method": "get",
"url": "http://example.com",
"authorization": {
"type": "api-key",
"config": {
"type": "bearer",
"api_key": "test-token",
"header": "", # Empty header - should default to Authorization
},
},
"headers": "",
"params": "",
"body": None,
},
}
)
result = node._run()
assert result.process_data is not None
data = result.process_data.get("request", "")
# In bearer mode, should use Authorization header (value is masked with *)
assert "Authorization: " in data
# Should contain masked Bearer token
assert "*" in data
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
def test_basic_authorization_with_custom_header_ignored(setup_http_mock):
"""
Test that when switching from custom to basic authorization,
the custom header settings don't interfere with basic auth.
This test verifies the fix for issue #23554.
"""
node = init_http_node(
config={
"id": "1",
"data": {
"title": "http",
"desc": "",
"method": "get",
"url": "http://example.com",
"authorization": {
"type": "api-key",
"config": {
"type": "basic",
"api_key": "user:pass",
"header": "", # Empty header - should default to Authorization
},
},
"headers": "",
"params": "",
"body": None,
},
}
)
result = node._run()
assert result.process_data is not None
data = result.process_data.get("request", "")
# In basic mode, should use Authorization header (value is masked with *)
assert "Authorization: " in data
# Should contain masked Basic credentials
assert "*" in data
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
def test_custom_authorization_with_empty_api_key(setup_http_mock):
"""
Test that custom authorization doesn't set header when api_key is empty.
This test verifies the fix for issue #23554.
"""
node = init_http_node(
config={
"id": "1",
"data": {
"title": "http",
"desc": "",
"method": "get",
"url": "http://example.com",
"authorization": {
"type": "api-key",
"config": {
"type": "custom",
"api_key": "", # Empty api_key
"header": "X-Custom-Auth",
},
},
"headers": "",
"params": "",
"body": None,
},
}
)
result = node._run()
assert result.process_data is not None
data = result.process_data.get("request", "")
# Custom header should NOT be set when api_key is empty
assert "X-Custom-Auth:" not in data
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
@ -239,6 +410,7 @@ def test_json(setup_http_mock):
assert "X-Header: 123" in data
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
def test_x_www_form_urlencoded(setup_http_mock):
node = init_http_node(
config={
@ -285,6 +457,7 @@ def test_x_www_form_urlencoded(setup_http_mock):
assert "X-Header: 123" in data
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
def test_form_data(setup_http_mock):
node = init_http_node(
config={
@ -334,6 +507,7 @@ def test_form_data(setup_http_mock):
assert "X-Header: 123" in data
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
def test_none_data(setup_http_mock):
node = init_http_node(
config={
@ -366,6 +540,7 @@ def test_none_data(setup_http_mock):
assert "123123123" not in data
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
def test_mock_404(setup_http_mock):
node = init_http_node(
config={
@ -394,6 +569,7 @@ def test_mock_404(setup_http_mock):
assert "Not Found" in resp.get("body", "")
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
def test_multi_colons_parse(setup_http_mock):
node = init_http_node(
config={

File diff suppressed because it is too large Load Diff

View File

@ -4,8 +4,8 @@ from unittest.mock import patch
import pytest
from werkzeug.exceptions import Forbidden
from controllers.common.errors import FilenameNotExistsError
from controllers.console.error import (
from controllers.common.errors import (
FilenameNotExistsError,
FileTooLargeError,
NoFileUploadedError,
TooManyFilesError,

View File

@ -243,8 +243,6 @@ def test_executor_with_form_data():
# Check the executor's data
assert executor.method == "post"
assert executor.url == "https://api.example.com/upload"
assert "Content-Type" in executor.headers
assert "multipart/form-data" in executor.headers["Content-Type"]
assert executor.params is None
assert executor.json is None
# '__multipart_placeholder__' is expected when no file inputs exist,
@ -252,6 +250,11 @@ def test_executor_with_form_data():
assert executor.files == [("__multipart_placeholder__", ("", b"", "application/octet-stream"))]
assert executor.content is None
# After fix for #23829: When placeholder files exist, Content-Type is removed
# to let httpx handle Content-Type and boundary automatically
headers = executor._assembling_headers()
assert "Content-Type" not in headers or "multipart/form-data" not in headers.get("Content-Type", "")
# Check that the form data is correctly loaded in executor.data
assert isinstance(executor.data, dict)
assert "text_field" in executor.data

View File

@ -3,7 +3,7 @@ import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import AppCard from '@/app/components/app/overview/appCard'
import AppCard from '@/app/components/app/overview/app-card'
import Loading from '@/app/components/base/loading'
import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card'
import { ToastContext } from '@/app/components/base/toast'
@ -17,7 +17,7 @@ import type { App } from '@/types/app'
import type { UpdateAppSiteCodeResponse } from '@/models/app'
import { asyncRunSafe } from '@/utils'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import type { IAppCardProps } from '@/app/components/app/overview/appCard'
import type { IAppCardProps } from '@/app/components/app/overview/app-card'
import { useStore as useAppStore } from '@/app/components/app/store'
export type ICardViewProps = {

View File

@ -3,8 +3,8 @@ import React, { useState } from 'react'
import dayjs from 'dayjs'
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
import { useTranslation } from 'react-i18next'
import type { PeriodParams } from '@/app/components/app/overview/appChart'
import { AvgResponseTime, AvgSessionInteractions, AvgUserInteractions, ConversationsChart, CostChart, EndUsersChart, MessagesChart, TokenPerSecond, UserSatisfactionRate, WorkflowCostChart, WorkflowDailyTerminalsChart, WorkflowMessagesChart } from '@/app/components/app/overview/appChart'
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
import { AvgResponseTime, AvgSessionInteractions, AvgUserInteractions, ConversationsChart, CostChart, EndUsersChart, MessagesChart, TokenPerSecond, UserSatisfactionRate, WorkflowCostChart, WorkflowDailyTerminalsChart, WorkflowMessagesChart } from '@/app/components/app/overview/app-chart'
import type { Item } from '@/app/components/base/select'
import { SimpleSelect } from '@/app/components/base/select'
import { TIME_PERIOD_MAPPING } from '@/app/components/app/log/filter'

View File

@ -35,7 +35,7 @@ import type { AppDetailResponse } from '@/models/app'
import { useAppContext } from '@/context/app-context'
import type { AppSSO } from '@/types/app'
import Indicator from '@/app/components/header/indicator'
import { fetchAppDetail } from '@/service/apps'
import { fetchAppDetailDirect } from '@/service/apps'
import { AccessMode } from '@/models/access-control'
import AccessControl from '../app-access-control'
import { useAppWhiteListSubjects } from '@/service/access-control'
@ -161,11 +161,15 @@ function AppCard({
return
setShowAccessControl(true)
}, [appDetail])
const handleAccessControlUpdate = useCallback(() => {
fetchAppDetail({ url: '/apps', id: appDetail!.id }).then((res) => {
const handleAccessControlUpdate = useCallback(async () => {
try {
const res = await fetchAppDetailDirect({ url: '/apps', id: appDetail!.id })
setAppDetail(res)
setShowAccessControl(false)
})
}
catch (error) {
console.error('Failed to fetch app detail:', error)
}
}, [appDetail, setAppDetail])
return (

View File

@ -123,7 +123,7 @@ const Chart: React.FC<IChartProps> = ({
dimensions: ['date', yField],
source: statistics,
},
grid: { top: 8, right: 36, bottom: 0, left: 0, containLabel: true },
grid: { top: 8, right: 36, bottom: 10, left: 25, containLabel: true },
tooltip: {
trigger: 'item',
position: 'top',
@ -165,7 +165,7 @@ const Chart: React.FC<IChartProps> = ({
lineStyle: {
color: COMMON_COLOR_MAP.splitLineDark,
},
interval(index, value) {
interval(_index, value) {
return !!value
},
},
@ -242,7 +242,7 @@ const Chart: React.FC<IChartProps> = ({
? ''
: <span>{t('appOverview.analysis.tokenUsage.consumed')} Tokens<span className='text-sm'>
<span className='ml-1 text-text-tertiary'>(</span>
<span className='text-orange-400'>~{sum(statistics.map(item => Number.parseFloat(get(item, 'total_price', '0')))).toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 4 })}</span>
<span className='text-orange-400'>~{sum(statistics.map(item => Number.parseFloat(String(get(item, 'total_price', '0'))))).toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 4 })}</span>
<span className='text-text-tertiary'>)</span>
</span></span>}
textStyle={{ main: `!text-3xl !font-normal ${sumData === 0 ? '!text-text-quaternary' : ''}` }} />
@ -268,7 +268,7 @@ export const MessagesChart: FC<IBizChartProps> = ({ id, period }) => {
const noDataFlag = !response.data || response.data.length === 0
return <Chart
basicInfo={{ title: t('appOverview.analysis.totalMessages.title'), explanation: t('appOverview.analysis.totalMessages.explanation'), timePeriod: period.name }}
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) }}
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) } as any}
chartType='messages'
{...(noDataFlag && { yMax: 500 })}
/>
@ -282,7 +282,7 @@ export const ConversationsChart: FC<IBizChartProps> = ({ id, period }) => {
const noDataFlag = !response.data || response.data.length === 0
return <Chart
basicInfo={{ title: t('appOverview.analysis.totalConversations.title'), explanation: t('appOverview.analysis.totalConversations.explanation'), timePeriod: period.name }}
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) }}
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) } as any}
chartType='conversations'
{...(noDataFlag && { yMax: 500 })}
/>
@ -297,7 +297,7 @@ export const EndUsersChart: FC<IBizChartProps> = ({ id, period }) => {
const noDataFlag = !response.data || response.data.length === 0
return <Chart
basicInfo={{ title: t('appOverview.analysis.activeUsers.title'), explanation: t('appOverview.analysis.activeUsers.explanation'), timePeriod: period.name }}
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) }}
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) } as any}
chartType='endUsers'
{...(noDataFlag && { yMax: 500 })}
/>
@ -380,7 +380,7 @@ export const CostChart: FC<IBizChartProps> = ({ id, period }) => {
const noDataFlag = !response.data || response.data.length === 0
return <Chart
basicInfo={{ title: t('appOverview.analysis.tokenUsage.title'), explanation: t('appOverview.analysis.tokenUsage.explanation'), timePeriod: period.name }}
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) }}
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) } as any}
chartType='costs'
{...(noDataFlag && { yMax: 100 })}
/>
@ -394,7 +394,7 @@ export const WorkflowMessagesChart: FC<IBizChartProps> = ({ id, period }) => {
const noDataFlag = !response.data || response.data.length === 0
return <Chart
basicInfo={{ title: t('appOverview.analysis.totalMessages.title'), explanation: t('appOverview.analysis.totalMessages.explanation'), timePeriod: period.name }}
chartData={!noDataFlag ? response : { data: getDefaultChartData({ ...(period.query ?? defaultPeriod), key: 'runs' }) }}
chartData={!noDataFlag ? response : { data: getDefaultChartData({ ...(period.query ?? defaultPeriod), key: 'runs' }) } as any}
chartType='conversations'
valueKey='runs'
{...(noDataFlag && { yMax: 500 })}
@ -410,7 +410,7 @@ export const WorkflowDailyTerminalsChart: FC<IBizChartProps> = ({ id, period })
const noDataFlag = !response.data || response.data.length === 0
return <Chart
basicInfo={{ title: t('appOverview.analysis.activeUsers.title'), explanation: t('appOverview.analysis.activeUsers.explanation'), timePeriod: period.name }}
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) }}
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) } as any}
chartType='endUsers'
{...(noDataFlag && { yMax: 500 })}
/>
@ -425,7 +425,7 @@ export const WorkflowCostChart: FC<IBizChartProps> = ({ id, period }) => {
const noDataFlag = !response.data || response.data.length === 0
return <Chart
basicInfo={{ title: t('appOverview.analysis.tokenUsage.title'), explanation: t('appOverview.analysis.tokenUsage.explanation'), timePeriod: period.name }}
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) }}
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) } as any}
chartType='workflowCosts'
{...(noDataFlag && { yMax: 100 })}
/>

View File

@ -18,3 +18,13 @@
.pluginInstallIcon {
background-image: url(../assets/chromeplugin-install.svg);
}
:global(html[data-theme="dark"]) .iframeIcon,
:global(html[data-theme="dark"]) .scriptsIcon,
:global(html[data-theme="dark"]) .chromePluginIcon {
filter: invert(0.86) hue-rotate(180deg) saturate(0.5) brightness(0.95);
}
:global(html[data-theme="dark"]) .pluginInstallIcon {
filter: invert(0.9);
}

View File

@ -117,7 +117,7 @@ const Flowchart = React.forwardRef((props: {
const [isInitialized, setIsInitialized] = useState(false)
const [currentTheme, setCurrentTheme] = useState<'light' | 'dark'>(props.theme || 'light')
const containerRef = useRef<HTMLDivElement>(null)
const chartId = useRef(`mermaid-chart-${Math.random().toString(36).substr(2, 9)}`).current
const chartId = useRef(`mermaid-chart-${Math.random().toString(36).slice(2, 11)}`).current
const [isLoading, setIsLoading] = useState(true)
const renderTimeoutRef = useRef<NodeJS.Timeout>()
const [errMsg, setErrMsg] = useState('')

View File

@ -259,7 +259,7 @@ function getFullMatchOffset(
): number {
let triggerOffset = offset
for (let i = triggerOffset; i <= entryText.length; i++) {
if (documentText.substr(-i) === entryText.substr(0, i))
if (documentText.slice(-i) === entryText.slice(0, i))
triggerOffset = i
}
return triggerOffset

View File

@ -32,7 +32,7 @@ const GotoAnything: FC<Props> = ({
const { t } = useTranslation()
const [show, setShow] = useState<boolean>(false)
const [searchQuery, setSearchQuery] = useState<string>('')
const [cmdVal, setCmdVal] = useState<string>('')
const [cmdVal, setCmdVal] = useState<string>('_')
const inputRef = useRef<HTMLInputElement>(null)
const handleNavSearch = useCallback((q: string) => {
setShow(true)
@ -120,9 +120,14 @@ const GotoAnything: FC<Props> = ({
},
)
// Prevent automatic selection of the first option when cmdVal is not set
const clearSelection = () => {
setCmdVal('_')
}
const handleCommandSelect = useCallback((commandKey: string) => {
setSearchQuery(`${commandKey} `)
setCmdVal('')
clearSelection()
setTimeout(() => {
inputRef.current?.focus()
}, 0)
@ -233,9 +238,6 @@ const GotoAnything: FC<Props> = ({
inputRef.current?.focus()
})
}
return () => {
setCmdVal('')
}
}, [show])
return (
@ -245,6 +247,7 @@ const GotoAnything: FC<Props> = ({
onClose={() => {
setShow(false)
setSearchQuery('')
clearSelection()
onHide?.()
}}
closable={false}
@ -268,7 +271,7 @@ const GotoAnything: FC<Props> = ({
onChange={(e) => {
setSearchQuery(e.target.value)
if (!e.target.value.startsWith('@'))
setCmdVal('')
clearSelection()
}}
className='flex-1 !border-0 !bg-transparent !shadow-none'
wrapperClassName='flex-1 !border-0 !bg-transparent'
@ -321,40 +324,40 @@ const GotoAnything: FC<Props> = ({
/>
) : (
Object.entries(groupedResults).map(([type, results], groupIndex) => (
<Command.Group key={groupIndex} heading={(() => {
const typeMap: Record<string, string> = {
'app': 'app.gotoAnything.groups.apps',
'plugin': 'app.gotoAnything.groups.plugins',
'knowledge': 'app.gotoAnything.groups.knowledgeBases',
'workflow-node': 'app.gotoAnything.groups.workflowNodes',
}
return t(typeMap[type] || `${type}s`)
})()} className='p-2 capitalize text-text-secondary'>
{results.map(result => (
<Command.Item
key={`${result.type}-${result.id}`}
value={result.title}
className='flex cursor-pointer items-center gap-3 rounded-md p-3 will-change-[background-color] hover:bg-state-base-hover aria-[selected=true]:bg-state-base-hover-alt data-[selected=true]:bg-state-base-hover-alt'
onSelect={() => handleNavigate(result)}
>
{result.icon}
<div className='min-w-0 flex-1'>
<div className='truncate font-medium text-text-secondary'>
{result.title}
</div>
{result.description && (
<div className='mt-0.5 truncate text-xs text-text-quaternary'>
{result.description}
<Command.Group key={groupIndex} heading={(() => {
const typeMap: Record<string, string> = {
'app': 'app.gotoAnything.groups.apps',
'plugin': 'app.gotoAnything.groups.plugins',
'knowledge': 'app.gotoAnything.groups.knowledgeBases',
'workflow-node': 'app.gotoAnything.groups.workflowNodes',
}
return t(typeMap[type] || `${type}s`)
})()} className='p-2 capitalize text-text-secondary'>
{results.map(result => (
<Command.Item
key={`${result.type}-${result.id}`}
value={result.title}
className='flex cursor-pointer items-center gap-3 rounded-md p-3 will-change-[background-color] aria-[selected=true]:bg-state-base-hover data-[selected=true]:bg-state-base-hover'
onSelect={() => handleNavigate(result)}
>
{result.icon}
<div className='min-w-0 flex-1'>
<div className='truncate font-medium text-text-secondary'>
{result.title}
</div>
)}
</div>
<div className='text-xs capitalize text-text-quaternary'>
{result.type}
</div>
</Command.Item>
))}
</Command.Group>
))
{result.description && (
<div className='mt-0.5 truncate text-xs text-text-quaternary'>
{result.description}
</div>
)}
</div>
<div className='text-xs capitalize text-text-quaternary'>
{result.type}
</div>
</Command.Item>
))}
</Command.Group>
))
)}
{!isCommandsMode && emptyResult}
{!isCommandsMode && defaultUI}
@ -373,7 +376,7 @@ const GotoAnything: FC<Props> = ({
{t('app.gotoAnything.resultCount', { count: searchResults.length })}
{searchMode !== 'general' && (
<span className='ml-2 opacity-60'>
{t('app.gotoAnything.inScope', { scope: searchMode.replace('@', '') })}
{t('app.gotoAnything.inScope', { scope: searchMode.replace('@', '') })}
</span>
)}
</>

View File

@ -9,7 +9,7 @@ export default function RoutePrefixHandle() {
const handleRouteChange = () => {
const addPrefixToImg = (e: HTMLImageElement) => {
const url = new URL(e.src)
const prefix = url.pathname.substr(0, basePath.length)
const prefix = url.pathname.slice(0, basePath.length)
if (prefix !== basePath && !url.href.startsWith('blob:') && !url.href.startsWith('data:')) {
url.pathname = basePath + url.pathname
e.src = url.toString()

View File

@ -1,6 +1,7 @@
'use client'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useFavicon, useTitle } from 'ahooks'
import { basePath } from '@/utils/var'
export default function useDocumentTitle(title: string) {
const isPending = useGlobalPublicStore(s => s.isGlobalPending)
@ -15,7 +16,7 @@ export default function useDocumentTitle(title: string) {
}
else {
titleStr = `${prefix}Dify`
favicon = '/favicon.ico'
favicon = `${basePath}/favicon.ico`
}
}
useTitle(titleStr)

View File

@ -256,11 +256,11 @@ const translation = {
maxActiveRequestsTip: 'Maximale Anzahl gleichzeitiger aktiver Anfragen pro App (0 für unbegrenzt)',
gotoAnything: {
actions: {
searchPlugins: 'Such-Plugins',
searchPlugins: 'Plugins durchsuchen',
searchKnowledgeBases: 'Wissensdatenbanken durchsuchen',
searchWorkflowNodes: 'Workflow-Knoten durchsuchen',
searchKnowledgeBasesDesc: 'Suchen und navigieren Sie zu Ihren Wissensdatenbanken',
searchApplications: 'Anwendungen suchen',
searchApplications: 'Anwendungen durchsuchen',
searchWorkflowNodesHelp: 'Diese Funktion funktioniert nur, wenn ein Workflow angezeigt wird. Navigieren Sie zuerst zu einem Workflow.',
searchApplicationsDesc: 'Suchen und navigieren Sie zu Ihren Anwendungen',
searchPluginsDesc: 'Suchen und navigieren Sie zu Ihren Plugins',

View File

@ -254,10 +254,10 @@ const translation = {
maxActiveRequestsTip: 'Número máximo de solicitudes activas concurrentes por aplicación (0 para ilimitado)',
gotoAnything: {
actions: {
searchApplications: 'Aplicaciones de búsqueda',
searchApplications: 'Buscar aplicaciones',
searchKnowledgeBasesDesc: 'Busque y navegue por sus bases de conocimiento',
searchWorkflowNodes: 'Buscar nodos de flujo de trabajo',
searchPlugins: 'Complementos de búsqueda',
searchPlugins: 'Buscar complementos',
searchWorkflowNodesDesc: 'Buscar y saltar a nodos en el flujo de trabajo actual por nombre o tipo',
searchKnowledgeBases: 'Buscar en las bases de conocimiento',
searchApplicationsDesc: 'Buscar y navegar a sus aplicaciones',

View File

@ -254,8 +254,8 @@ const translation = {
maxActiveRequestsTip: 'حداکثر تعداد درخواست‌های فعال همزمان در هر برنامه (0 برای نامحدود)',
gotoAnything: {
actions: {
searchPlugins: 'افزونه های جستجو',
searchWorkflowNodes: 'گره های گردش کار جستجو',
searchPlugins: 'جستجوی افزونه ها',
searchWorkflowNodes: 'جستجوی گره های گردش کار',
searchApplications: 'جستجوی برنامه ها',
searchKnowledgeBases: 'جستجو در پایگاه های دانش',
searchWorkflowNodesHelp: 'این ویژگی فقط هنگام مشاهده گردش کار کار می کند. ابتدا به گردش کار بروید.',

View File

@ -58,7 +58,7 @@ const translation = {
appCreateDSLErrorTitle: 'Incompatibilité de version',
appCreateDSLErrorPart3: 'Version actuelle de lapplication DSL :',
appCreateDSLErrorPart2: 'Voulez-vous continuer ?',
foundResults: '{{compte}} Résultats',
foundResults: '{{count}} Résultats',
workflowShortDescription: 'Flux agentique pour automatisations intelligentes',
agentShortDescription: 'Agent intelligent avec raisonnement et utilisation autonome de loutil',
learnMore: 'Pour en savoir plus',
@ -75,7 +75,7 @@ const translation = {
completionUserDescription: 'Créez rapidement un assistant IA pour les tâches de génération de texte avec une configuration simple.',
agentUserDescription: 'Un agent intelligent capable dun raisonnement itératif et dune utilisation autonome doutils pour atteindre les objectifs de la tâche.',
forBeginners: 'Types dapplications plus basiques',
foundResult: '{{compte}} Résultat',
foundResult: '{{count}} Résultat',
noIdeaTip: 'Pas didées ? Consultez nos modèles',
optional: 'Optionnel',
advancedShortDescription: 'Workflow amélioré pour conversations multi-tours',
@ -258,7 +258,7 @@ const translation = {
searchKnowledgeBasesDesc: 'Recherchez et accédez à vos bases de connaissances',
searchWorkflowNodesDesc: 'Recherchez et accédez aux nœuds du flux de travail actuel par nom ou type',
searchApplicationsDesc: 'Recherchez et accédez à vos applications',
searchPlugins: 'Plugins de recherche',
searchPlugins: 'Rechercher des plugins',
searchWorkflowNodes: 'Rechercher des nœuds de workflow',
searchKnowledgeBases: 'Rechercher dans les bases de connaissances',
searchApplications: 'Rechercher des applications',

View File

@ -162,7 +162,7 @@ const translation = {
general: 'Généralités',
fullDocTip: 'Lintégralité du document est utilisée comme morceau parent et récupérée directement. Veuillez noter que pour des raisons de performance, le texte dépassant 10000 jetons sera automatiquement tronqué.',
fullDoc: 'Doc complet',
previewChunkCount: '{{compte}} Tronçons estimés',
previewChunkCount: '{{count}} Tronçons estimés',
childChunkForRetrieval: 'Child-chunk pour lextraction',
parentChildDelimiterTip: 'Un délimiteur est le caractère utilisé pour séparer le texte. \\n\\n est recommandé pour diviser le document dorigine en gros morceaux parents. Vous pouvez également utiliser des délimiteurs spéciaux définis par vous-même.',
qaSwitchHighQualityTipTitle: 'Le format Q&R nécessite une méthode dindexation de haute qualité',

View File

@ -749,8 +749,8 @@ const translation = {
continueOnError: 'continuer sur lerreur',
},
comma: ',',
error_one: '{{compte}} Erreur',
error_other: '{{compte}} Erreurs',
error_one: '{{count}} Erreur',
error_other: '{{count}} Erreurs',
parallelModeEnableDesc: 'En mode parallèle, les tâches au sein des itérations prennent en charge lexécution parallèle. Vous pouvez le configurer dans le panneau des propriétés à droite.',
parallelModeUpper: 'MODE PARALLÈLE',
parallelPanelDesc: 'En mode parallèle, les tâches de litération prennent en charge lexécution parallèle.',

View File

@ -254,28 +254,28 @@ const translation = {
maxActiveRequestsTip: 'प्रति ऐप सक्रिय अनुरोधों की अधिकतम संख्या (असीमित के लिए 0)',
gotoAnything: {
actions: {
searchPlugins: 'खोज प्लगइन्स',
searchWorkflowNodes: 'खोज कार्यप्रवाह नोड्स',
searchPlugins: 'प्लगइन्स खोजें',
searchWorkflowNodes: 'कार्यप्रवाह नोड्स खोजें',
searchKnowledgeBases: 'ज्ञान आधार खोजें',
searchApplications: 'अनुसंधान एप्लिकेशन',
searchApplications: 'एप्लिकेशन खोजें',
searchPluginsDesc: 'अपने प्लगइन्स को खोजें और नेविगेट करें',
searchWorkflowNodesDesc: 'वर्तमान कार्यप्रवाह में नाम या प्रकार द्वारा नोड्स को खोजें और उन पर कूदें',
searchKnowledgeBasesDesc: 'अपने ज्ञान आधारों की खोज करें और उन्हें नेविगेट करें',
searchApplicationsDesc: 'अपने अनुप्रयोगों की खोज करें और उन्हें नेविगेट करें',
searchWorkflowNodesHelp: 'यह सुविधा केवल तब काम करती है जब आप एक कार्यप्रवाह देख रहे हों। पहले एक कार्यप्रवाह पर जाएं।',
themeCategoryTitle: 'थीम',
runTitle: 'आदेश',
runTitle: 'कमांड',
languageCategoryTitle: 'भाषा',
languageCategoryDesc: 'इंटरफेस भाषा बदलें',
themeSystem: 'सिस्टम थीम',
themeLight: 'लाइट थीम',
themeDarkDesc: 'अंधेरे रूप का उपयोग करें',
themeDarkDesc: 'डार्क उपस्थिति का प्रयोग करें',
themeDark: 'डार्क थीम',
themeLightDesc: 'हल्की उपस्थिति का प्रयोग करें',
languageChangeDesc: 'यूआई भाषा बदलें',
themeCategoryDesc: 'ऐप्लिकेशन थीम बदलें',
languageChangeDesc: 'इंटरफेस भाषा बदलें',
themeCategoryDesc: 'ऐप की थीम बदलें',
themeSystemDesc: 'अपने ऑपरेटिंग सिस्टम की उपस्थिति का पालन करें',
runDesc: 'त्वरित आदेश चलाएँ (थीम, भाषा, ...)',
runDesc: 'त्वरित कमांड चलाएँ (थीम, भाषा, ...)',
},
emptyState: {
noPluginsFound: 'कोई प्लगइन नहीं मिले',

View File

@ -266,7 +266,7 @@ const translation = {
searchApplications: 'Cerca applicazioni',
searchPluginsDesc: 'Cerca e naviga verso i tuoi plugin',
searchKnowledgeBasesDesc: 'Cerca e naviga nelle tue knowledge base',
searchPlugins: 'Plugin di ricerca',
searchPlugins: 'Cerca plugin',
searchWorkflowNodesDesc: 'Trovare e passare ai nodi nel flusso di lavoro corrente in base al nome o al tipo',
searchKnowledgeBases: 'Cerca nelle Basi di Conoscenza',
themeCategoryTitle: 'Tema',

View File

@ -258,11 +258,11 @@ const translation = {
searchApplicationsDesc: 'Pesquise e navegue até seus aplicativos',
searchPluginsDesc: 'Pesquise e navegue até seus plug-ins',
searchKnowledgeBases: 'Pesquisar bases de conhecimento',
searchApplications: 'Aplicativos de pesquisa',
searchApplications: 'Pesquisar aplicativos',
searchWorkflowNodesDesc: 'Localizar e ir para nós no fluxo de trabalho atual por nome ou tipo',
searchWorkflowNodesHelp: 'Esse recurso só funciona ao visualizar um fluxo de trabalho. Navegue até um fluxo de trabalho primeiro.',
searchKnowledgeBasesDesc: 'Pesquise e navegue até suas bases de conhecimento',
searchWorkflowNodes: 'Nós de fluxo de trabalho de pesquisa',
searchWorkflowNodes: 'Pesquisar nós de fluxo de trabalho',
themeDarkDesc: 'Use aparência escura',
themeCategoryDesc: 'Mudar o tema do aplicativo',
themeLight: 'Tema Claro',

View File

@ -254,7 +254,7 @@ const translation = {
maxActiveRequestsTip: 'Максимальное количество одновременно активных запросов на одно приложение (0 для неограниченного количества)',
gotoAnything: {
actions: {
searchPlugins: 'Поисковые плагины',
searchPlugins: 'Поиск плагинов',
searchKnowledgeBases: 'Поиск в базах знаний',
searchApplications: 'Поиск приложений',
searchKnowledgeBasesDesc: 'Поиск и переход к базам знаний',
@ -269,11 +269,11 @@ const translation = {
themeCategoryTitle: 'Тема',
languageCategoryTitle: 'Язык',
themeSystem: 'Системная тема',
runDesc: 'Запустите быстрые команды (тема, язык, ...)',
runDesc: 'Запустите быстрые команды (тема, язык, )',
themeLight: 'Светлая тема',
themeDarkDesc: 'Используйте темный внешний вид',
languageChangeDesc: 'Изменить язык интерфейса',
languageCategoryDesc: 'Переключить язык интерфейса',
languageChangeDesc: 'Измените язык интерфейса',
languageCategoryDesc: 'Переключите язык интерфейса',
themeLightDesc: 'Используйте светлый внешний вид',
themeSystemDesc: 'Следуйте внешнему виду вашей операционной системы',
},

View File

@ -258,7 +258,7 @@ const translation = {
searchKnowledgeBasesDesc: 'Iskanje in krmarjenje do zbirk znanja',
searchWorkflowNodesHelp: 'Ta funkcija deluje le pri ogledu poteka dela. Najprej se pomaknite do poteka dela.',
searchApplicationsDesc: 'Iskanje in krmarjenje do aplikacij',
searchPlugins: 'Iskalni vtičniki',
searchPlugins: 'Iskanje vtičnikov',
searchApplications: 'Iskanje aplikacij',
searchWorkflowNodesDesc: 'Iskanje vozlišč in skok nanje v trenutnem poteku dela po imenu ali vrsti',
searchKnowledgeBases: 'Iskanje po zbirkah znanja',

View File

@ -37,11 +37,11 @@ const translation = {
captionName: 'ไอคอนและชื่อโปรเจกต์',
appNamePlaceholder: 'ตั้งชื่อโปรเจกต์ของคุณ',
captionDescription: 'คำอธิบาย',
appDescriptionPlaceholder: 'ป้อนคําอธิบายของโปรเจกต์',
appDescriptionPlaceholder: 'ป้อนคอธิบายของโปรเจกต์',
useTemplate: 'ใช้เทมเพลตนี้',
previewDemo: 'ตัวอย่างการใช้งาน',
chatApp: 'ผู้ช่วย',
chatAppIntro: 'ฉันต้องการสร้างโปรเจกต์ ที่เป็นแอปพลิเคชันที่ใช้การแชท โปรเจกต์นี้ใช้รูปแบบคําถามและคําตอบ ทําให้สามารถสนทนาต่อเนื่องได้หลายรอบ(Multi-turn)',
chatAppIntro: 'ฉันต้องการสร้างโปรเจกต์ ที่เป็นแอปพลิเคชันที่ใช้การแชท โปรเจกต์นี้ใช้รูปแบบคำถามและคำตอบ ทําให้สามารถสนทนาต่อเนื่องได้หลายรอบ(Multi-turn)',
agentAssistant: 'ผู้ช่วยใหม่',
completeApp: 'เครื่องมือสร้างข้อความ',
completeAppIntro: 'ฉันต้องการสร้างโปรเจกต์ที่ ที่สามารถสร้างข้อความคุณภาพสูงตามข้อความแจ้ง เช่น การสร้างบทความ สรุป การแปล และอื่นๆ',
@ -294,7 +294,7 @@ const translation = {
searchTemporarilyUnavailable: 'การค้นหาไม่พร้อมใช้งานชั่วคราว',
someServicesUnavailable: 'บริการค้นหาบางบริการไม่พร้อมใช้งาน',
clearToSearchAll: 'ล้าง @ เพื่อค้นหาทั้งหมด',
searchPlaceholder: 'ค้นหาหรือพิมพ์ @ สําหรับคําสั่ง...',
searchPlaceholder: 'ค้นหาหรือพิมพ์ @ สำหรับคำสั่ง...',
servicesUnavailableMessage: 'บริการค้นหาบางบริการอาจประสบปัญหา ลองอีกครั้งในอีกสักครู่',
searching: 'กำลังค้นหา...',
searchHint: 'เริ่มพิมพ์เพื่อค้นหาทุกอย่างได้ทันที',
@ -303,7 +303,7 @@ const translation = {
resultCount: '{{count}} ผลลัพธ์',
resultCount_other: '{{count}} ผลลัพธ์',
inScope: 'ใน {{scope}}s',
noMatchingCommands: 'ไม่พบคําสั่งที่ตรงกัน',
noMatchingCommands: 'ไม่พบคสั่งที่ตรงกัน',
tryDifferentSearch: 'ลองใช้ข้อความค้นหาอื่น',
},
}

View File

@ -252,11 +252,10 @@ const translation = {
actions: {
searchKnowledgeBasesDesc: 'Bilgi bankalarınızda arama yapın ve bu forumlara gidin',
searchWorkflowNodesDesc: 'Geçerli iş akışındaki düğümleri ada veya türe göre bulun ve atlayın',
searchApplications: 'Arama Uygulamaları',
searchApplications: 'Uygulamaları Ara',
searchKnowledgeBases: 'Bilgi Bankalarında Ara',
searchWorkflowNodes: 'Arama İş Akışı Düğümleri',
searchPluginsDesc: 'Eklentilerinizi arayın ve eklentilerinize gidin',
searchPlugins: 'Arama Eklentileri',
searchWorkflowNodes: 'İş Akışı Düğümlerini Ara',
searchPlugins: 'Eklentileri Ara',
searchWorkflowNodesHelp: 'Bu özellik yalnızca bir iş akışını görüntülerken çalışır. Önce bir iş akışına gidin.',
searchApplicationsDesc: 'Uygulamalarınızı arayın ve uygulamalarınıza gidin',
languageChangeDesc: 'UI dilini değiştir',

View File

@ -256,22 +256,22 @@ const translation = {
actions: {
searchApplications: 'Пошук додатків',
searchKnowledgeBases: 'Пошук по базах знань',
searchWorkflowNodes: 'Вузли документообігу пошуку',
searchWorkflowNodes: 'Пошук вузлів робочого процесу',
searchApplicationsDesc: 'Шукайте та переходьте до своїх програм',
searchPluginsDesc: 'Пошук і навігація до ваших плагінів',
searchWorkflowNodesHelp: 'Ця функція працює лише під час перегляду робочого процесу. Спочатку перейдіть до робочого процесу.',
searchPlugins: 'Пошукові плагіни',
searchPlugins: 'Пошук плагінів',
searchKnowledgeBasesDesc: 'Шукайте та переходьте до своїх баз знань',
searchWorkflowNodesDesc: 'Знаходьте вузли в поточному робочому процесі та переходьте до них за іменем або типом',
themeSystem: 'Тема системи',
themeSystem: 'Системна тема',
languageCategoryTitle: 'Мова',
themeCategoryTitle: 'Тема',
themeLight: 'Світла тема',
runTitle: 'Команди',
languageChangeDesc: 'Змінити мову інтерфейсу',
themeDark: 'Темний режим',
themeDark: 'Темна тема',
themeDarkDesc: 'Використовуйте темний режим',
runDesc: 'Run quick commands (theme, language, ...)',
runDesc: 'Запустіть швидкі команди (тема, мова, ...)',
themeCategoryDesc: 'Переключити тему застосунку',
themeLightDesc: 'Використовуйте світлий вигляд',
themeSystemDesc: 'Дотримуйтесь зовнішнього вигляду вашої операційної системи',

View File

@ -162,7 +162,7 @@ async function base<T>(url: string, options: FetchOptionType = {}, otherOptions:
...baseHooks.beforeRequest || [],
isPublicAPI && beforeRequestPublicAuthorization,
!isPublicAPI && !isMarketplaceAPI && beforeRequestAuthorization,
].filter(Boolean),
].filter((h): h is BeforeRequestHook => Boolean(h)),
afterResponse: [
...baseHooks.afterResponse || [],
afterResponseErrorCode(otherOptions),

View File

@ -7,7 +7,6 @@ export const mergeValidCompletionParams = (
if (!oldParams || Object.keys(oldParams).length === 0)
return { params: {}, removedDetails: {} }
const acceptedKeys = new Set(rules.map(r => r.name))
const ruleMap: Record<string, ModelParameterRule> = {}
rules.forEach((r) => {
ruleMap[r.name] = r
@ -17,11 +16,6 @@ export const mergeValidCompletionParams = (
const removedDetails: Record<string, string> = {}
Object.entries(oldParams).forEach(([key, value]) => {
if (!acceptedKeys.has(key)) {
removedDetails[key] = 'unsupported'
return
}
const rule = ruleMap[key]
if (!rule) {
removedDetails[key] = 'unsupported'