mirror of
https://github.com/langgenius/dify.git
synced 2024-11-16 11:42:29 +08:00
Merge branch 'feat/add-remote-file-upload-api' into deploy/dev
Some checks failed
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Has been cancelled
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Has been cancelled
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Has been cancelled
Some checks failed
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Has been cancelled
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Has been cancelled
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Has been cancelled
This commit is contained in:
commit
0c67c35f81
|
@ -1,5 +1,9 @@
|
|||
![cover-v5-optimized](https://github.com/langgenius/dify/assets/13230914/f9e19af5-61ba-4119-b926-d10c4c06ebab)
|
||||
|
||||
<p align="center">
|
||||
📌 <a href="https://dify.ai/blog/introducing-dify-workflow-file-upload-a-demo-on-ai-podcast">Introducing Dify Workflow File Upload: Recreate Google NotebookLM Podcast</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cloud.dify.ai">Dify Cloud</a> ·
|
||||
<a href="https://docs.dify.ai/getting-started/install-self-hosted">Self-hosting</a> ·
|
||||
|
|
6
api/controllers/common/errors.py
Normal file
6
api/controllers/common/errors.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
|
||||
class FilenameNotExistsError(HTTPException):
|
||||
code = 400
|
||||
description = "The specified filename does not exist."
|
|
@ -2,9 +2,21 @@ from flask import Blueprint
|
|||
|
||||
from libs.external_api import ExternalApi
|
||||
|
||||
from .files import FileApi, FilePreviewApi, FileSupportTypeApi
|
||||
from .remote_files import RemoteFileInfoApi, RemoteFileUploadApi
|
||||
|
||||
bp = Blueprint("console", __name__, url_prefix="/console/api")
|
||||
api = ExternalApi(bp)
|
||||
|
||||
# File
|
||||
api.add_resource(FileApi, "/files/upload")
|
||||
api.add_resource(FilePreviewApi, "/files/<uuid:file_id>/preview")
|
||||
api.add_resource(FileSupportTypeApi, "/files/support-type")
|
||||
|
||||
# Remote files
|
||||
api.add_resource(RemoteFileInfoApi, "/remote-files/<path:url>")
|
||||
api.add_resource(RemoteFileUploadApi, "/remote-files/upload")
|
||||
|
||||
# Import other controllers
|
||||
from . import admin, apikey, extension, feature, ping, setup, version
|
||||
|
||||
|
@ -43,7 +55,6 @@ from .datasets import (
|
|||
datasets_document,
|
||||
datasets_segments,
|
||||
external,
|
||||
file,
|
||||
hit_testing,
|
||||
website,
|
||||
)
|
||||
|
|
|
@ -10,8 +10,7 @@ from models.dataset import Dataset
|
|||
from models.model import ApiToken, App
|
||||
|
||||
from . import api
|
||||
from .setup import setup_required
|
||||
from .wraps import account_initialization_required
|
||||
from .wraps import account_initialization_required, setup_required
|
||||
|
||||
api_key_fields = {
|
||||
"id": fields.String,
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
from flask_restful import Resource, reqparse
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from libs.login import login_required
|
||||
from services.advanced_prompt_template_service import AdvancedPromptTemplateService
|
||||
|
||||
|
|
|
@ -2,8 +2,7 @@ from flask_restful import Resource, reqparse
|
|||
|
||||
from controllers.console import api
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from libs.helper import uuid_value
|
||||
from libs.login import login_required
|
||||
from models.model import AppMode
|
||||
|
|
|
@ -6,8 +6,11 @@ from werkzeug.exceptions import Forbidden
|
|||
from controllers.console import api
|
||||
from controllers.console.app.error import NoFileUploadedError
|
||||
from controllers.console.datasets.error import TooManyFilesError
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_resource_check,
|
||||
setup_required,
|
||||
)
|
||||
from extensions.ext_redis import redis_client
|
||||
from fields.annotation_fields import (
|
||||
annotation_fields,
|
||||
|
|
|
@ -6,8 +6,11 @@ from werkzeug.exceptions import BadRequest, Forbidden, abort
|
|||
|
||||
from controllers.console import api
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_resource_check,
|
||||
setup_required,
|
||||
)
|
||||
from core.ops.ops_trace_manager import OpsTraceManager
|
||||
from fields.app_fields import (
|
||||
app_detail_fields,
|
||||
|
|
|
@ -18,8 +18,7 @@ from controllers.console.app.error import (
|
|||
UnsupportedAudioTypeError,
|
||||
)
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
|
||||
from core.model_runtime.errors.invoke import InvokeError
|
||||
from libs.login import login_required
|
||||
|
|
|
@ -15,8 +15,7 @@ from controllers.console.app.error import (
|
|||
ProviderQuotaExceededError,
|
||||
)
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
|
||||
from core.app.apps.base_app_queue_manager import AppQueueManager
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
|
|
|
@ -10,8 +10,7 @@ from werkzeug.exceptions import Forbidden, NotFound
|
|||
|
||||
from controllers.console import api
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from extensions.ext_database import db
|
||||
from fields.conversation_fields import (
|
||||
|
|
|
@ -4,8 +4,7 @@ from sqlalchemy.orm import Session
|
|||
|
||||
from controllers.console import api
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from extensions.ext_database import db
|
||||
from fields.conversation_variable_fields import paginated_conversation_variable_fields
|
||||
from libs.login import login_required
|
||||
|
|
|
@ -10,8 +10,7 @@ from controllers.console.app.error import (
|
|||
ProviderNotInitializeError,
|
||||
ProviderQuotaExceededError,
|
||||
)
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
|
||||
from core.llm_generator.llm_generator import LLMGenerator
|
||||
from core.model_runtime.errors.invoke import InvokeError
|
||||
|
|
|
@ -14,8 +14,11 @@ from controllers.console.app.error import (
|
|||
)
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.explore.error import AppSuggestedQuestionsAfterAnswerDisabledError
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_resource_check,
|
||||
setup_required,
|
||||
)
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
|
||||
from core.model_runtime.errors.invoke import InvokeError
|
||||
|
|
|
@ -6,8 +6,7 @@ from flask_restful import Resource
|
|||
|
||||
from controllers.console import api
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from core.agent.entities import AgentToolEntity
|
||||
from core.tools.tool_manager import ToolManager
|
||||
from core.tools.utils.configuration import ToolParameterConfigurationManager
|
||||
|
|
|
@ -2,8 +2,7 @@ from flask_restful import Resource, reqparse
|
|||
|
||||
from controllers.console import api
|
||||
from controllers.console.app.error import TracingConfigCheckError, TracingConfigIsExist, TracingConfigNotExist
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from libs.login import login_required
|
||||
from services.ops_service import OpsService
|
||||
|
||||
|
|
|
@ -7,8 +7,7 @@ from werkzeug.exceptions import Forbidden, NotFound
|
|||
from constants.languages import supported_language
|
||||
from controllers.console import api
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from extensions.ext_database import db
|
||||
from fields.app_fields import app_site_fields
|
||||
from libs.login import login_required
|
||||
|
|
|
@ -8,8 +8,7 @@ from flask_restful import Resource, reqparse
|
|||
|
||||
from controllers.console import api
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from extensions.ext_database import db
|
||||
from libs.helper import DatetimeString
|
||||
from libs.login import login_required
|
||||
|
|
|
@ -9,8 +9,7 @@ import services
|
|||
from controllers.console import api
|
||||
from controllers.console.app.error import ConversationCompletedError, DraftWorkflowNotExist, DraftWorkflowNotSync
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from core.app.apps.base_app_queue_manager import AppQueueManager
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from factories import variable_factory
|
||||
|
|
|
@ -3,8 +3,7 @@ from flask_restful.inputs import int_range
|
|||
|
||||
from controllers.console import api
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from fields.workflow_app_log_fields import workflow_app_log_pagination_fields
|
||||
from libs.login import login_required
|
||||
from models import App
|
||||
|
|
|
@ -3,8 +3,7 @@ from flask_restful.inputs import int_range
|
|||
|
||||
from controllers.console import api
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from fields.workflow_run_fields import (
|
||||
advanced_chat_workflow_run_pagination_fields,
|
||||
workflow_run_detail_fields,
|
||||
|
|
|
@ -8,8 +8,7 @@ from flask_restful import Resource, reqparse
|
|||
|
||||
from controllers.console import api
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from extensions.ext_database import db
|
||||
from libs.helper import DatetimeString
|
||||
from libs.login import login_required
|
||||
|
|
|
@ -7,8 +7,7 @@ from controllers.console.auth.error import ApiKeyAuthFailedError
|
|||
from libs.login import login_required
|
||||
from services.auth.api_key_auth_service import ApiKeyAuthService
|
||||
|
||||
from ..setup import setup_required
|
||||
from ..wraps import account_initialization_required
|
||||
from ..wraps import account_initialization_required, setup_required
|
||||
|
||||
|
||||
class ApiKeyAuthDataSource(Resource):
|
||||
|
|
|
@ -11,8 +11,7 @@ from controllers.console import api
|
|||
from libs.login import login_required
|
||||
from libs.oauth_data_source import NotionOAuth
|
||||
|
||||
from ..setup import setup_required
|
||||
from ..wraps import account_initialization_required
|
||||
from ..wraps import account_initialization_required, setup_required
|
||||
|
||||
|
||||
def get_oauth_providers():
|
||||
|
|
|
@ -13,7 +13,7 @@ from controllers.console.auth.error import (
|
|||
PasswordMismatchError,
|
||||
)
|
||||
from controllers.console.error import EmailSendIpLimitError, NotAllowedRegister
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import setup_required
|
||||
from events.tenant_event import tenant_was_created
|
||||
from extensions.ext_database import db
|
||||
from libs.helper import email, extract_remote_ip
|
||||
|
|
|
@ -20,7 +20,7 @@ from controllers.console.error import (
|
|||
NotAllowedCreateWorkspace,
|
||||
NotAllowedRegister,
|
||||
)
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import setup_required
|
||||
from events.tenant_event import tenant_was_created
|
||||
from libs.helper import email, extract_remote_ip
|
||||
from libs.password import valid_password
|
||||
|
|
|
@ -2,8 +2,7 @@ from flask_login import current_user
|
|||
from flask_restful import Resource, reqparse
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required, only_edition_cloud
|
||||
from controllers.console.wraps import account_initialization_required, only_edition_cloud, setup_required
|
||||
from libs.login import login_required
|
||||
from services.billing_service import BillingService
|
||||
|
||||
|
|
|
@ -7,8 +7,7 @@ from flask_restful import Resource, marshal_with, reqparse
|
|||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from core.indexing_runner import IndexingRunner
|
||||
from core.rag.extractor.entity.extract_setting import ExtractSetting
|
||||
from core.rag.extractor.notion_extractor import NotionExtractor
|
||||
|
|
|
@ -10,8 +10,7 @@ from controllers.console import api
|
|||
from controllers.console.apikey import api_key_fields, api_key_list
|
||||
from controllers.console.app.error import ProviderNotInitializeError
|
||||
from controllers.console.datasets.error import DatasetInUseError, DatasetNameDuplicateError, IndexingEstimateError
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
|
||||
from core.indexing_runner import IndexingRunner
|
||||
from core.model_runtime.entities.model_entities import ModelType
|
||||
|
|
|
@ -24,8 +24,11 @@ from controllers.console.datasets.error import (
|
|||
InvalidActionError,
|
||||
InvalidMetadataError,
|
||||
)
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_resource_check,
|
||||
setup_required,
|
||||
)
|
||||
from core.errors.error import (
|
||||
LLMBadRequestError,
|
||||
ModelCurrentlyNotSupportError,
|
||||
|
|
|
@ -11,11 +11,11 @@ import services
|
|||
from controllers.console import api
|
||||
from controllers.console.app.error import ProviderNotInitializeError
|
||||
from controllers.console.datasets.error import InvalidActionError, NoFileUploadedError, TooManyFilesError
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_knowledge_limit_check,
|
||||
cloud_edition_billing_resource_check,
|
||||
setup_required,
|
||||
)
|
||||
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
|
||||
from core.model_manager import ModelManager
|
||||
|
|
|
@ -6,8 +6,7 @@ from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
|
|||
import services
|
||||
from controllers.console import api
|
||||
from controllers.console.datasets.error import DatasetNameDuplicateError
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from fields.dataset_fields import dataset_detail_fields
|
||||
from libs.login import login_required
|
||||
from services.dataset_service import DatasetService
|
||||
|
|
|
@ -2,8 +2,7 @@ from flask_restful import Resource
|
|||
|
||||
from controllers.console import api
|
||||
from controllers.console.datasets.hit_testing_base import DatasetsHitTestingBase
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from libs.login import login_required
|
||||
|
||||
|
||||
|
|
|
@ -2,8 +2,7 @@ from flask_restful import Resource, reqparse
|
|||
|
||||
from controllers.console import api
|
||||
from controllers.console.datasets.error import WebsiteCrawlError
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from libs.login import login_required
|
||||
from services.website_service import WebsiteService
|
||||
|
||||
|
|
|
@ -3,8 +3,7 @@ from flask_restful import Resource, marshal_with, reqparse
|
|||
|
||||
from constants import HIDDEN_VALUE
|
||||
from controllers.console import api
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from fields.api_based_extension_fields import api_based_extension_fields
|
||||
from libs.login import login_required
|
||||
from models.api_based_extension import APIBasedExtension
|
||||
|
|
|
@ -5,8 +5,7 @@ from libs.login import login_required
|
|||
from services.feature_service import FeatureService
|
||||
|
||||
from . import api
|
||||
from .setup import setup_required
|
||||
from .wraps import account_initialization_required, cloud_utm_record
|
||||
from .wraps import account_initialization_required, cloud_utm_record, setup_required
|
||||
|
||||
|
||||
class FeatureApi(Resource):
|
||||
|
|
|
@ -1,25 +1,26 @@
|
|||
import urllib.parse
|
||||
|
||||
from flask import request
|
||||
from flask_login import current_user
|
||||
from flask_restful import Resource, marshal_with, reqparse
|
||||
from flask_restful import Resource, marshal_with
|
||||
|
||||
import services
|
||||
from configs import dify_config
|
||||
from constants import DOCUMENT_EXTENSIONS
|
||||
from controllers.console import api
|
||||
from controllers.console.datasets.error import (
|
||||
from controllers.common.errors import FilenameNotExistsError
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_resource_check,
|
||||
setup_required,
|
||||
)
|
||||
from fields.file_fields import file_fields, upload_config_fields
|
||||
from libs.login import login_required
|
||||
from services.file_service import FileService
|
||||
|
||||
from .errors import (
|
||||
FileTooLargeError,
|
||||
NoFileUploadedError,
|
||||
TooManyFilesError,
|
||||
UnsupportedFileTypeError,
|
||||
)
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check
|
||||
from core.helper import ssrf_proxy
|
||||
from fields.file_fields import file_fields, remote_file_info_fields, upload_config_fields
|
||||
from libs.login import login_required
|
||||
from services.file_service import FileService
|
||||
|
||||
PREVIEW_WORDS_LIMIT = 3000
|
||||
|
||||
|
@ -44,21 +45,29 @@ class FileApi(Resource):
|
|||
@marshal_with(file_fields)
|
||||
@cloud_edition_billing_resource_check("documents")
|
||||
def post(self):
|
||||
# get file from request
|
||||
file = request.files["file"]
|
||||
source = request.form.get("source")
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("source", type=str, required=False, location="args")
|
||||
source = parser.parse_args().get("source")
|
||||
|
||||
# check file
|
||||
if "file" not in request.files:
|
||||
raise NoFileUploadedError()
|
||||
|
||||
if len(request.files) > 1:
|
||||
raise TooManyFilesError()
|
||||
|
||||
if not file.filename:
|
||||
raise FilenameNotExistsError
|
||||
|
||||
if source not in ("datasets", None):
|
||||
source = None
|
||||
|
||||
try:
|
||||
upload_file = FileService.upload_file(file=file, user=current_user, source=source)
|
||||
upload_file = FileService.upload_file(
|
||||
filename=file.filename,
|
||||
content=file.read(),
|
||||
mimetype=file.mimetype,
|
||||
user=current_user,
|
||||
source=source,
|
||||
)
|
||||
except services.errors.file.FileTooLargeError as file_too_large_error:
|
||||
raise FileTooLargeError(file_too_large_error.description)
|
||||
except services.errors.file.UnsupportedFileTypeError:
|
||||
|
@ -83,23 +92,3 @@ class FileSupportTypeApi(Resource):
|
|||
@account_initialization_required
|
||||
def get(self):
|
||||
return {"allowed_extensions": DOCUMENT_EXTENSIONS}
|
||||
|
||||
|
||||
class RemoteFileInfoApi(Resource):
|
||||
@marshal_with(remote_file_info_fields)
|
||||
def get(self, url):
|
||||
decoded_url = urllib.parse.unquote(url)
|
||||
try:
|
||||
response = ssrf_proxy.head(decoded_url)
|
||||
return {
|
||||
"file_type": response.headers.get("Content-Type", "application/octet-stream"),
|
||||
"file_length": int(response.headers.get("Content-Length", 0)),
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}, 400
|
||||
|
||||
|
||||
api.add_resource(FileApi, "/files/upload")
|
||||
api.add_resource(FilePreviewApi, "/files/<uuid:file_id>/preview")
|
||||
api.add_resource(FileSupportTypeApi, "/files/support-type")
|
||||
api.add_resource(RemoteFileInfoApi, "/remote-files/<path:url>")
|
25
api/controllers/console/files/errors.py
Normal file
25
api/controllers/console/files/errors.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
from libs.exception import BaseHTTPException
|
||||
|
||||
|
||||
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
|
89
api/controllers/console/remote_files.py
Normal file
89
api/controllers/console/remote_files.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import urllib.parse
|
||||
from typing import cast
|
||||
from uuid import uuid4
|
||||
|
||||
from flask_login import current_user
|
||||
from flask_restful import Resource, marshal_with, reqparse
|
||||
|
||||
from core.helper import ssrf_proxy
|
||||
from fields.file_fields import file_fields, remote_file_info_fields
|
||||
from models.account import Account
|
||||
from services.file_service import FileService
|
||||
|
||||
|
||||
class RemoteFileInfoApi(Resource):
|
||||
@marshal_with(remote_file_info_fields)
|
||||
def get(self, url):
|
||||
decoded_url = urllib.parse.unquote(url)
|
||||
try:
|
||||
response = ssrf_proxy.head(decoded_url)
|
||||
return {
|
||||
"file_type": response.headers.get("Content-Type", "application/octet-stream"),
|
||||
"file_length": int(response.headers.get("Content-Length", 0)),
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}, 400
|
||||
|
||||
|
||||
class RemoteFileUploadApi(Resource):
|
||||
@marshal_with(file_fields)
|
||||
def post(self):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("url", type=str, required=True, help="URL is required")
|
||||
args = parser.parse_args()
|
||||
|
||||
url = args["url"]
|
||||
|
||||
try:
|
||||
response = ssrf_proxy.get(url)
|
||||
response.raise_for_status()
|
||||
content = response.content
|
||||
except Exception as e:
|
||||
return {"error": str(e)}, 400
|
||||
|
||||
# Try to extract filename from URL
|
||||
parsed_url = urllib.parse.urlparse(url)
|
||||
url_path = parsed_url.path
|
||||
filename = os.path.basename(url_path)
|
||||
|
||||
# If filename couldn't be extracted, use Content-Disposition header
|
||||
if not filename:
|
||||
content_disposition = response.headers.get("Content-Disposition")
|
||||
if content_disposition:
|
||||
filename_match = re.search(r'filename="?(.+)"?', content_disposition)
|
||||
if filename_match:
|
||||
filename = filename_match.group(1)
|
||||
|
||||
# If still no filename, generate a unique one
|
||||
if not filename:
|
||||
unique_name = str(uuid4())
|
||||
filename = f"{unique_name}"
|
||||
|
||||
# Guess MIME type from filename first, then URL
|
||||
mimetype, _ = mimetypes.guess_type(filename)
|
||||
if mimetype is None:
|
||||
mimetype, _ = mimetypes.guess_type(url)
|
||||
if mimetype is None:
|
||||
# If guessing fails, use Content-Type from response headers
|
||||
mimetype = response.headers.get("Content-Type", "application/octet-stream")
|
||||
|
||||
# Ensure filename has an extension
|
||||
if not os.path.splitext(filename)[1]:
|
||||
extension = mimetypes.guess_extension(mimetype) or ".bin"
|
||||
filename = f"{filename}{extension}"
|
||||
|
||||
try:
|
||||
user = cast(Account, current_user)
|
||||
upload_file = FileService.upload_file(
|
||||
filename=filename,
|
||||
content=content,
|
||||
mimetype=mimetype,
|
||||
user=user,
|
||||
)
|
||||
except Exception as e:
|
||||
return {"error": str(e)}, 400
|
||||
|
||||
return upload_file, 201
|
|
@ -1,5 +1,3 @@
|
|||
from functools import wraps
|
||||
|
||||
from flask import request
|
||||
from flask_restful import Resource, reqparse
|
||||
|
||||
|
@ -10,7 +8,7 @@ from models.model import DifySetup
|
|||
from services.account_service import RegisterService, TenantService
|
||||
|
||||
from . import api
|
||||
from .error import AlreadySetupError, NotInitValidateError, NotSetupError
|
||||
from .error import AlreadySetupError, NotInitValidateError
|
||||
from .init_validate import get_init_validate_status
|
||||
from .wraps import only_edition_self_hosted
|
||||
|
||||
|
@ -52,26 +50,10 @@ class SetupApi(Resource):
|
|||
return {"result": "success"}, 201
|
||||
|
||||
|
||||
def setup_required(view):
|
||||
@wraps(view)
|
||||
def decorated(*args, **kwargs):
|
||||
# check setup
|
||||
if not get_init_validate_status():
|
||||
raise NotInitValidateError()
|
||||
|
||||
elif not get_setup_status():
|
||||
raise NotSetupError()
|
||||
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
|
||||
def get_setup_status():
|
||||
if dify_config.EDITION == "SELF_HOSTED":
|
||||
return DifySetup.query.first()
|
||||
else:
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
api.add_resource(SetupApi, "/setup")
|
||||
|
|
|
@ -4,8 +4,7 @@ from flask_restful import Resource, marshal_with, reqparse
|
|||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from fields.tag_fields import tag_fields
|
||||
from libs.login import login_required
|
||||
from models.model import Tag
|
||||
|
|
|
@ -8,14 +8,13 @@ from flask_restful import Resource, fields, marshal_with, reqparse
|
|||
from configs import dify_config
|
||||
from constants.languages import supported_language
|
||||
from controllers.console import api
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.workspace.error import (
|
||||
AccountAlreadyInitedError,
|
||||
CurrentPasswordIncorrectError,
|
||||
InvalidInvitationCodeError,
|
||||
RepeatPasswordNotMatchError,
|
||||
)
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from extensions.ext_database import db
|
||||
from fields.member_fields import account_fields
|
||||
from libs.helper import TimestampField, timezone
|
||||
|
|
|
@ -2,8 +2,7 @@ from flask_restful import Resource, reqparse
|
|||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from core.model_runtime.entities.model_entities import ModelType
|
||||
from core.model_runtime.errors.validate import CredentialsValidateFailedError
|
||||
from libs.login import current_user, login_required
|
||||
|
|
|
@ -4,8 +4,11 @@ from flask_restful import Resource, abort, marshal_with, reqparse
|
|||
import services
|
||||
from configs import dify_config
|
||||
from controllers.console import api
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_resource_check,
|
||||
setup_required,
|
||||
)
|
||||
from extensions.ext_database import db
|
||||
from fields.member_fields import account_with_role_list_fields
|
||||
from libs.login import login_required
|
||||
|
|
|
@ -6,8 +6,7 @@ from flask_restful import Resource, reqparse
|
|||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from core.model_runtime.entities.model_entities import ModelType
|
||||
from core.model_runtime.errors.validate import CredentialsValidateFailedError
|
||||
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||
|
|
|
@ -5,8 +5,7 @@ from flask_restful import Resource, reqparse
|
|||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from core.model_runtime.entities.model_entities import ModelType
|
||||
from core.model_runtime.errors.validate import CredentialsValidateFailedError
|
||||
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||
|
|
|
@ -7,8 +7,7 @@ from werkzeug.exceptions import Forbidden
|
|||
|
||||
from configs import dify_config
|
||||
from controllers.console import api
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||
from libs.helper import alphanumeric, uuid_value
|
||||
from libs.login import login_required
|
||||
|
|
|
@ -6,6 +6,7 @@ from flask_restful import Resource, fields, inputs, marshal, marshal_with, reqpa
|
|||
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 (
|
||||
|
@ -15,8 +16,11 @@ from controllers.console.datasets.error import (
|
|||
UnsupportedFileTypeError,
|
||||
)
|
||||
from controllers.console.error import AccountNotLinkTenantError
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_resource_check,
|
||||
setup_required,
|
||||
)
|
||||
from extensions.ext_database import db
|
||||
from libs.helper import TimestampField
|
||||
from libs.login import login_required
|
||||
|
@ -193,12 +197,20 @@ class WebappLogoWorkspaceApi(Resource):
|
|||
if len(request.files) > 1:
|
||||
raise TooManyFilesError()
|
||||
|
||||
if not file.filename:
|
||||
raise FilenameNotExistsError
|
||||
|
||||
extension = file.filename.split(".")[-1]
|
||||
if extension.lower() not in {"svg", "png"}:
|
||||
raise UnsupportedFileTypeError()
|
||||
|
||||
try:
|
||||
upload_file = FileService.upload_file(file=file, user=current_user)
|
||||
upload_file = FileService.upload_file(
|
||||
filename=file.filename,
|
||||
content=file.read(),
|
||||
mimetype=file.mimetype,
|
||||
user=current_user,
|
||||
)
|
||||
|
||||
except services.errors.file.FileTooLargeError as file_too_large_error:
|
||||
raise FileTooLargeError(file_too_large_error.description)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import json
|
||||
import os
|
||||
from functools import wraps
|
||||
|
||||
from flask import abort, request
|
||||
|
@ -6,9 +7,12 @@ from flask_login import current_user
|
|||
|
||||
from configs import dify_config
|
||||
from controllers.console.workspace.error import AccountNotInitializedError
|
||||
from models.model import DifySetup
|
||||
from services.feature_service import FeatureService
|
||||
from services.operation_service import OperationService
|
||||
|
||||
from .error import NotInitValidateError, NotSetupError
|
||||
|
||||
|
||||
def account_initialization_required(view):
|
||||
@wraps(view)
|
||||
|
@ -124,3 +128,17 @@ def cloud_utm_record(view):
|
|||
return view(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
|
||||
def setup_required(view):
|
||||
@wraps(view)
|
||||
def decorated(*args, **kwargs):
|
||||
# check setup
|
||||
if dify_config.EDITION == "SELF_HOSTED" and os.environ.get("INIT_PASSWORD") and not DifySetup.query.first():
|
||||
raise NotInitValidateError()
|
||||
elif dify_config.EDITION == "SELF_HOSTED" and not DifySetup.query.first():
|
||||
raise NotSetupError()
|
||||
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from flask_restful import Resource, reqparse
|
||||
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import setup_required
|
||||
from controllers.inner_api import api
|
||||
from controllers.inner_api.wraps import inner_api_only
|
||||
from events.tenant_event import tenant_was_created
|
||||
|
|
|
@ -2,6 +2,7 @@ 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 (
|
||||
FileTooLargeError,
|
||||
|
@ -31,8 +32,17 @@ class FileApi(Resource):
|
|||
if len(request.files) > 1:
|
||||
raise TooManyFilesError()
|
||||
|
||||
if not file.filename:
|
||||
raise FilenameNotExistsError
|
||||
|
||||
try:
|
||||
upload_file = FileService.upload_file(file, end_user)
|
||||
upload_file = FileService.upload_file(
|
||||
filename=file.filename,
|
||||
content=file.read(),
|
||||
mimetype=file.mimetype,
|
||||
user=end_user,
|
||||
source="datasets",
|
||||
)
|
||||
except services.errors.file.FileTooLargeError as file_too_large_error:
|
||||
raise FileTooLargeError(file_too_large_error.description)
|
||||
except services.errors.file.UnsupportedFileTypeError:
|
||||
|
|
|
@ -6,6 +6,7 @@ from sqlalchemy import desc
|
|||
from werkzeug.exceptions import NotFound
|
||||
|
||||
import services.dataset_service
|
||||
from controllers.common.errors import FilenameNotExistsError
|
||||
from controllers.service_api import api
|
||||
from controllers.service_api.app.error import ProviderNotInitializeError
|
||||
from controllers.service_api.dataset.error import (
|
||||
|
@ -55,7 +56,12 @@ class DocumentAddByTextApi(DatasetApiResource):
|
|||
if not dataset.indexing_technique and not args["indexing_technique"]:
|
||||
raise ValueError("indexing_technique is required.")
|
||||
|
||||
upload_file = FileService.upload_text(args.get("text"), args.get("name"))
|
||||
text = args.get("text")
|
||||
name = args.get("name")
|
||||
if text is None or name is None:
|
||||
raise ValueError("Both 'text' and 'name' must be non-null values.")
|
||||
|
||||
upload_file = FileService.upload_text(text=str(text), text_name=str(name))
|
||||
data_source = {
|
||||
"type": "upload_file",
|
||||
"info_list": {"data_source_type": "upload_file", "file_info_list": {"file_ids": [upload_file.id]}},
|
||||
|
@ -104,7 +110,11 @@ class DocumentUpdateByTextApi(DatasetApiResource):
|
|||
raise ValueError("Dataset is not exist.")
|
||||
|
||||
if args["text"]:
|
||||
upload_file = FileService.upload_text(args.get("text"), args.get("name"))
|
||||
text = args.get("text")
|
||||
name = args.get("name")
|
||||
if text is None or name is None:
|
||||
raise ValueError("Both text and name must be strings.")
|
||||
upload_file = FileService.upload_text(text=str(text), text_name=str(name))
|
||||
data_source = {
|
||||
"type": "upload_file",
|
||||
"info_list": {"data_source_type": "upload_file", "file_info_list": {"file_ids": [upload_file.id]}},
|
||||
|
@ -163,7 +173,16 @@ class DocumentAddByFileApi(DatasetApiResource):
|
|||
if len(request.files) > 1:
|
||||
raise TooManyFilesError()
|
||||
|
||||
upload_file = FileService.upload_file(file, current_user)
|
||||
if not file.filename:
|
||||
raise FilenameNotExistsError
|
||||
|
||||
upload_file = FileService.upload_file(
|
||||
filename=file.filename,
|
||||
content=file.read(),
|
||||
mimetype=file.mimetype,
|
||||
user=current_user,
|
||||
source="datasets",
|
||||
)
|
||||
data_source = {"type": "upload_file", "info_list": {"file_info_list": {"file_ids": [upload_file.id]}}}
|
||||
args["data_source"] = data_source
|
||||
# validate args
|
||||
|
@ -212,7 +231,16 @@ class DocumentUpdateByFileApi(DatasetApiResource):
|
|||
if len(request.files) > 1:
|
||||
raise TooManyFilesError()
|
||||
|
||||
upload_file = FileService.upload_file(file, current_user)
|
||||
if not file.filename:
|
||||
raise FilenameNotExistsError
|
||||
|
||||
upload_file = FileService.upload_file(
|
||||
filename=file.filename,
|
||||
content=file.read(),
|
||||
mimetype=file.mimetype,
|
||||
user=current_user,
|
||||
source="datasets",
|
||||
)
|
||||
data_source = {"type": "upload_file", "info_list": {"file_info_list": {"file_ids": [upload_file.id]}}}
|
||||
args["data_source"] = data_source
|
||||
# validate args
|
||||
|
|
|
@ -2,8 +2,16 @@ from flask import Blueprint
|
|||
|
||||
from libs.external_api import ExternalApi
|
||||
|
||||
from .files import FileApi
|
||||
from .remote_files import RemoteFileInfoApi
|
||||
|
||||
bp = Blueprint("web", __name__, url_prefix="/api")
|
||||
api = ExternalApi(bp)
|
||||
|
||||
# Files
|
||||
api.add_resource(FileApi, "/files/upload")
|
||||
|
||||
from . import app, audio, completion, conversation, feature, file, message, passport, saved_message, site, workflow
|
||||
# Remote files
|
||||
api.add_resource(RemoteFileInfoApi, "/remote-files/<path:url>")
|
||||
|
||||
from . import app, audio, completion, conversation, feature, message, passport, saved_message, site, workflow
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
import urllib.parse
|
||||
|
||||
from flask import request
|
||||
from flask_restful import marshal_with, reqparse
|
||||
|
||||
import services
|
||||
from controllers.web import api
|
||||
from controllers.web.error import FileTooLargeError, NoFileUploadedError, TooManyFilesError, UnsupportedFileTypeError
|
||||
from controllers.web.wraps import WebApiResource
|
||||
from core.helper import ssrf_proxy
|
||||
from fields.file_fields import file_fields, remote_file_info_fields
|
||||
from services.file_service import FileService
|
||||
|
||||
|
||||
class FileApi(WebApiResource):
|
||||
@marshal_with(file_fields)
|
||||
def post(self, app_model, end_user):
|
||||
# get file from request
|
||||
file = request.files["file"]
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("source", type=str, required=False, location="args")
|
||||
source = parser.parse_args().get("source")
|
||||
|
||||
# check file
|
||||
if "file" not in request.files:
|
||||
raise NoFileUploadedError()
|
||||
|
||||
if len(request.files) > 1:
|
||||
raise TooManyFilesError()
|
||||
try:
|
||||
upload_file = FileService.upload_file(file=file, user=end_user, source=source)
|
||||
except services.errors.file.FileTooLargeError as file_too_large_error:
|
||||
raise FileTooLargeError(file_too_large_error.description)
|
||||
except services.errors.file.UnsupportedFileTypeError:
|
||||
raise UnsupportedFileTypeError()
|
||||
|
||||
return upload_file, 201
|
||||
|
||||
|
||||
class RemoteFileInfoApi(WebApiResource):
|
||||
@marshal_with(remote_file_info_fields)
|
||||
def get(self, url):
|
||||
decoded_url = urllib.parse.unquote(url)
|
||||
try:
|
||||
response = ssrf_proxy.head(decoded_url)
|
||||
return {
|
||||
"file_type": response.headers.get("Content-Type", "application/octet-stream"),
|
||||
"file_length": int(response.headers.get("Content-Length", -1)),
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}, 400
|
||||
|
||||
|
||||
api.add_resource(FileApi, "/files/upload")
|
||||
api.add_resource(RemoteFileInfoApi, "/remote-files/<path:url>")
|
43
api/controllers/web/files.py
Normal file
43
api/controllers/web/files.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
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.web.wraps import WebApiResource
|
||||
from fields.file_fields import file_fields
|
||||
from services.file_service import FileService
|
||||
|
||||
|
||||
class FileApi(WebApiResource):
|
||||
@marshal_with(file_fields)
|
||||
def post(self, app_model, end_user):
|
||||
file = request.files["file"]
|
||||
source = request.form.get("source")
|
||||
|
||||
if "file" not in request.files:
|
||||
raise NoFileUploadedError()
|
||||
|
||||
if len(request.files) > 1:
|
||||
raise TooManyFilesError()
|
||||
|
||||
if not file.filename:
|
||||
raise FilenameNotExistsError
|
||||
|
||||
if source not in ("datasets", None):
|
||||
source = None
|
||||
|
||||
try:
|
||||
upload_file = FileService.upload_file(
|
||||
filename=file.filename,
|
||||
content=file.read(),
|
||||
mimetype=file.mimetype,
|
||||
user=end_user,
|
||||
source=source,
|
||||
)
|
||||
except services.errors.file.FileTooLargeError as file_too_large_error:
|
||||
raise FileTooLargeError(file_too_large_error.description)
|
||||
except services.errors.file.UnsupportedFileTypeError:
|
||||
raise UnsupportedFileTypeError()
|
||||
|
||||
return upload_file, 201
|
87
api/controllers/web/remote_files.py
Normal file
87
api/controllers/web/remote_files.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import urllib.parse
|
||||
from uuid import uuid4
|
||||
|
||||
from flask_login import current_user
|
||||
from flask_restful import marshal_with, reqparse
|
||||
|
||||
from controllers.web.wraps import WebApiResource
|
||||
from core.helper import ssrf_proxy
|
||||
from fields.file_fields import file_fields, remote_file_info_fields
|
||||
from services.file_service import FileService
|
||||
|
||||
|
||||
class RemoteFileInfoApi(WebApiResource):
|
||||
@marshal_with(remote_file_info_fields)
|
||||
def get(self, url):
|
||||
decoded_url = urllib.parse.unquote(url)
|
||||
try:
|
||||
response = ssrf_proxy.head(decoded_url)
|
||||
return {
|
||||
"file_type": response.headers.get("Content-Type", "application/octet-stream"),
|
||||
"file_length": int(response.headers.get("Content-Length", -1)),
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}, 400
|
||||
|
||||
|
||||
class RemoteFileUploadApi(WebApiResource):
|
||||
@marshal_with(file_fields)
|
||||
def post(self):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("url", type=str, required=True, help="URL is required")
|
||||
args = parser.parse_args()
|
||||
|
||||
url = args["url"]
|
||||
|
||||
try:
|
||||
response = ssrf_proxy.get(url)
|
||||
response.raise_for_status()
|
||||
content = response.content
|
||||
except Exception as e:
|
||||
return {"error": str(e)}, 400
|
||||
|
||||
# Try to extract filename from URL
|
||||
parsed_url = urllib.parse.urlparse(url)
|
||||
url_path = parsed_url.path
|
||||
filename = os.path.basename(url_path)
|
||||
|
||||
# If filename couldn't be extracted, use Content-Disposition header
|
||||
if not filename:
|
||||
content_disposition = response.headers.get("Content-Disposition")
|
||||
if content_disposition:
|
||||
filename_match = re.search(r'filename="?(.+)"?', content_disposition)
|
||||
if filename_match:
|
||||
filename = filename_match.group(1)
|
||||
|
||||
# If still no filename, generate a unique one
|
||||
if not filename:
|
||||
unique_name = str(uuid4())
|
||||
filename = f"{unique_name}"
|
||||
|
||||
# Guess MIME type from filename first, then URL
|
||||
mimetype, _ = mimetypes.guess_type(filename)
|
||||
if mimetype is None:
|
||||
mimetype, _ = mimetypes.guess_type(url)
|
||||
if mimetype is None:
|
||||
# If guessing fails, use Content-Type from response headers
|
||||
mimetype = response.headers.get("Content-Type", "application/octet-stream")
|
||||
|
||||
# Ensure filename has an extension
|
||||
if not os.path.splitext(filename)[1]:
|
||||
extension = mimetypes.guess_extension(mimetype) or ".bin"
|
||||
filename = f"{filename}{extension}"
|
||||
|
||||
try:
|
||||
upload_file = FileService.upload_file(
|
||||
filename=filename,
|
||||
content=content,
|
||||
mimetype=mimetype,
|
||||
user=current_user,
|
||||
)
|
||||
except Exception as e:
|
||||
return {"error": str(e)}, 400
|
||||
|
||||
return upload_file, 201
|
|
@ -234,7 +234,7 @@ class WordExtractor(BaseExtractor):
|
|||
def parse_paragraph(paragraph):
|
||||
paragraph_content = []
|
||||
for run in paragraph.runs:
|
||||
if hasattr(run.element, "tag") and isinstance(element.tag, str) and run.element.tag.endswith("r"):
|
||||
if hasattr(run.element, "tag") and isinstance(run.element.tag, str) and run.element.tag.endswith("r"):
|
||||
drawing_elements = run.element.findall(
|
||||
".//{http://schemas.openxmlformats.org/wordprocessingml/2006/main}drawing"
|
||||
)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import base64
|
||||
import io
|
||||
import json
|
||||
import random
|
||||
import uuid
|
||||
|
@ -6,45 +8,48 @@ import httpx
|
|||
from websocket import WebSocket
|
||||
from yarl import URL
|
||||
|
||||
from core.file.file_manager import _get_encoded_string
|
||||
from core.file.models import File
|
||||
|
||||
|
||||
class ComfyUiClient:
|
||||
def __init__(self, base_url: str):
|
||||
self.base_url = URL(base_url)
|
||||
|
||||
def get_history(self, prompt_id: str):
|
||||
def get_history(self, prompt_id: str) -> dict:
|
||||
res = httpx.get(str(self.base_url / "history"), params={"prompt_id": prompt_id})
|
||||
history = res.json()[prompt_id]
|
||||
return history
|
||||
|
||||
def get_image(self, filename: str, subfolder: str, folder_type: str):
|
||||
def get_image(self, filename: str, subfolder: str, folder_type: str) -> bytes:
|
||||
response = httpx.get(
|
||||
str(self.base_url / "view"),
|
||||
params={"filename": filename, "subfolder": subfolder, "type": folder_type},
|
||||
)
|
||||
return response.content
|
||||
|
||||
def upload_image(self, input_path: str, name: str, image_type: str = "input", overwrite: bool = False):
|
||||
# plan to support img2img in dify 0.10.0
|
||||
with open(input_path, "rb") as file:
|
||||
files = {"image": (name, file, "image/png")}
|
||||
data = {"type": image_type, "overwrite": str(overwrite).lower()}
|
||||
def upload_image(self, image_file: File) -> dict:
|
||||
image_content = base64.b64decode(_get_encoded_string(image_file))
|
||||
file = io.BytesIO(image_content)
|
||||
files = {"image": (image_file.filename, file, image_file.mime_type), "overwrite": "true"}
|
||||
res = httpx.post(str(self.base_url / "upload/image"), files=files)
|
||||
return res.json()
|
||||
|
||||
res = httpx.post(str(self.base_url / "upload/image"), data=data, files=files)
|
||||
return res
|
||||
|
||||
def queue_prompt(self, client_id: str, prompt: dict):
|
||||
def queue_prompt(self, client_id: str, prompt: dict) -> str:
|
||||
res = httpx.post(str(self.base_url / "prompt"), json={"client_id": client_id, "prompt": prompt})
|
||||
prompt_id = res.json()["prompt_id"]
|
||||
return prompt_id
|
||||
|
||||
def open_websocket_connection(self):
|
||||
def open_websocket_connection(self) -> tuple[WebSocket, str]:
|
||||
client_id = str(uuid.uuid4())
|
||||
ws = WebSocket()
|
||||
ws_address = f"ws://{self.base_url.authority}/ws?clientId={client_id}"
|
||||
ws.connect(ws_address)
|
||||
return ws, client_id
|
||||
|
||||
def set_prompt(self, origin_prompt: dict, positive_prompt: str, negative_prompt: str = ""):
|
||||
def set_prompt(
|
||||
self, origin_prompt: dict, positive_prompt: str, negative_prompt: str = "", image_name: str = ""
|
||||
) -> dict:
|
||||
"""
|
||||
find the first KSampler, then can find the prompt node through it.
|
||||
"""
|
||||
|
@ -58,6 +63,10 @@ class ComfyUiClient:
|
|||
if negative_prompt != "":
|
||||
negative_input_id = prompt.get(k_sampler)["inputs"]["negative"][0]
|
||||
prompt.get(negative_input_id)["inputs"]["text"] = negative_prompt
|
||||
|
||||
if image_name != "":
|
||||
image_loader = [key for key, value in id_to_class_type.items() if value == "LoadImage"][0]
|
||||
prompt.get(image_loader)["inputs"]["image"] = image_name
|
||||
return prompt
|
||||
|
||||
def track_progress(self, prompt: dict, ws: WebSocket, prompt_id: str):
|
||||
|
@ -89,7 +98,7 @@ class ComfyUiClient:
|
|||
else:
|
||||
continue
|
||||
|
||||
def generate_image_by_prompt(self, prompt: dict):
|
||||
def generate_image_by_prompt(self, prompt: dict) -> list[bytes]:
|
||||
try:
|
||||
ws, client_id = self.open_websocket_connection()
|
||||
prompt_id = self.queue_prompt(client_id, prompt)
|
||||
|
|
|
@ -2,10 +2,9 @@ import json
|
|||
from typing import Any
|
||||
|
||||
from core.tools.entities.tool_entities import ToolInvokeMessage
|
||||
from core.tools.provider.builtin.comfyui.tools.comfyui_client import ComfyUiClient
|
||||
from core.tools.tool.builtin_tool import BuiltinTool
|
||||
|
||||
from .comfyui_client import ComfyUiClient
|
||||
|
||||
|
||||
class ComfyUIWorkflowTool(BuiltinTool):
|
||||
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage | list[ToolInvokeMessage]:
|
||||
|
@ -14,13 +13,16 @@ class ComfyUIWorkflowTool(BuiltinTool):
|
|||
positive_prompt = tool_parameters.get("positive_prompt")
|
||||
negative_prompt = tool_parameters.get("negative_prompt")
|
||||
workflow = tool_parameters.get("workflow_json")
|
||||
image_name = ""
|
||||
if image := tool_parameters.get("image"):
|
||||
image_name = comfyui.upload_image(image).get("name")
|
||||
|
||||
try:
|
||||
origin_prompt = json.loads(workflow)
|
||||
except:
|
||||
return self.create_text_message("the Workflow JSON is not correct")
|
||||
|
||||
prompt = comfyui.set_prompt(origin_prompt, positive_prompt, negative_prompt)
|
||||
prompt = comfyui.set_prompt(origin_prompt, positive_prompt, negative_prompt, image_name)
|
||||
images = comfyui.generate_image_by_prompt(prompt)
|
||||
result = []
|
||||
for img in images:
|
||||
|
|
|
@ -24,6 +24,13 @@ parameters:
|
|||
zh_Hans: 负面提示词
|
||||
llm_description: Negative prompt, you should describe the image you don't want to generate as a list of words as possible as detailed, the prompt must be written in English.
|
||||
form: llm
|
||||
- name: image
|
||||
type: file
|
||||
label:
|
||||
en_US: Input Image
|
||||
zh_Hans: 输入的图片
|
||||
llm_description: The input image, used to transfer to the comfyui workflow to generate another image.
|
||||
form: llm
|
||||
- name: workflow_json
|
||||
type: string
|
||||
required: true
|
||||
|
|
|
@ -131,15 +131,14 @@ class GraphEngine:
|
|||
yield GraphRunStartedEvent()
|
||||
|
||||
try:
|
||||
stream_processor_cls: type[AnswerStreamProcessor | EndStreamProcessor]
|
||||
if self.init_params.workflow_type == WorkflowType.CHAT:
|
||||
stream_processor_cls = AnswerStreamProcessor
|
||||
stream_processor = AnswerStreamProcessor(
|
||||
graph=self.graph, variable_pool=self.graph_runtime_state.variable_pool
|
||||
)
|
||||
else:
|
||||
stream_processor_cls = EndStreamProcessor
|
||||
|
||||
stream_processor = stream_processor_cls(
|
||||
graph=self.graph, variable_pool=self.graph_runtime_state.variable_pool
|
||||
)
|
||||
stream_processor = EndStreamProcessor(
|
||||
graph=self.graph, variable_pool=self.graph_runtime_state.variable_pool
|
||||
)
|
||||
|
||||
# run graph
|
||||
generator = stream_processor.process(self._run(start_node_id=self.graph.root_node_id))
|
||||
|
|
|
@ -149,10 +149,10 @@ class AnswerStreamGeneratorRouter:
|
|||
source_node_id = edge.source_node_id
|
||||
source_node_type = node_id_config_mapping[source_node_id].get("data", {}).get("type")
|
||||
if source_node_type in {
|
||||
NodeType.ANSWER.value,
|
||||
NodeType.IF_ELSE.value,
|
||||
NodeType.QUESTION_CLASSIFIER.value,
|
||||
NodeType.ITERATION.value,
|
||||
NodeType.ANSWER,
|
||||
NodeType.IF_ELSE,
|
||||
NodeType.QUESTION_CLASSIFIER,
|
||||
NodeType.ITERATION,
|
||||
}:
|
||||
answer_dependencies[answer_node_id].append(source_node_id)
|
||||
else:
|
||||
|
|
|
@ -22,7 +22,7 @@ class AnswerStreamProcessor(StreamProcessor):
|
|||
super().__init__(graph, variable_pool)
|
||||
self.generate_routes = graph.answer_stream_generate_routes
|
||||
self.route_position = {}
|
||||
for answer_node_id, route_chunks in self.generate_routes.answer_generate_route.items():
|
||||
for answer_node_id in self.generate_routes.answer_generate_route:
|
||||
self.route_position[answer_node_id] = 0
|
||||
self.current_stream_chunk_generating_node_ids: dict[str, list[str]] = {}
|
||||
|
||||
|
|
|
@ -41,7 +41,6 @@ class StreamProcessor(ABC):
|
|||
continue
|
||||
else:
|
||||
unreachable_first_node_ids.append(edge.target_node_id)
|
||||
unreachable_first_node_ids.extend(self._fetch_node_ids_in_reachable_branch(edge.target_node_id))
|
||||
|
||||
for node_id in unreachable_first_node_ids:
|
||||
self._remove_node_ids_in_unreachable_branch(node_id, reachable_node_ids)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from collections.abc import Sequence
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
@ -32,7 +33,7 @@ class VarGenerateRouteChunk(GenerateRouteChunk):
|
|||
|
||||
type: GenerateRouteChunk.ChunkType = GenerateRouteChunk.ChunkType.VAR
|
||||
"""generate route chunk type"""
|
||||
value_selector: list[str] = Field(..., description="value selector")
|
||||
value_selector: Sequence[str] = Field(..., description="value selector")
|
||||
|
||||
|
||||
class TextGenerateRouteChunk(GenerateRouteChunk):
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import csv
|
||||
import io
|
||||
import json
|
||||
|
||||
import docx
|
||||
import pandas as pd
|
||||
|
@ -77,34 +78,31 @@ class DocumentExtractorNode(BaseNode[DocumentExtractorNodeData]):
|
|||
|
||||
def _extract_text_by_mime_type(*, file_content: bytes, mime_type: str) -> str:
|
||||
"""Extract text from a file based on its MIME type."""
|
||||
if mime_type.startswith("text/plain") or mime_type in {"text/html", "text/htm", "text/markdown", "text/xml"}:
|
||||
return _extract_text_from_plain_text(file_content)
|
||||
elif mime_type == "application/pdf":
|
||||
return _extract_text_from_pdf(file_content)
|
||||
elif mime_type in {
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/msword",
|
||||
}:
|
||||
return _extract_text_from_doc(file_content)
|
||||
elif mime_type == "text/csv":
|
||||
return _extract_text_from_csv(file_content)
|
||||
elif mime_type in {
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.ms-excel",
|
||||
}:
|
||||
return _extract_text_from_excel(file_content)
|
||||
elif mime_type == "application/vnd.ms-powerpoint":
|
||||
return _extract_text_from_ppt(file_content)
|
||||
elif mime_type == "application/vnd.openxmlformats-officedocument.presentationml.presentation":
|
||||
return _extract_text_from_pptx(file_content)
|
||||
elif mime_type == "application/epub+zip":
|
||||
return _extract_text_from_epub(file_content)
|
||||
elif mime_type == "message/rfc822":
|
||||
return _extract_text_from_eml(file_content)
|
||||
elif mime_type == "application/vnd.ms-outlook":
|
||||
return _extract_text_from_msg(file_content)
|
||||
else:
|
||||
raise UnsupportedFileTypeError(f"Unsupported MIME type: {mime_type}")
|
||||
match mime_type:
|
||||
case "text/plain" | "text/html" | "text/htm" | "text/markdown" | "text/xml":
|
||||
return _extract_text_from_plain_text(file_content)
|
||||
case "application/pdf":
|
||||
return _extract_text_from_pdf(file_content)
|
||||
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document" | "application/msword":
|
||||
return _extract_text_from_doc(file_content)
|
||||
case "text/csv":
|
||||
return _extract_text_from_csv(file_content)
|
||||
case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" | "application/vnd.ms-excel":
|
||||
return _extract_text_from_excel(file_content)
|
||||
case "application/vnd.ms-powerpoint":
|
||||
return _extract_text_from_ppt(file_content)
|
||||
case "application/vnd.openxmlformats-officedocument.presentationml.presentation":
|
||||
return _extract_text_from_pptx(file_content)
|
||||
case "application/epub+zip":
|
||||
return _extract_text_from_epub(file_content)
|
||||
case "message/rfc822":
|
||||
return _extract_text_from_eml(file_content)
|
||||
case "application/vnd.ms-outlook":
|
||||
return _extract_text_from_msg(file_content)
|
||||
case "application/json":
|
||||
return _extract_text_from_json(file_content)
|
||||
case _:
|
||||
raise UnsupportedFileTypeError(f"Unsupported MIME type: {mime_type}")
|
||||
|
||||
|
||||
def _extract_text_by_file_extension(*, file_content: bytes, file_extension: str) -> str:
|
||||
|
@ -112,6 +110,8 @@ def _extract_text_by_file_extension(*, file_content: bytes, file_extension: str)
|
|||
match file_extension:
|
||||
case ".txt" | ".markdown" | ".md" | ".html" | ".htm" | ".xml":
|
||||
return _extract_text_from_plain_text(file_content)
|
||||
case ".json":
|
||||
return _extract_text_from_json(file_content)
|
||||
case ".pdf":
|
||||
return _extract_text_from_pdf(file_content)
|
||||
case ".doc" | ".docx":
|
||||
|
@ -141,6 +141,14 @@ def _extract_text_from_plain_text(file_content: bytes) -> str:
|
|||
raise TextExtractionError("Failed to decode plain text file") from e
|
||||
|
||||
|
||||
def _extract_text_from_json(file_content: bytes) -> str:
|
||||
try:
|
||||
json_data = json.loads(file_content.decode("utf-8"))
|
||||
return json.dumps(json_data, indent=2, ensure_ascii=False)
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as e:
|
||||
raise TextExtractionError(f"Failed to decode or parse JSON file: {e}") from e
|
||||
|
||||
|
||||
def _extract_text_from_pdf(file_content: bytes) -> str:
|
||||
try:
|
||||
pdf_file = io.BytesIO(file_content)
|
||||
|
@ -183,13 +191,13 @@ def _download_file_content(file: File) -> bytes:
|
|||
|
||||
|
||||
def _extract_text_from_file(file: File):
|
||||
if file.mime_type is None:
|
||||
raise UnsupportedFileTypeError("Unable to determine file type: MIME type is missing")
|
||||
file_content = _download_file_content(file)
|
||||
if file.transfer_method == FileTransferMethod.REMOTE_URL:
|
||||
if file.extension:
|
||||
extracted_text = _extract_text_by_file_extension(file_content=file_content, file_extension=file.extension)
|
||||
elif file.mime_type:
|
||||
extracted_text = _extract_text_by_mime_type(file_content=file_content, mime_type=file.mime_type)
|
||||
else:
|
||||
extracted_text = _extract_text_by_file_extension(file_content=file_content, file_extension=file.extension)
|
||||
raise UnsupportedFileTypeError("Unable to determine file type: MIME type or file extension is missing")
|
||||
return extracted_text
|
||||
|
||||
|
||||
|
|
|
@ -560,10 +560,28 @@ class DocumentSegment(db.Model):
|
|||
)
|
||||
|
||||
def get_sign_content(self):
|
||||
pattern = r"/files/([a-f0-9\-]+)/file-preview"
|
||||
text = self.content
|
||||
matches = re.finditer(pattern, text)
|
||||
signed_urls = []
|
||||
text = self.content
|
||||
|
||||
# For data before v0.10.0
|
||||
pattern = r"/files/([a-f0-9\-]+)/image-preview"
|
||||
matches = re.finditer(pattern, text)
|
||||
for match in matches:
|
||||
upload_file_id = match.group(1)
|
||||
nonce = os.urandom(16).hex()
|
||||
timestamp = str(int(time.time()))
|
||||
data_to_sign = f"image-preview|{upload_file_id}|{timestamp}|{nonce}"
|
||||
secret_key = dify_config.SECRET_KEY.encode() if dify_config.SECRET_KEY else b""
|
||||
sign = hmac.new(secret_key, data_to_sign.encode(), hashlib.sha256).digest()
|
||||
encoded_sign = base64.urlsafe_b64encode(sign).decode()
|
||||
|
||||
params = f"timestamp={timestamp}&nonce={nonce}&sign={encoded_sign}"
|
||||
signed_url = f"{match.group(0)}?{params}"
|
||||
signed_urls.append((match.start(), match.end(), signed_url))
|
||||
|
||||
# For data after v0.10.0
|
||||
pattern = r"/files/([a-f0-9\-]+)/file-preview"
|
||||
matches = re.finditer(pattern, text)
|
||||
for match in matches:
|
||||
upload_file_id = match.group(1)
|
||||
nonce = os.urandom(16).hex()
|
||||
|
|
|
@ -505,9 +505,7 @@ class TenantService:
|
|||
def create_owner_tenant_if_not_exist(
|
||||
account: Account, name: Optional[str] = None, is_setup: Optional[bool] = False
|
||||
):
|
||||
"""Create owner tenant if not exist"""
|
||||
if not FeatureService.get_system_features().is_allow_create_workspace and not is_setup:
|
||||
raise WorkSpaceNotAllowedCreateError()
|
||||
"""Check if user have a workspace or not"""
|
||||
available_ta = (
|
||||
TenantAccountJoin.query.filter_by(account_id=account.id).order_by(TenantAccountJoin.id.asc()).first()
|
||||
)
|
||||
|
@ -515,6 +513,10 @@ class TenantService:
|
|||
if available_ta:
|
||||
return
|
||||
|
||||
"""Create owner tenant if not exist"""
|
||||
if not FeatureService.get_system_features().is_allow_create_workspace and not is_setup:
|
||||
raise WorkSpaceNotAllowedCreateError()
|
||||
|
||||
if name:
|
||||
tenant = TenantService.create_tenant(name=name, is_setup=is_setup)
|
||||
else:
|
||||
|
|
|
@ -4,7 +4,7 @@ import logging
|
|||
import random
|
||||
import time
|
||||
import uuid
|
||||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
from flask_login import current_user
|
||||
from sqlalchemy import func
|
||||
|
@ -675,7 +675,7 @@ class DocumentService:
|
|||
def save_document_with_dataset_id(
|
||||
dataset: Dataset,
|
||||
document_data: dict,
|
||||
account: Account,
|
||||
account: Account | Any,
|
||||
dataset_process_rule: Optional[DatasetProcessRule] = None,
|
||||
created_from: str = "web",
|
||||
):
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import datetime
|
||||
import hashlib
|
||||
import uuid
|
||||
from typing import Literal, Union
|
||||
from typing import Any, Literal, Union
|
||||
|
||||
from flask_login import current_user
|
||||
from werkzeug.datastructures import FileStorage
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from configs import dify_config
|
||||
|
@ -21,7 +20,8 @@ from extensions.ext_storage import storage
|
|||
from models.account import Account
|
||||
from models.enums import CreatedByRole
|
||||
from models.model import EndUser, UploadFile
|
||||
from services.errors.file import FileNotExistsError, FileTooLargeError, UnsupportedFileTypeError
|
||||
|
||||
from .errors.file import FileTooLargeError, UnsupportedFileTypeError
|
||||
|
||||
PREVIEW_WORDS_LIMIT = 3000
|
||||
|
||||
|
@ -29,12 +29,14 @@ PREVIEW_WORDS_LIMIT = 3000
|
|||
class FileService:
|
||||
@staticmethod
|
||||
def upload_file(
|
||||
file: FileStorage, user: Union[Account, EndUser], source: Literal["datasets"] | None = None
|
||||
*,
|
||||
filename: str,
|
||||
content: bytes,
|
||||
mimetype: str,
|
||||
user: Union[Account, EndUser, Any],
|
||||
source: Literal["datasets"] | None = None,
|
||||
) -> UploadFile:
|
||||
# get file name
|
||||
filename = file.filename
|
||||
if not filename:
|
||||
raise FileNotExistsError
|
||||
# get file extension
|
||||
extension = filename.split(".")[-1]
|
||||
if len(filename) > 200:
|
||||
filename = filename.split(".")[0][:200] + "." + extension
|
||||
|
@ -52,10 +54,8 @@ class FileService:
|
|||
else:
|
||||
file_size_limit = dify_config.UPLOAD_FILE_SIZE_LIMIT * 1024 * 1024
|
||||
|
||||
# read file content
|
||||
file_content = file.read()
|
||||
# get file size
|
||||
file_size = len(file_content)
|
||||
file_size = len(content)
|
||||
|
||||
# check if the file size is exceeded
|
||||
if file_size > file_size_limit:
|
||||
|
@ -74,7 +74,7 @@ class FileService:
|
|||
file_key = "upload_files/" + current_tenant_id + "/" + file_uuid + "." + extension
|
||||
|
||||
# save file to storage
|
||||
storage.save(file_key, file_content)
|
||||
storage.save(file_key, content)
|
||||
|
||||
# save file to db
|
||||
upload_file = UploadFile(
|
||||
|
@ -84,12 +84,12 @@ class FileService:
|
|||
name=filename,
|
||||
size=file_size,
|
||||
extension=extension,
|
||||
mime_type=file.mimetype,
|
||||
mime_type=mimetype,
|
||||
created_by_role=(CreatedByRole.ACCOUNT if isinstance(user, Account) else CreatedByRole.END_USER),
|
||||
created_by=user.id,
|
||||
created_at=datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None),
|
||||
used=False,
|
||||
hash=hashlib.sha3_256(file_content).hexdigest(),
|
||||
hash=hashlib.sha3_256(content).hexdigest(),
|
||||
)
|
||||
|
||||
db.session.add(upload_file)
|
||||
|
|
|
@ -125,7 +125,7 @@ def test_run_extract_text(
|
|||
result = document_extractor_node._run()
|
||||
|
||||
assert isinstance(result, NodeRunResult)
|
||||
assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED
|
||||
assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED, result.error
|
||||
assert result.outputs is not None
|
||||
assert result.outputs["text"] == expected_text
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import { useChatContext } from '@/app/components/base/chat/chat/context'
|
|||
import VideoGallery from '@/app/components/base/video-gallery'
|
||||
import AudioGallery from '@/app/components/base/audio-gallery'
|
||||
import SVGRenderer from '@/app/components/base/svg-gallery'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
// Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD
|
||||
const capitalizationLanguageNameMap: Record<string, string> = {
|
||||
|
@ -240,6 +241,22 @@ const Link = ({ node, ...props }: any) => {
|
|||
}
|
||||
}
|
||||
|
||||
const MarkdownButton = ({ node }: any) => {
|
||||
const { onSend } = useChatContext()
|
||||
const variant = node.properties.dataVariant
|
||||
const message = node.properties.dataMessage
|
||||
const size = node.properties.dataSize
|
||||
|
||||
return <Button variant={variant}
|
||||
size={size}
|
||||
className={cn('!h-8 !px-3 select-none')}
|
||||
onClick={() => onSend?.(message)}
|
||||
>
|
||||
<span className='text-[13px]'>{node.children[0].value}</span>
|
||||
</Button>
|
||||
}
|
||||
MarkdownButton.displayName = 'MarkdownButton'
|
||||
|
||||
export function Markdown(props: { content: string; className?: string }) {
|
||||
const latexContent = preprocessLaTeX(props.content)
|
||||
return (
|
||||
|
@ -271,6 +288,7 @@ export function Markdown(props: { content: string; className?: string }) {
|
|||
audio: AudioBlock,
|
||||
a: Link,
|
||||
p: Paragraph,
|
||||
button: MarkdownButton,
|
||||
}}
|
||||
linkTarget='_blank'
|
||||
>
|
||||
|
|
|
@ -47,7 +47,9 @@ Chat applications support session persistence, allowing previous chat history to
|
|||
</Property>
|
||||
<Property name='inputs' type='object' key='inputs'>
|
||||
Allows the entry of various variable values defined by the App.
|
||||
The `inputs` parameter contains multiple key/value pairs, with each key corresponding to a specific variable and each value being the specific value for that variable. Default `{}`
|
||||
The `inputs` parameter contains multiple key/value pairs, with each key corresponding to a specific variable and each value being the specific value for that variable.
|
||||
If the variable is of file type, specify an object that has the keys described in `files` below.
|
||||
Default `{}`
|
||||
</Property>
|
||||
<Property name='response_mode' type='string' key='response_mode'>
|
||||
The mode of response return, supporting:
|
||||
|
@ -307,8 +309,8 @@ Chat applications support session persistence, allowing previous chat history to
|
|||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
Upload a file (currently only images are supported) for use when sending messages, enabling multimodal understanding of images and text.
|
||||
Supports png, jpg, jpeg, webp, gif formats.
|
||||
Upload a file for use when sending messages, enabling multimodal understanding of images and text.
|
||||
Supports any formats that are supported by your application.
|
||||
Uploaded files are for use by the current end-user only.
|
||||
|
||||
### Request Body
|
||||
|
|
|
@ -46,6 +46,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
|
|||
<Property name='inputs' type='object' key='inputs'>
|
||||
允许传入 App 定义的各变量值。
|
||||
inputs 参数包含了多组键值对(Key/Value pairs),每组的键对应一个特定变量,每组的值则是该变量的具体值。
|
||||
如果变量是文件类型,请指定一个包含以下 `files` 中所述键的对象。
|
||||
默认 `{}`
|
||||
</Property>
|
||||
<Property name='response_mode' type='string' key='response_mode'>
|
||||
|
@ -317,8 +318,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
|
|||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
上传文件(目前仅支持图片)并在发送消息时使用,可实现图文多模态理解。
|
||||
支持 png, jpg, jpeg, webp, gif 格式。
|
||||
上传文件并在发送消息时使用,可实现图文多模态理解。
|
||||
支持您的应用程序所支持的所有格式。
|
||||
<i>上传的文件仅供当前终端用户使用。</i>
|
||||
|
||||
### Request Body
|
||||
|
|
|
@ -44,6 +44,7 @@ Workflow applications offers non-session support and is ideal for translation, a
|
|||
Allows the entry of various variable values defined by the App.
|
||||
The `inputs` parameter contains multiple key/value pairs, with each key corresponding to a specific variable and each value being the specific value for that variable.
|
||||
The workflow application requires at least one key/value pair to be inputted.
|
||||
If the variable is of File type, specify an object that has the keys described in `files` below.
|
||||
- `response_mode` (string) Required
|
||||
The mode of response return, supporting:
|
||||
- `streaming` Streaming mode (recommended), implements a typewriter-like output through SSE ([Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)).
|
||||
|
@ -328,6 +329,81 @@ Workflow applications offers non-session support and is ideal for translation, a
|
|||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/files/upload'
|
||||
method='POST'
|
||||
title='File Upload'
|
||||
name='#file-upload'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
Upload a file for use when sending messages, enabling multimodal understanding of images and text.
|
||||
Supports any formats that are supported by your workflow.
|
||||
Uploaded files are for use by the current end-user only.
|
||||
|
||||
### Request Body
|
||||
This interface requires a `multipart/form-data` request.
|
||||
- `file` (File) Required
|
||||
The file to be uploaded.
|
||||
- `user` (string) Required
|
||||
User identifier, defined by the developer's rules, must be unique within the application.
|
||||
|
||||
### Response
|
||||
After a successful upload, the server will return the file's ID and related information.
|
||||
- `id` (uuid) ID
|
||||
- `name` (string) File name
|
||||
- `size` (int) File size (bytes)
|
||||
- `extension` (string) File extension
|
||||
- `mime_type` (string) File mime-type
|
||||
- `created_by` (uuid) End-user ID
|
||||
- `created_at` (timestamp) Creation timestamp, e.g., 1705395332
|
||||
|
||||
### Errors
|
||||
- 400, `no_file_uploaded`, a file must be provided
|
||||
- 400, `too_many_files`, currently only one file is accepted
|
||||
- 400, `unsupported_preview`, the file does not support preview
|
||||
- 400, `unsupported_estimate`, the file does not support estimation
|
||||
- 413, `file_too_large`, the file is too large
|
||||
- 415, `unsupported_file_type`, unsupported extension, currently only document files are accepted
|
||||
- 503, `s3_connection_failed`, unable to connect to S3 service
|
||||
- 503, `s3_permission_denied`, no permission to upload files to S3
|
||||
- 503, `s3_file_too_large`, file exceeds S3 size limit
|
||||
- 500, internal server error
|
||||
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
### Request Example
|
||||
<CodeGroup title="Request" tag="POST" label="/files/upload" targetCode={`curl -X POST '${props.appDetail.api_base_url}/files/upload' \\\n--header 'Authorization: Bearer {api_key}' \\\n--form 'file=@localfile;type=image/[png|jpeg|jpg|webp|gif] \\\n--form 'user=abc-123'`}>
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl -X POST '${props.appDetail.api_base_url}/files/upload' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--form 'file=@"/path/to/file"'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
### Response Example
|
||||
<CodeGroup title="Response">
|
||||
```json {{ title: 'Response' }}
|
||||
{
|
||||
"id": "72fa9618-8f89-4a37-9b33-7e1178a24a67",
|
||||
"name": "example.png",
|
||||
"size": 1024,
|
||||
"extension": "png",
|
||||
"mime_type": "image/png",
|
||||
"created_by": "6ad1ab0a-73ff-4ac1-b9e4-cdb312f71f13",
|
||||
"created_at": 1577836800,
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/parameters'
|
||||
method='GET'
|
||||
|
|
|
@ -42,6 +42,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
|
|||
- `inputs` (object) Required
|
||||
允许传入 App 定义的各变量值。
|
||||
inputs 参数包含了多组键值对(Key/Value pairs),每组的键对应一个特定变量,每组的值则是该变量的具体值。
|
||||
如果变量是文件类型,请指定一个包含以下 `files` 中所述键的对象。
|
||||
- `response_mode` (string) Required
|
||||
返回响应模式,支持:
|
||||
- `streaming` 流式模式(推荐)。基于 SSE(**[Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)**)实现类似打字机输出方式的流式返回。
|
||||
|
@ -324,6 +325,79 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
|
|||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/files/upload'
|
||||
method='POST'
|
||||
title='上传文件'
|
||||
name='#files-upload'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
上传文件并在发送消息时使用,可实现图文多模态理解。
|
||||
支持您的工作流程所支持的任何格式。
|
||||
<i>上传的文件仅供当前终端用户使用。</i>
|
||||
|
||||
### Request Body
|
||||
该接口需使用 `multipart/form-data` 进行请求。
|
||||
<Properties>
|
||||
<Property name='file' type='file' key='file'>
|
||||
要上传的文件。
|
||||
</Property>
|
||||
<Property name='user' type='string' key='user'>
|
||||
用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### Response
|
||||
成功上传后,服务器会返回文件的 ID 和相关信息。
|
||||
- `id` (uuid) ID
|
||||
- `name` (string) 文件名
|
||||
- `size` (int) 文件大小(byte)
|
||||
- `extension` (string) 文件后缀
|
||||
- `mime_type` (string) 文件 mime-type
|
||||
- `created_by` (uuid) 上传人 ID
|
||||
- `created_at` (timestamp) 上传时间
|
||||
|
||||
### Errors
|
||||
- 400,`no_file_uploaded`,必须提供文件
|
||||
- 400,`too_many_files`,目前只接受一个文件
|
||||
- 400,`unsupported_preview`,该文件不支持预览
|
||||
- 400,`unsupported_estimate`,该文件不支持估算
|
||||
- 413,`file_too_large`,文件太大
|
||||
- 415,`unsupported_file_type`,不支持的扩展名,当前只接受文档类文件
|
||||
- 503,`s3_connection_failed`,无法连接到 S3 服务
|
||||
- 503,`s3_permission_denied`,无权限上传文件到 S3
|
||||
- 503,`s3_file_too_large`,文件超出 S3 大小限制
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="POST" label="/files/upload" targetCode={`curl -X POST '${props.appDetail.api_base_url}/files/upload' \\\n--header 'Authorization: Bearer {api_key}' \\\n--form 'file=@localfile;type=image/[png|jpeg|jpg|webp|gif] \\\n--form 'user=abc-123'`}>
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl -X POST '${props.appDetail.api_base_url}/files/upload' \
|
||||
--header 'Authorization: Bearer {api_key}' \
|
||||
--form 'file=@"/path/to/file"'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```json {{ title: 'Response' }}
|
||||
{
|
||||
"id": "72fa9618-8f89-4a37-9b33-7e1178a24a67",
|
||||
"name": "example.png",
|
||||
"size": 1024,
|
||||
"extension": "png",
|
||||
"mime_type": "image/png",
|
||||
"created_by": 123,
|
||||
"created_at": 1577836800,
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/parameters'
|
||||
method='GET'
|
||||
|
|
Loading…
Reference in New Issue
Block a user