本专栏致力于探索和讨论当今最前沿的技术趋势和应用领域,包括但不限于ChatGPT和Stable Diffusion等。我们将深入研究大型模型的开发和应用,以及与之相关的人工智能生成内容(AIGC)技术。通过深入的技术解析和实践经验分享,旨在帮助读者更好地理解和应用这些领域的最新进展
一、引言
在复杂的软件系统中,文件处理模块往往扮演着至关重要的角色。Dify 的 file 模块精心设计,以应对多样化场景下的文件管理需求。它不仅支持多种文件类型与传输方式,还融入了安全验证机制与灵活的文件操作功能,为整个系统提供坚实的文件处理基础。
二、模块概览
Dify 的 file 模块由多个子模块协同工作,包括文件模型定义、文件解析、签名验证、文件生成与工具文件解析等。这些子模块相互配合,共同完成文件的处理流程。
(一) 核心组件介绍
文件模型(File Model)
- 定义了文件的核心属性,如文件类型、传输方法、相关标识符等,是整个文件处理流程的数据基础。
文件解析器(UploadFileParser)
- 负责解析上传的文件,根据配置决定以何种方式(URL 或 Base64)提供图像数据。
工具文件解析器(ToolFileParser)
- 专注于工具文件的管理,提供工具文件管理器的获取接口。
签名验证与生成(helpers.py)
- 包含文件 URL 签名的生成与验证逻辑,确保文件访问的安全性。
文件管理器(file_manager.py)
- 提供文件的高级操作,如将文件转换为提示消息内容、下载文件等。
三、文件模型(models.py)
(一) 文件类型与传输方法枚举
- FileType 枚举 :定义了支持的文件类型,如图像(IMAGE)、文档(DOCUMENT)、音频(AUDIO)、视频(VIDEO)和自定义(CUSTOM)。FileTransferMethod 枚举 :规定了文件传输方式,包括远程 URL(REMOTE_URL)、本地文件(LOCAL_FILE)和工具文件(TOOL_FILE)。
(二) 文件配置类
- ImageConfig :用于存储图像上传相关的配置,如数量限制、传输方法和详细信息。FileUploadConfig :包含了文件上传的全面配置,涵盖图像配置、允许的文件类型、扩展名、上传方法和数量限制。
(三) 文件模型类(File)
- 核心属性 :包括文件标识符(id)、所属租户(tenant_id)、文件类型(type)、传输方法(transfer_method)、远程 URL(remote_url)、相关 ID(related_id)、文件名(filename)、扩展名(extension)、MIME 类型(mime_type)、大小(size)和存储键(_storage_key)。方法 :提供了将文件转换为字典、生成 Markdown 表示、生成文件 URL 以及转换为插件参数等方法。
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"" 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"])
- get_tool_file_manager 方法 :静态方法,从 tool_file_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 枚举 :定义了数组文件属性。
(二) 常量
- FILE_MODEL_IDENTITY 常量 :标识文件模型。
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 模块也可能会与更多的模块进行深度集成,以满足更加复杂的业务需求。