掘金 人工智能 前天 17:08
【Dify(v1.x) 核心源码深入解析】File 模块
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入探讨了Dify文件处理模块的设计与实现,该模块支持多种文件类型和传输方式,并融入安全验证机制。通过文件模型定义、解析器、签名验证等关键组件,Dify实现了对文件的高效管理,确保了系统的稳定性和安全性。文章还详细介绍了文件URL的签名生成与验证,以及工具文件的管理方法,为开发者提供了宝贵的参考。

🖼️ 文件模型定义:Dify的文件处理模块基于核心的文件模型,该模型定义了文件的关键属性,包括文件类型(如图像、文档)、传输方法(远程URL、本地文件、工具文件)等。文件模型是整个文件处理流程的数据基础,确保了文件信息的结构化和规范化。

🔍 文件解析器:UploadFileParser 负责解析上传的文件,并根据配置决定以URL或Base64编码的方式提供图像数据。get_image_data方法是核心,它根据配置选择返回URL或Base64编码的图像数据,并生成带有签名的临时图像URL,确保了文件访问的安全性。

🔑 签名验证与生成:Dify通过helpers.py模块实现文件URL的签名生成与验证。get_signed_file_url函数生成带有签名的文件URL,而verify_plugin_file_signature函数则验证插件文件签名的有效性,这保证了文件传输的安全性,防止未授权访问。

⚙️ 工具文件管理:ToolFileParser 类用于管理工具文件,提供获取工具文件管理器的接口。通过get_tool_file_manager方法,可以方便地获取工具文件管理器实例,从而进行工具文件的相关操作,支持了系统对不同类型文件的处理需求。

重磅推荐专栏:《大模型AIGC》《课程大纲》《知识星球》

本专栏致力于探索和讨论当今最前沿的技术趋势和应用领域,包括但不限于ChatGPT和Stable Diffusion等。我们将深入研究大型模型的开发和应用,以及与之相关的人工智能生成内容(AIGC)技术。通过深入的技术解析和实践经验分享,旨在帮助读者更好地理解和应用这些领域的最新进展

一、引言

在复杂的软件系统中,文件处理模块往往扮演着至关重要的角色。Dify 的 file 模块精心设计,以应对多样化场景下的文件管理需求。它不仅支持多种文件类型与传输方式,还融入了安全验证机制与灵活的文件操作功能,为整个系统提供坚实的文件处理基础。

二、模块概览

Dify 的 file 模块由多个子模块协同工作,包括文件模型定义、文件解析、签名验证、文件生成与工具文件解析等。这些子模块相互配合,共同完成文件的处理流程。

(一) 核心组件介绍

    文件模型(File Model)

      定义了文件的核心属性,如文件类型、传输方法、相关标识符等,是整个文件处理流程的数据基础。

    文件解析器(UploadFileParser)

      负责解析上传的文件,根据配置决定以何种方式(URL 或 Base64)提供图像数据。

    工具文件解析器(ToolFileParser)

      专注于工具文件的管理,提供工具文件管理器的获取接口。

    签名验证与生成(helpers.py)

      包含文件 URL 签名的生成与验证逻辑,确保文件访问的安全性。

    文件管理器(file_manager.py)

      提供文件的高级操作,如将文件转换为提示消息内容、下载文件等。

三、文件模型(models.py)

(一) 文件类型与传输方法枚举

(二) 文件配置类

(三) 文件模型类(File)

class File(BaseModel):    dify_model_identity: str = FILE_MODEL_IDENTITY    id: Optional[str] = None  # message file id    tenant_id: str    type: FileType    transfer_method: FileTransferMethod    remote_url: Optional[str] = None  # remote url    related_id: Optional[str] = None    filename: Optional[str] = None    extension: Optional[str] = Field(default=None, description="File extension, should contains dot")    mime_type: Optional[str] = None    size: int = -1    # Those properties are private, should not be exposed to the outside.    _storage_key: str    def __init__(        self,        *,        id: Optional[str] = None,        tenant_id: str,        type: FileType,        transfer_method: FileTransferMethod,        remote_url: Optional[str] = None,        related_id: Optional[str] = None,        filename: Optional[str] = None,        extension: Optional[str] = None,        mime_type: Optional[str] = None,        size: int = -1,        storage_key: Optional[str] = None,        dify_model_identity: Optional[str] = FILE_MODEL_IDENTITY,        url: Optional[str] = None,    ):        super().__init__(            id=id,            tenant_id=tenant_id,            type=type,            transfer_method=transfer_method,            remote_url=remote_url,            related_id=related_id,            filename=filename,            extension=extension,            mime_type=mime_type,            size=size,            dify_model_identity=dify_model_identity,            url=url,        )        self._storage_key = str(storage_key)    def to_dict(self) -> Mapping[str, str | int | None]:        data = self.model_dump(mode="json")        return {            **data,            "url": self.generate_url(),        }    @property    def markdown(self) -> str:        url = self.generate_url()        if self.type == FileType.IMAGE:            text = f"![{self.filename or ''}]({url})"        else:            text = f"[{self.filename or url}]({url})"        return text    def generate_url(self) -> Optional[str]:        if self.transfer_method == FileTransferMethod.REMOTE_URL:            return self.remote_url        elif self.transfer_method == FileTransferMethod.LOCAL_FILE:            if self.related_id is None:                raise ValueError("Missing file related_id")            return helpers.get_signed_file_url(upload_file_id=self.related_id)        elif self.transfer_method == FileTransferMethod.TOOL_FILE:            assert self.related_id is not None            assert self.extension is not None            return ToolFileParser.get_tool_file_manager().sign_file(                tool_file_id=self.related_id, extension=self.extension            )    def to_plugin_parameter(self) -> dict[str, Any]:        return {            "dify_model_identity": FILE_MODEL_IDENTITY,            "mime_type": self.mime_type,            "filename": self.filename,            "extension": self.extension,            "size": self.size,            "type": self.type,            "url": self.generate_url(),        }    @model_validator(mode="after")    def validate_after(self):        match self.transfer_method:            case FileTransferMethod.REMOTE_URL:                if not self.remote_url:                    raise ValueError("Missing file url")                if not isinstance(self.remote_url, str) or not self.remote_url.startswith("http"):                    raise ValueError("Invalid file url")            case FileTransferMethod.LOCAL_FILE:                if not self.related_id:                    raise ValueError("Missing file related_id")            case FileTransferMethod.TOOL_FILE:                if not self.related_id:                    raise ValueError("Missing file related_id")        return self

(四) 类图

classDiagram    class File {        +id str        +tenant_id str        +type FileType        +transfer_method FileTransferMethod        +remote_url str        +related_id str        +filename str        +extension str        +mime_type str        +size int        +_storage_key str        +to_dict() dict        +markdown str        +generate_url() str        +to_plugin_parameter() dict    }    class FileType {        +IMAGE str        +DOCUMENT str        +AUDIO str        +VIDEO str        +CUSTOM str    }    class FileTransferMethod {        +REMOTE_URL str        +LOCAL_FILE str        +TOOL_FILE str    }    File "1" -- "1" FileType: type    File "1" -- "1" FileTransferMethod: transfer_method

四、文件解析器(upload_file_parser.py)

(一) 功能概述

UploadFileParser 类专注于解析上传的文件,特别是图像文件。它根据系统配置决定以 URL 或 Base64 的形式提供图像数据,并且能够生成带有签名的临时图像 URL。

(二) 关键方法详解

    get_image_data 方法

      用于获取图像数据。首先检查文件是否存在且为支持的图像格式。根据配置(dify_config.MULTIMODAL_SEND_FORMAT)或强制参数(force_url)决定返回 URL 还是 Base64 编码的数据。如果选择 URL 方式,调用 get_signed_temp_image_url 方法获取签名 URL;如果选择 Base64 方式,从存储中加载文件数据并进行 Base64 编码。
@classmethoddef get_image_data(cls, upload_file, force_url: bool = False) -> Optional[str]:    if not upload_file:        return None    if upload_file.extension not in IMAGE_EXTENSIONS:        return None    if dify_config.MULTIMODAL_SEND_FORMAT == "url" or force_url:        return cls.get_signed_temp_image_url(upload_file.id)    else:        # get image file base64        try:            data = storage.load(upload_file.key)        except FileNotFoundError:            logging.exception(f"File not found: {upload_file.key}")            return None        encoded_string = base64.b64encode(data).decode("utf-8")        return f"data:{upload_file.mime_type};base64,{encoded_string}"

    get_signed_temp_image_url 方法

      根据文件 ID 生成带有签名的临时图像 URL。利用 UrlSigner 类的 get_signed_url 方法完成签名过程。
@classmethoddef get_signed_temp_image_url(cls, upload_file_id) -> str:    base_url = dify_config.FILES_URL    image_preview_url = f"{base_url}/files/{upload_file_id}/image-preview"    return UrlSigner.get_signed_url(url=image_preview_url, sign_key=upload_file_id, prefix="image-preview")

    verify_image_file_signature 方法

      验证图像文件签名的有效性。首先调用 UrlSigner 类的 verify 方法进行签名验证,然后检查时间戳是否在允许的时间范围内。
@classmethoddef verify_image_file_signature(cls, upload_file_id: str, timestamp: str, nonce: str, sign: str) -> bool:    result = UrlSigner.verify(        sign_key=upload_file_id, timestamp=timestamp, nonce=nonce, sign=sign, prefix="image-preview"    )    # verify signature    if not result:        return False    current_time = int(time.time())    return current_time - int(timestamp) <= dify_config.FILES_ACCESS_TIMEOUT

五、工具文件解析器(tool_file_parser.py)

(一) 功能描述

ToolFileParser 类主要用于管理工具文件,提供获取工具文件管理器的接口。

(二) 关键代码解读

class ToolFileParser:    @staticmethod    def get_tool_file_manager() -> "ToolFileManager":        return cast("ToolFileManager", tool_file_manager["manager"])

六、签名生成与验证(helpers.py)

(一) 签名生成函数

    get_signed_file_url 函数

      生成带有签名的文件 URL。构造 URL 路径,生成时间戳和随机数(nonce),利用 HMAC-SHA256 算法对特定字符串进行签名,最后将签名编码并附加到 URL 上。
def get_signed_file_url(upload_file_id: str) -> str:    url = f"{dify_config.FILES_URL}/files/{upload_file_id}/file-preview"    timestamp = str(int(time.time()))    nonce = os.urandom(16).hex()    key = dify_config.SECRET_KEY.encode()    msg = f"file-preview|{upload_file_id}|{timestamp}|{nonce}"    sign = hmac.new(key, msg.encode(), hashlib.sha256).digest()    encoded_sign = base64.urlsafe_b64encode(sign).decode()    return f"{url}?timestamp={timestamp}&nonce={nonce}&sign={encoded_sign}"

    get_signed_file_url_for_plugin 函数

      为插件生成带有签名的文件上传 URL。与 get_signed_file_url 函数类似,但构造的 URL 路径不同,并且在签名字符串中包含了更多的参数(如文件名、MIME 类型、租户 ID 和用户 ID)。
def get_signed_file_url_for_plugin(filename: str, mimetype: str, tenant_id: str, user_id: str) -> str:    url = f"{dify_config.FILES_URL}/files/upload/for-plugin"    if user_id is None:        user_id = "DEFAULT-USER"    timestamp = str(int(time.time()))    nonce = os.urandom(16).hex()    key = dify_config.SECRET_KEY.encode()    msg = f"upload|{filename}|{mimetype}|{tenant_id}|{user_id}|{timestamp}|{nonce}"    sign = hmac.new(key, msg.encode(), hashlib.sha256).digest()    encoded_sign = base64.urlsafe_b64encode(sign).decode()    return f"{url}?timestamp={timestamp}&nonce={nonce}&sign={encoded_sign}&user_id={user_id}&tenant_id={tenant_id}"

(二) 签名验证函数

    verify_plugin_file_signature 函数

      验证插件文件签名的有效性。重新计算签名并将其与传入的签名进行比较,同时检查时间戳是否在允许的时间范围内。
def verify_plugin_file_signature(    *, filename: str, mimetype: str, tenant_id: str, user_id: str | None, timestamp: str, nonce: str, sign: str) -> bool:    if user_id is None:        user_id = "DEFAULT-USER"    data_to_sign = f"upload|{filename}|{mimetype}|{tenant_id}|{user_id}|{timestamp}|{nonce}"    secret_key = dify_config.SECRET_KEY.encode()    recalculated_sign = hmac.new(secret_key, data_to_sign.encode(), hashlib.sha256).digest()    recalculated_encoded_sign = base64.urlsafe_b64encode(recalculated_sign).decode()    # verify signature    if sign != recalculated_encoded_sign:        return False    current_time = int(time.time())    return current_time - int(timestamp) <= dify_config.FILES_ACCESS_TIMEOUT

    verify_image_signature 函数

      验证图像文件签名的有效性。与 verify_plugin_file_signature 函数类似,但签名字符串的构造方式略有不同。
def verify_image_signature(*, upload_file_id: str, timestamp: str, nonce: str, sign: str) -> bool:    data_to_sign = f"image-preview|{upload_file_id}|{timestamp}|{nonce}"    secret_key = dify_config.SECRET_KEY.encode()    recalculated_sign = hmac.new(secret_key, data_to_sign.encode(), hashlib.sha256).digest()    recalculated_encoded_sign = base64.urlsafe_b64encode(recalculated_sign).decode()    # verify signature    if sign != recalculated_encoded_sign:        return False    current_time = int(time.time())    return current_time - int(timestamp) <= dify_config.FILES_ACCESS_TIMEOUT

    verify_file_signature 函数

      验证文件签名的有效性。与 verify_image_signature 函数类似,适用于通用文件的签名验证。
def verify_file_signature(*, upload_file_id: str, timestamp: str, nonce: str, sign: str) -> bool:    data_to_sign = f"file-preview|{upload_file_id}|{timestamp}|{nonce}"    secret_key = dify_config.SECRET_KEY.encode()    recalculated_sign = hmac.new(secret_key, data_to_sign.encode(), hashlib.sha256).digest()    recalculated_encoded_sign = base64.urlsafe_b64encode(recalculated_sign).decode()    # verify signature    if sign != recalculated_encoded_sign:        return False    current_time = int(time.time())    return current_time - int(timestamp) <= dify_config.FILES_ACCESS_TIMEOUT

(三) 流程图

graph TD    A[生成文件 URL 签名] --> B{选择签名类型}    B -->|image-preview| C[构造 image-preview 签名字符串]    B -->|upload| D[构造 upload 签名字符串]    B -->|file-preview| E[构造 file-preview 签名字符串]    C --> F[计算 HMAC-SHA256 签名]    D --> F    E --> F    F --> G[将签名编码并附加到 URL]    G --> H[返回带有签名的 URL]

七、文件管理器(file_manager.py)

(一) 功能概述

File Manager 模块提供了对文件的高级操作,如获取文件属性、将文件转换为提示消息内容、下载文件等。

(二) 关键方法详解

    get_attr 函数

      根据文件和指定的属性,获取文件的相应属性值。支持获取文件类型、大小、名称、MIME 类型、传输方法、URL、扩展名和相关 ID。
def get_attr(*, file: File, attr: FileAttribute):    match attr:        case FileAttribute.TYPE:            return file.type.value        case FileAttribute.SIZE:            return file.size        case FileAttribute.NAME:            return file.filename        case FileAttribute.MIME_TYPE:            return file.mime_type        case FileAttribute.TRANSFER_METHOD:            return file.transfer_method.value        case FileAttribute.URL:            return file.remote_url        case FileAttribute.EXTENSION:            return file.extension        case FileAttribute.RELATED_ID:            return file.related_id

    to_prompt_message_content 函数

      将文件转换为提示消息内容。根据文件类型选择相应的提示消息内容类(如图像、音频、视频或文档),并将文件的相关信息(如 Base64 数据、URL、格式和 MIME 类型)传递给该类以生成提示消息内容。
def to_prompt_message_content(    f: File,    /,    *,    image_detail_config: ImagePromptMessageContent.DETAIL | None = None,) -> MultiModalPromptMessageContent:    if f.extension is None:        raise ValueError("Missing file extension")    if f.mime_type is None:        raise ValueError("Missing file mime_type")    params = {        "base64_data": _get_encoded_string(f) if dify_config.MULTIMODAL_SEND_FORMAT == "base64" else "",        "url": _to_url(f) if dify_config.MULTIMODAL_SEND_FORMAT == "url" else "",        "format": f.extension.removeprefix("."),        "mime_type": f.mime_type,    }    if f.type == FileType.IMAGE:        params["detail"] = image_detail_config or ImagePromptMessageContent.DETAIL.LOW    prompt_class_map: Mapping[FileType, type[MultiModalPromptMessageContent]] = {        FileType.IMAGE: ImagePromptMessageContent,        FileType.AUDIO: AudioPromptMessageContent,        FileType.VIDEO: VideoPromptMessageContent,        FileType.DOCUMENT: DocumentPromptMessageContent,    }    try:        return prompt_class_map[f.type].model_validate(params)    except KeyError:        raise ValueError(f"file type {f.type} is not supported")

    download 函数

      下载文件。根据文件的传输方法,分别处理远程 URL、本地文件和工具文件的下载。
def download(f: File, /):    if f.transfer_method in (FileTransferMethod.TOOL_FILE, FileTransferMethod.LOCAL_FILE):        return _download_file_content(f._storage_key)    elif f.transfer_method == FileTransferMethod.REMOTE_URL:        response = ssrf_proxy.get(f.remote_url, follow_redirects=True)        response.raise_for_status()        return response.content    raise ValueError(f"unsupported transfer method: {f.transfer_method}")

    _get_encoded_string 函数

      获取文件的 Base64 编码字符串。根据文件的传输方法,从相应的位置获取文件数据(如远程 URL 或存储),然后进行 Base64 编码。
def _get_encoded_string(f: File, /):    match f.transfer_method:        case FileTransferMethod.REMOTE_URL:            response = ssrf_proxy.get(f.remote_url, follow_redirects=True)            response.raise_for_status()            data = response.content        case FileTransferMethod.LOCAL_FILE:            data = _download_file_content(f._storage_key)        case FileTransferMethod.TOOL_FILE:            data = _download_file_content(f._storage_key)    encoded_string = base64.b64encode(data).decode("utf-8")    return encoded_string

    _to_url 函数

      将文件转换为 URL。根据文件的传输方法,生成相应的 URL,包括远程 URL、本地文件的签名 URL 和工具文件的签名 URL。
def _to_url(f: File, /):    if f.transfer_method == FileTransferMethod.REMOTE_URL:        if f.remote_url is None:            raise ValueError("Missing file remote_url")        return f.remote_url    elif f.transfer_method == FileTransferMethod.LOCAL_FILE:        if f.related_id is None:            raise ValueError("Missing file related_id")        return f.remote_url or helpers.get_signed_file_url(upload_file_id=f.related_id)    elif f.transfer_method == FileTransferMethod.TOOL_FILE:        # add sign url        if f.related_id is None or f.extension is None:            raise ValueError("Missing file related_id or extension")        return ToolFileParser.get_tool_file_manager().sign_file(tool_file_id=f.related_id, extension=f.extension)    else:        raise ValueError(f"Unsupported transfer method: {f.transfer_method}")

(三) 时序图

sequenceDiagram    participant 调用者    participant 文件管理器    participant 存储    participant 网络代理    调用者->>文件管理器: 调用 download 方法    文件管理器->>文件管理器: 判断传输方法    alt 远程 URL        文件管理器->>网络代理: 通过 ssrf_proxy 获取远程 URL 内容        网络代理-->>文件管理器: 返回响应内容    else 本地文件或工具文件        文件管理器->>存储: 调用 _download_file_content 方法获取文件内容        存储-->>文件管理器: 返回文件数据    end    文件管理器-->>调用者: 返回文件内容

八、枚举与常量(enums.py 和 constants.py)

(一) 枚举类

    FileType 枚举 :定义了支持的文件类型。FileTransferMethod 枚举 :规定了文件传输方式。FileBelongsTo 枚举 :表示文件所属(用户或助手)。FileAttribute 枚举 :列举了文件的属性。ArrayFileAttribute 枚举 :定义了数组文件属性。

(二) 常量

from enum import StrEnumclass FileType(StrEnum):    IMAGE = "image"    DOCUMENT = "document"    AUDIO = "audio"    VIDEO = "video"    CUSTOM = "custom"    @staticmethod    def value_of(value):        for member in FileType:            if member.value == value:                return member        raise ValueError(f"No matching enum found for value '{value}'")class FileTransferMethod(StrEnum):    REMOTE_URL = "remote_url"    LOCAL_FILE = "local_file"    TOOL_FILE = "tool_file"    @staticmethod    def value_of(value):        for member in FileTransferMethod:            if member.value == value:                return member        raise ValueError(f"No matching enum found for value '{value}'")class FileBelongsTo(StrEnum):    USER = "user"    ASSISTANT = "assistant"    @staticmethod    def value_of(value):        for member in FileBelongsTo:            if member.value == value:                return member        raise ValueError(f"No matching enum found for value '{value}'")class FileAttribute(StrEnum):    TYPE = "type"    SIZE = "size"    NAME = "name"    MIME_TYPE = "mime_type"    TRANSFER_METHOD = "transfer_method"    URL = "url"    EXTENSION = "extension"    RELATED_ID = "related_id"class ArrayFileAttribute(StrEnum):    LENGTH = "length"
FILE_MODEL_IDENTITY = "__dify__file__"

九、总结与展望

Dify 的 file 模块通过精心设计的架构和丰富的功能,为系统的文件处理提供了强大的支持。文件模型定义了文件的核心属性,文件解析器和工具文件解析器分别处理不同类型文件的解析需求,签名验证与生成确保了文件访问的安全性,文件管理器提供了文件的高级操作功能。枚举与常量的定义则为模块的使用提供了清晰的规范。

在未来的发展中,Dify 的 file 模块可能会进一步优化性能,扩展对更多文件类型的支持,并加强对文件安全性和完整性的保护。同时,随着系统功能的不断丰富,file 模块也可能会与更多的模块进行深度集成,以满足更加复杂的业务需求。

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Dify 文件处理 文件安全 签名验证
相关文章