mirror of https://github.com/langgenius/dify.git
Merge branch 'main' into feat/rag-2
This commit is contained in:
commit
02720c9b95
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
from libs.exception import BaseHTTPException
|
||||
|
||||
|
||||
class UnsupportedFileTypeError(BaseHTTPException):
|
||||
error_code = "unsupported_file_type"
|
||||
description = "File type not allowed."
|
||||
code = 415
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
@ -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 })}
|
||||
/>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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('')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -254,8 +254,8 @@ const translation = {
|
|||
maxActiveRequestsTip: 'حداکثر تعداد درخواستهای فعال همزمان در هر برنامه (0 برای نامحدود)',
|
||||
gotoAnything: {
|
||||
actions: {
|
||||
searchPlugins: 'افزونه های جستجو',
|
||||
searchWorkflowNodes: 'گره های گردش کار جستجو',
|
||||
searchPlugins: 'جستجوی افزونه ها',
|
||||
searchWorkflowNodes: 'جستجوی گره های گردش کار',
|
||||
searchApplications: 'جستجوی برنامه ها',
|
||||
searchKnowledgeBases: 'جستجو در پایگاه های دانش',
|
||||
searchWorkflowNodesHelp: 'این ویژگی فقط هنگام مشاهده گردش کار کار می کند. ابتدا به گردش کار بروید.',
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ const translation = {
|
|||
appCreateDSLErrorTitle: 'Incompatibilité de version',
|
||||
appCreateDSLErrorPart3: 'Version actuelle de l’application 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 l’outil',
|
||||
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 d’un raisonnement itératif et d’une utilisation autonome d’outils pour atteindre les objectifs de la tâche.',
|
||||
forBeginners: 'Types d’applications plus basiques',
|
||||
foundResult: '{{compte}} Résultat',
|
||||
foundResult: '{{count}} Résultat',
|
||||
noIdeaTip: 'Pas d’idé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',
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ const translation = {
|
|||
general: 'Généralités',
|
||||
fullDocTip: 'L’inté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 l’extraction',
|
||||
parentChildDelimiterTip: 'Un délimiteur est le caractère utilisé pour séparer le texte. \\n\\n est recommandé pour diviser le document d’origine 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 d’indexation de haute qualité',
|
||||
|
|
|
|||
|
|
@ -749,8 +749,8 @@ const translation = {
|
|||
continueOnError: 'continuer sur l’erreur',
|
||||
},
|
||||
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 l’exé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 l’itération prennent en charge l’exécution parallèle.',
|
||||
|
|
|
|||
|
|
@ -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: 'कोई प्लगइन नहीं मिले',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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: 'Следуйте внешнему виду вашей операционной системы',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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: 'ลองใช้ข้อความค้นหาอื่น',
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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: 'Дотримуйтесь зовнішнього вигляду вашої операційної системи',
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
Loading…
Reference in New Issue