本专栏致力于探索和讨论当今最前沿的技术趋势和应用领域,包括但不限于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
(四) 类图
四、文件解析器(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 编码。
@classmethod
def 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 方法完成签名过程。
@classmethod
def 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 方法进行签名验证,然后检查时间戳是否在允许的时间范围内。
@classmethod
def 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
(三) 流程图
七、文件管理器(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}")
(三) 时序图
八、枚举与常量(enums.py 和 constants.py)
(一) 枚举类
- FileType 枚举 :定义了支持的文件类型。
- FileTransferMethod 枚举 :规定了文件传输方式。
- FileBelongsTo 枚举 :表示文件所属(用户或助手)。
- FileAttribute 枚举 :列举了文件的属性。
- ArrayFileAttribute 枚举 :定义了数组文件属性。
(二) 常量
- FILE_MODEL_IDENTITY 常量 :标识文件模型。
from enum import StrEnum
class 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 模块也可能会与更多的模块进行深度集成,以满足更加复杂的业务需求。