从Dify源码学习flask开发

Dify 介绍

Dify 的后端使用 Python 编写,使用 Flask 框架。它使用 SQLAlchemy 作为 ORM,使用 Celery 作为任务队列。授权逻辑通过 Flask-login 进行处理。

  • 后端代码组织架构
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
[api/]
├── configs               // 配置信息
├── constants             // 常量设置
├── controllers           // API路由和请求处理逻辑
├── core                  // 核心应用和工具
├── docker                // Docker & 容器和相关配置
├── events                // 事件处理
├── extensions            // 插件
├── fields                // 序列化字段定义
├── libs                  // 可复用的库
├── migrations            // 数据库迁移脚本
├── models                // 数据库模型定义
├── services              // 业务处理逻辑
├── template              // 模板
├── tasks                 // 异步任务和后台作业处理
└── tests                 // 测试
  • 在配置文件中设置 DEBUG 变量,代码中判断为真时输出调试信息。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# /api/app_factory.py
for ext in extensions:
    short_name = ext.__name__.split(".")[-1]
    is_enabled = ext.is_enabled() if hasattr(ext, "is_enabled") else True
    if not is_enabled:
        if dify_config.DEBUG:
            logging.info(f"Skipped {short_name}")
        continue

    start_time = time.perf_counter()
    ext.init_app(app)
    end_time = time.perf_counter()
    if dify_config.DEBUG:
        logging.info(f"Loaded {short_name} ({round((end_time - start_time) * 1000, 2)} ms)")
  • 自定义类继承Flask。
1
2
3
4
5
6
# api/dify_app.py
from flask import Flask


class DifyApp(Flask):
    pass
  • 使用应用工厂设计模式,并且从 .env 文件加载配置。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# /api/app_factory.py
from dify_app import DifyApp


# ----------------------------
# Application Factory Function
# ----------------------------
def create_flask_app_with_configs() -> DifyApp:
    """
    create a raw flask app
    with configs loaded from .env file
    """
    dify_app = DifyApp(__name__)
    dify_app.config.from_mapping(dify_config.model_dump())

    # add before request hook
    @dify_app.before_request
    def before_request():
        # add an unique identifier to each request
        RecyclableContextVar.increment_thread_recycles()

    return dify_app
  • 使用 time.perf_counter() 计算函数运行时间

time.time() 的值是当前时间戳,取自计算机的系统时间(可能被人为更改),如果是计算时间差,应采用 time.perf_counter() ,这个函数返回的是程序启动的时间(Python 3.10之前)或系统已经运行的时间(Python 3.10 及以后)。

特性 time.time() time.perf_counter()
功能 返回自 Unix 纪元 以来的秒数 返回一个 高精度的性能计数器(performance counter)
数值来源 系统时钟(RTC/NTP) CPU/硬件计时器(TSC/HPET)
精度 通常毫秒级(1e-3) 通常纳秒级(1e-9)
用途 获取当前时间(时间戳) 高精度计时(基准测试)
受系统时间影响 是(可能跳跃/回退) 否(单调递增)
适用场景 日志、时间戳记录 代码性能分析、短时间间隔测量
数值示例 749884346.7837772 699648.9371005
1
2
3
4
5
6
7
8
9
# /api/app_factory.py
def create_app() -> DifyApp:
    start_time = time.perf_counter()
    app = create_flask_app_with_configs()
    initialize_extensions(app)
    end_time = time.perf_counter()
    if dify_config.DEBUG:
        logging.info(f"Finished create_app ({round((end_time - start_time) * 1000, 2)} ms)")
    return app
  • 统一扩展并集中注册

extensions 文件夹中创建若干插件模块。

可在类中声明 is_enabled函数,从配置文件中读取是否开启的配置。

声明 init_app 函数,供统一注册使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# api/extensions/ext_mail.py
import logging
from typing import Optional

from flask import Flask

from configs import dify_config
from dify_app import DifyApp


class Mail:
    def __init__(self):
        self._client = None
        self._default_send_from = None

    def is_inited(self) -> bool:
        return self._client is not None

    def init_app(self, app: Flask):
        mail_type = dify_config.MAIL_TYPE
        if not mail_type:
            logging.warning("MAIL_TYPE is not set")
            return

        if dify_config.MAIL_DEFAULT_SEND_FROM:
            self._default_send_from = dify_config.MAIL_DEFAULT_SEND_FROM

        match mail_type:
            case "resend":
                ...
            case "smtp":
                ...

def is_enabled() -> bool:
    return dify_config.MAIL_TYPE is not None and dify_config.MAIL_TYPE != ""


def init_app(app: DifyApp):
    mail.init_app(app)


mail = Mail()

在应用工厂中进行统一注册和初始化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# /api/app_factory.py
def initialize_extensions(app: DifyApp):
    from extensions import (
        ext_app_metrics,
        ext_blueprints,
        ...
        ext_mail,
        ext_warnings,
    )

    extensions = [
        ext_app_metrics,
        ext_blueprints,
        ...
        ext_mail,
        ext_warnings,
    ]
    for ext in extensions:
        short_name = ext.__name__.split(".")[-1]
        is_enabled = ext.is_enabled() if hasattr(ext, "is_enabled") else True
        if not is_enabled:
            if dify_config.DEBUG:
                logging.info(f"Skipped {short_name}")
            continue

        start_time = time.perf_counter()
        ext.init_app(app)
        end_time = time.perf_counter()
        if dify_config.DEBUG:
            logging.info(f"Loaded {short_name} ({round((end_time - start_time) * 1000, 2)} ms)")
  • 使用pydantic和dotenv文件进行项目的配置

dotenv文件部分内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# api/.env.example
# Your App secret key will be used for securely signing the session cookie
# Make sure you are changing this key for your deployment with a strong key.
# You can generate a strong key using `openssl rand -base64 42`.
# Alternatively you can set it with `SECRET_KEY` environment variable.
SECRET_KEY=

# Console API base URL
CONSOLE_API_URL=http://127.0.0.1:5001
CONSOLE_WEB_URL=http://127.0.0.1:3000

# Service API base URL
SERVICE_API_URL=http://127.0.0.1:5001

# Web APP base URL
APP_WEB_URL=http://127.0.0.1:3000

# Files URL
FILES_URL=http://127.0.0.1:5001

# The time in seconds after the signature is rejected
FILES_ACCESS_TIMEOUT=300

# Access token expiration time in minutes
ACCESS_TOKEN_EXPIRE_MINUTES=60

# Refresh token expiration time in days
REFRESH_TOKEN_EXPIRE_DAYS=30
# ...

声明 DifyConfig 类,继承其他具体配置类,并指明从.env文件读取。

使用配置时,先 dify_config = DifyConfig() 实例化,然后使用 dify_config.XXX 即可。

其中 extra="ignore" 指定了只有在类变量中的配置才会从 .env 文件中读取并覆盖,只存在于 .env 文件中的不会被解析。

如果想解析只存在于 .env 文件中的配置,需要指定 extra="allow",并且在读取的时候使用 DifyConfig.model_extra[xxx] 格式,且 xxx 为小写(pydantic会自动将 .env 文件中的大写转换成小写)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# api/configs/app_config.py
class DifyConfig(
    # Packaging info
    PackagingInfo,
    # Deployment configs
    DeploymentConfig,
    ...
):
    model_config = SettingsConfigDict(
        # read from dotenv format config file
        env_file=".env",
        env_file_encoding="utf-8",
        # ignore extra attributes
        extra="ignore",
    )

其他具体配置类(以PackagingInfo为例)代码示例如下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# api/configs/packaging/__init__.py
from pydantic import Field
from pydantic_settings import BaseSettings


class PackagingInfo(BaseSettings):
    """
    Packaging build information
    """

    CURRENT_VERSION: str = Field(
        description="Dify version",
        default="1.4.2",
    )

    COMMIT_SHA: str = Field(
        description="SHA-1 checksum of the git commit used to build the app",
        default="",
    )
  • 自定义异常类,统一异常描述。

先定义一个异常基类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# api/libs/exception.py
from typing import Optional

from werkzeug.exceptions import HTTPException


class BaseHTTPException(HTTPException):
    error_code: str = "unknown"
    data: Optional[dict] = None

    def __init__(self, description=None, response=None):
        super().__init__(description, response)

        self.data = {
            "code": self.error_code,
            "message": self.description,
            "status": self.code,
        }

其他包中定义 error 模块,继承异常基类,实现具体异常类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# api/controllers/console/error.py
from libs.exception import BaseHTTPException


class AlreadySetupError(BaseHTTPException):
    error_code = "already_setup"
    description = "Dify has been successfully installed. Please refresh the page or return to the dashboard homepage."
    code = 403


class NotSetupError(BaseHTTPException):
    error_code = "not_setup"
    description = (
        "Dify has not been initialized and installed yet. "
        "Please proceed with the initialization and installation process first."
    )
    code = 401


class NotInitValidateError(BaseHTTPException):
    error_code = "not_init_validated"
    description = "Init validation has not been completed yet. Please proceed with the init validation process first."
    code = 401
updatedupdated2026-02-052026-02-05