Repository: SwanHubX/SwanLab (language: python)

Issue:
refactor callbacks
## Description

* 对测试脚本做了更新，区分于正式环境，相关配置可找 @SAKURA-CAT 要，具体更新可看`create_experiment.py`的改动
* 对swanlab内部回调做了一些重构，这将解藕swanlab本体与上传服务本身，区分云端模式与本地模式，方便后续分包
* 重构的回调也支持用户自定义，不过目前没有这需求，暂时不管
* close #490 
* 删除了一直没有使用的代码文件`model.py`等
[REQUEST] swanlab.init 增加mode参数，支持设置disable模式（不做任何记录）
```python
import swanlab

swanlab.init(
  mode="disable"
)
```

- mode有三种类型: "cloud"、"local"、"disable"，默认为"cloud"。
- 与"cloud"参数的关系：是cloud参数的升级版，所以cloud参数可能得在未来被废弃

disable模式开启后，将不再打印swanlab信息，也不会在本地记录任何内容。


Discussion / hints:
由于测试脚本的改动，所以有关mp的测试脚本也需要改动一下，很简单，运行一个`open_dev_mode`函数即可 @ShaohonChen 


Fix patch:
diff --git a/swanlab/api/http.py b/swanlab/api/http.py
index d0a26165..16548582 100644
--- a/swanlab/api/http.py
+++ b/swanlab/api/http.py
@@ -252,8 +252,7 @@ def create_http(login_info: LoginInfo) -> HTTP:
     创建http请求对象
     """
     global http
-    if http is None:
-        http = HTTP(login_info)
+    http = HTTP(login_info)
     return http
 
 
diff --git a/swanlab/api/info.py b/swanlab/api/info.py
index c10a7a74..69d82dcf 100644
--- a/swanlab/api/info.py
+++ b/swanlab/api/info.py
@@ -80,7 +80,7 @@ def __str__(self) -> str:
         """错误时会返回错误信息"""
         if self.__resp.reason == "OK":
             return "Login success"
-        if self.__resp.reason == "Unauthorized":
+        if self.__resp.reason == "Unauthorized" or self.__resp.reason == "Authorization Required":
             return "Error api key"
         if self.__resp.reason == "Forbidden":
             return "You need to be verified first"
diff --git a/swanlab/data/callback_cloud.py b/swanlab/data/callback_cloud.py
index 3f44377e..b18bd2a8 100644
--- a/swanlab/data/callback_cloud.py
+++ b/swanlab/data/callback_cloud.py
@@ -7,9 +7,8 @@
 @Description:
     云端回调
 """
-from .run.callback import NewKeyInfo
+from .run.callback import MetricInfo, ColumnInfo
 from swanlab.cloud import UploadType
-from typing import Optional, Dict
 from swanlab.error import ApiError
 from swanlab.api.upload.model import ColumnModel
 from urllib.parse import quote
@@ -45,12 +44,29 @@ def __init__(self):
         标记是否正在退出云端环境
         """
 
-    def before_init_project(self, project: str, workspace: str, *args, **kwargs) -> int:
-        if self.login_info is None:
-            swanlog.debug("Login info is None, get login info.")
-            self.login_info = self.get_login_info()
-        http = create_http(self.login_info)
-        return http.mount_project(project, workspace).history_exp_count
+    @classmethod
+    def get_login_info(cls):
+        """
+        发起登录，获取登录信息，执行此方法会覆盖原有的login_info
+        """
+        key = None
+        try:
+            key = get_key(os.path.join(get_swanlab_folder(), ".netrc"), get_host_api())[2]
+        except KeyFileError:
+            fd = sys.stdin.fileno()
+            # 不是标准终端，且非jupyter环境，无法控制其回显
+            if not os.isatty(fd) and not in_jupyter():
+                raise KeyFileError("The key file is not found, call `swanlab.login()` or use `swanlab login` ")
+        return terminal_login(key)
+
+    def _view_web_print(self):
+        self._watch_tip_print()
+        http = get_http()
+        project_url = get_host_web() + f"/@{http.groupname}/{http.projname}"
+        experiment_url = project_url + f"/runs/{http.exp_id}"
+        swanlog.info("🏠 View project at " + FONT.blue(FONT.underline(project_url)))
+        swanlog.info("🚀 View run at " + FONT.blue(FONT.underline(experiment_url)))
+        return experiment_url
 
     def _clean_handler(self):
         run = get_run()
@@ -74,16 +90,18 @@ def _except_handler(self, tp, val, tb):
         if tp != KeyboardInterrupt:
             raise tp(val)
 
-    def _view_web_print(self):
-        self._watch_tip_print()
-        http = get_http()
-        project_url = get_host_web() + f"/@{http.groupname}/{http.projname}"
-        experiment_url = project_url + f"/runs/{http.exp_id}"
-        swanlog.info("🏠 View project at " + FONT.blue(FONT.underline(project_url)))
-        swanlog.info("🚀 View run at " + FONT.blue(FONT.underline(experiment_url)))
-        return experiment_url
+    def __str__(self):
+        return "SwanLabCloudRunCallback"
 
-    def on_train_begin(self):
+    def on_init(self, project: str, workspace: str, logdir: str = None) -> int:
+        super(CloudRunCallback, self).on_init(project, workspace, logdir)
+        if self.login_info is None:
+            swanlog.debug("Login info is None, get login info.")
+            self.login_info = self.get_login_info()
+        http = create_http(self.login_info)
+        return http.mount_project(project, workspace).history_exp_count
+
+    def on_run(self):
         # 注册实验信息
         try:
             get_http().mount_exp(
@@ -120,7 +138,27 @@ def _write_call_call(message):
         if in_jupyter():
             show_button_html(experiment_url)
 
-    def on_train_end(self, error: str = None):
+    def on_column_create(self, column_info: ColumnInfo):
+        self.pool.queue.put((
+            UploadType.COLUMN,
+            [ColumnModel(column_info.key, column_info.data_type.upper(), column_info.error)]
+        ))
+
+    def on_metric_create(self, metric_info: MetricInfo):
+        super(CloudRunCallback, self).on_metric_create(metric_info)
+        if metric_info.error:
+            return
+        new_data = metric_info.metric
+        new_data['key'] = metric_info.key
+        new_data['index'] = metric_info.step
+        new_data['epoch'] = metric_info.epoch
+        if metric_info.data_type == "default":
+            return self.pool.queue.put((UploadType.SCALAR_METRIC, [new_data]))
+        key = quote(metric_info.key, safe="")
+        data = (new_data, key, metric_info.data_type, metric_info.static_dir)
+        self.pool.queue.put((UploadType.MEDIA_METRIC, [data]))
+
+    def on_stop(self, error: str = None):
         # 打印信息
         self._view_web_print()
         run = get_run()
@@ -145,40 +183,3 @@ def _():
         # 取消注册系统回调
         self._unregister_sys_callback()
         self.exiting = False
-
-    def on_metric_create(self, key: str, key_info: NewKeyInfo, static_dir: str):
-        """
-        指标创建回调函数,新增指标信息时调用
-        :param key: 指标key名称
-        :param key_info: 指标信息
-        :param static_dir: 媒体文件目录
-        """
-        if key_info is None:
-            return
-        new_data, data_type, step, epoch = key_info
-        new_data['key'] = key
-        new_data['index'] = step
-        new_data['epoch'] = epoch
-        if data_type == "default":
-            return self.pool.queue.put((UploadType.SCALAR_METRIC, [new_data]))
-        key = quote(key, safe="")
-        data = (new_data, key, data_type, static_dir)
-        self.pool.queue.put((UploadType.MEDIA_METRIC, [data]))
-
-    def on_column_create(self, key, data_type: str, error: Optional[Dict] = None):
-        self.pool.queue.put((UploadType.COLUMN, [ColumnModel(key, data_type.upper(), error)]))
-
-    @classmethod
-    def get_login_info(cls):
-        """
-        发起登录，获取登录信息，执行此方法会覆盖原有的login_info
-        """
-        key = None
-        try:
-            key = get_key(os.path.join(get_swanlab_folder(), ".netrc"), get_host_api())[2]
-        except KeyFileError:
-            fd = sys.stdin.fileno()
-            # 不是标准终端，且非jupyter环境，无法控制其回显
-            if not os.isatty(fd) and not in_jupyter():
-                raise KeyFileError("The key file is not found, call `swanlab.login()` or use `swanlab login` ")
-        return terminal_login(key)
diff --git a/swanlab/data/callback_local.py b/swanlab/data/callback_local.py
index 550bc6b0..57aacd67 100644
--- a/swanlab/data/callback_local.py
+++ b/swanlab/data/callback_local.py
@@ -7,11 +7,17 @@
 @Description:
     基本回调函数注册表，此时不考虑云端情况
 """
+from typing import Callable
 from swanlab.log import swanlog
 from swanlab.utils.font import FONT
 from swanlab.data.run.main import get_run, SwanLabRunState
-from swanlab.data.run.callback import SwanLabRunCallback
+from swanlab.data.run.callback import SwanLabRunCallback, MetricInfo
+from swanlab.data.system import get_system_info, get_requirements
+from swanlab.env import ROOT
+from datetime import datetime
 import traceback
+import json
+import os
 
 
 class LocalRunCallback(SwanLabRunCallback):
@@ -41,8 +47,49 @@ def _error_print(tp):
         else:
             swanlog.error("Error happened while training")
 
-    def before_init_project(self, *args, **kwargs):
-        pass
+    @staticmethod
+    def _init_logdir(logdir: str = None) -> str:
+        """
+        根据传入的logdir，初始化日志文件夹
+        FIXME shit code
+        """
+        # 如果传入了logdir，则将logdir设置为环境变量，代表日志文件存放的路径
+        if logdir is not None:
+            try:
+                if not isinstance(logdir, str):
+                    raise ValueError("path must be a string")
+                if not os.path.isabs(logdir):
+                    logdir = os.path.abspath(logdir)
+                # 如果创建失败，也是抛出IOError
+                try:
+                    os.makedirs(logdir, exist_ok=True)
+                except Exception as e:
+                    raise IOError(f"create path: {logdir} failed, error: {e}")
+                if not os.access(logdir, os.W_OK):
+                    raise IOError(f"no write permission for path: {logdir}")
+            except ValueError:
+                raise ValueError("logdir must be a str.")
+            except IOError:
+                raise IOError("logdir must be a path and have Write permission.")
+            os.environ[ROOT] = logdir
+        # 如果没有传入logdir，则使用默认的logdir, 即当前工作目录下的swanlog文件夹，但是需要保证目录存在
+        else:
+            logdir = os.environ.get(ROOT) or os.path.join(os.getcwd(), "swanlog")
+            logdir = os.path.abspath(logdir)
+            try:
+                os.makedirs(logdir, exist_ok=True)
+                if not os.access(logdir, os.W_OK):
+                    raise IOError
+            except IOError:
+                raise IOError("logdir must have Write permission.")
+        # 如果logdir是空的，创建.gitignore文件，写入*
+        if not os.listdir(logdir):
+            with open(os.path.join(logdir, ".gitignore"), "w") as f:
+                f.write("*")
+        return logdir
+
+    def __str__(self):
+        return "SwanLabLocalRunCallback"
 
     def _except_handler(self, tp, val, tb):
         """
@@ -62,29 +109,59 @@ def _clean_handler(self):
         # 如果正在运行
         run.finish() if run.is_running else swanlog.debug("Duplicate finish, ignore it.")
 
-    def on_train_begin(self, *args, **kwargs):
-        """
-        训练开始，注册系统回调
-        """
+    def on_init(self, proj_name: str, workspace: str, logdir: str = None):
+        self._init_logdir(logdir)
+
+    def before_init_experiment(
+        self,
+        run_id: str,
+        exp_name: str,
+        description: str,
+        num: int,
+        suffix: str,
+        setter: Callable[[str, str, str, str], None]
+    ):
+        requirements_path = self.settings.requirements_path
+        metadata_path = self.settings.metadata_path
+        # 将实验依赖存入 requirements.txt
+        with open(requirements_path, "w") as f:
+            f.write(get_requirements())
+        # 将实验环境(硬件信息、git信息等等)存入 swanlab-metadata.json
+        with open(metadata_path, "w") as f:
+            json.dump(get_system_info(self.settings), f)
+
+    def on_run(self):
+        swanlog.install(self.settings.console_dir)
         # 注入系统回调
         self._register_sys_callback()
         # 打印信息
         self._train_begin_print()
         swanlog.info("Experiment_name: " + FONT.yellow(self.settings.exp_name))
         self._watch_tip_print()
+        if not os.path.exists(self.settings.log_dir):
+            os.mkdir(self.settings.log_dir)
 
-    def on_train_end(self, error: str = None):
+    def on_metric_create(self, metric_info: MetricInfo):
+        if metric_info.error:
+            return
+        self.settings.mkdir(os.path.dirname(metric_info.metric_path))
+        self.settings.mkdir(os.path.dirname(metric_info.summary_path))
+        with open(metric_info.summary_path, "w+") as f:
+            json.dump(metric_info.summary, f, ensure_ascii=False)
+        with open(metric_info.metric_path, "a") as f:
+            f.write(json.dumps(metric_info.metric, ensure_ascii=False) + "\n")
+
+    def on_stop(self, error: str = None):
         """
         训练结束，取消系统回调
         此函数被`run.finish`调用
         """
+        # 写入错误信息
+        if error is not None:
+            with open(self.settings.error_path, "a") as fError:
+                print(datetime.now(), file=fError)
+                print(error, file=fError)
         # 打印信息
         self._watch_tip_print()
         # 取消注册系统回调
         self._unregister_sys_callback()
-
-    def on_metric_create(self, *args, **kwargs):
-        pass
-
-    def on_column_create(self, *args, **kwargs):
-        pass
diff --git a/swanlab/data/config.py b/swanlab/data/config.py
index 805f51fc..52e6f106 100644
--- a/swanlab/data/config.py
+++ b/swanlab/data/config.py
@@ -84,9 +84,14 @@ def __init__(self, config: dict = None, settings: SwanDataSettings = None):
         """
         self.__config.update(self.__check_config(config))
         self.__settings["save_path"] = settings.config_path if settings is not None else None
+        self.__settings["should_save"] = settings.should_save if settings is not None else False
         if self._inited:
             self.__save()
 
+    @property
+    def should_shave(self):
+        return self.__settings.get("should_save")
+
     @staticmethod
     def __check_config(config: dict) -> dict:
         """
@@ -320,6 +325,8 @@ def __save(self):
         """
         保存config为json，不必校验config的YAML格式，将在写入时完成校验
         """
+        if not self.should_shave:
+            return
         swanlog.debug("Save config to {}".format(self.__settings.get("save_path")))
         with open(self.__settings.get("save_path"), "w") as f:
             # 将config的每个key的value转换为desc和value两部分，value就是原来的value，desc是None
diff --git a/swanlab/data/modules/audio.py b/swanlab/data/modules/audio.py
index e71e7f6e..5f011299 100644
--- a/swanlab/data/modules/audio.py
+++ b/swanlab/data/modules/audio.py
@@ -48,6 +48,9 @@ def get_data(self):
         if isinstance(self.value, list):
             return self.get_data_list()
         self.__preprocess(self.value)
+        # 判断是否要保存(mode='disabled'时不保存)
+        if not self.settings.should_save:
+            return
         hash_name = (
             get_file_hash_numpy_array(self.audio_data)[:16]
             if isinstance(self.audio_data, np.ndarray)
diff --git a/swanlab/data/modules/image.py b/swanlab/data/modules/image.py
index 050303dd..5c8254e4 100644
--- a/swanlab/data/modules/image.py
+++ b/swanlab/data/modules/image.py
@@ -80,6 +80,9 @@ def get_data(self):
             return self.get_data_list()
         # 图像预处理
         self.__preprocess(self.value)
+        # 判断是否要保存(mode='disabled'时不保存)
+        if not self.settings.should_save:
+            return
         # 获取图像的hash值
         hash_name = get_file_hash_pil(self.image_data)[:16]
         # 设置保存路径, 保存文件名
diff --git a/swanlab/data/modules/object_3d.py b/swanlab/data/modules/object_3d.py
index 19656cab..7ad50a0a 100644
--- a/swanlab/data/modules/object_3d.py
+++ b/swanlab/data/modules/object_3d.py
@@ -62,6 +62,10 @@ def get_data(self):
 
         self.object3d_data = self.__preprocess(self.value)
 
+        # 判断是否要保存(mode='disabled'时不保存)
+        if not self.settings.should_save:
+            return
+
         # 根据不同的输入类型进行不同的哈希校验
         hash_name = (
             get_file_hash_numpy_array(self.object3d_data)[:16]
diff --git a/swanlab/data/modules/text.py b/swanlab/data/modules/text.py
index 32434b28..0b9a7d68 100644
--- a/swanlab/data/modules/text.py
+++ b/swanlab/data/modules/text.py
@@ -28,6 +28,10 @@ def get_data(self):
         # 预处理文本数据
         self.__preprocess(self.value)
 
+        # 判断是否要保存(mode='disabled'时不保存)
+        if not self.settings.should_save:
+            return
+
         return self.text_data
 
     def expect_types(self, *args, **kwargs) -> list:
diff --git a/swanlab/data/modules/video.py b/swanlab/data/modules/video.py
index 28b4084b..40a876f4 100644
--- a/swanlab/data/modules/video.py
+++ b/swanlab/data/modules/video.py
@@ -70,6 +70,11 @@ def get_data(self, *args, **kwargs):
             return self.get_data_list()
         # 视频预处理
         self.video_data = self.__preprocess(self.value)
+
+        # 判断是否要保存(mode='disabled'时不保存)
+        if not self.settings.should_save:
+            return
+
         # 获取视频hash值
         hash_name = (
             get_file_hash_numpy_array(self.video_data)[:16]
diff --git a/swanlab/data/run/callback.py b/swanlab/data/run/callback.py
index 0e097c9e..d6d64658 100644
--- a/swanlab/data/run/callback.py
+++ b/swanlab/data/run/callback.py
@@ -7,10 +7,10 @@
 @Description:
     回调函数注册抽象类
 """
-from typing import Union, Tuple, Optional
+from typing import Union, Optional, Callable, Dict
+from abc import ABC, abstractmethod
 from swanlab.data.settings import SwanDataSettings
 from swanlab.data.modules import DataType
-from abc import ABC, abstractmethod
 from swanlab.log import swanlog
 from swanlab.utils.font import FONT
 from swanlab.env import is_windows
@@ -19,16 +19,125 @@
 import sys
 import os
 
-NewKeyInfo = Union[None, Tuple[dict, Union[float, DataType], int, int]]
-"""
-新的key对象、数据类型、步数、行数
-为None代表没有添加新的key
-"""
+
+class ColumnInfo:
+    """
+    列信息，当创建列时，会生成这个对象
+    """
+
+    def __init__(
+        self,
+        key: str,
+        namespace: str,
+        data_type: str,
+        chart_type: str,
+        sort: int,
+        error: Optional[Dict] = None,
+        reference: Optional[str] = None,
+        config: Optional[Dict] = None
+    ):
+        self.key = key
+        """
+        列的key名称
+        """
+        self.namespace = namespace
+        """
+        列的命名空间
+        """
+        self.data_type = data_type
+        """
+        列的数据类型
+        """
+        self.chart_type = chart_type
+        """
+        列的图表类型
+        """
+        self.error = error
+        """
+        列的类型错误信息
+        """
+        self.reference = reference if reference is not None else "step"
+        """
+        列的参考对象
+        """
+        self.sort = sort
+        """
+        列在namespace中的排序
+        """
+        self.config = config if config is not None else {}
+        """
+        列的额外配置信息
+        """
 
 
-class SwanLabRunCallback(ABC):
+class MetricInfo:
     """
-    SwanLabRunCallback抽象类
+    指标信息，当新的指标被log时，会生成这个对象
+    """
+
+    def __init__(
+        self,
+        key: str,
+        column_info: ColumnInfo,
+        metric: Union[Dict, None] = None,
+        summary: Union[Dict, None] = None,
+        data_type: Union[float, DataType] = None,
+        step: int = None,
+        epoch: int = None,
+        metric_path: str = None,
+        summary_path: str = None,
+        static_dir: str = None,
+        error: bool = True
+    ):
+        self.key = key
+        """
+        指标的key名称
+        """
+        self.column_info = column_info
+        """
+        指标对应的列信息
+        """
+        self.metric = metric
+        """
+        指标信息，error时为None
+        """
+        self.summary = summary
+        """
+        摘要信息，error时为None
+        """
+        self.data_type = data_type
+        """
+        当前指标的数据类型，error时为None
+        """
+        self.step = step
+        """
+        当前指标的步数，error时为None
+        """
+        self.epoch = epoch
+        """
+        当前指标对应本地的行数，error时为None
+        """
+        self.metric_path = metric_path
+        """
+        指标文件的路径，error时为None
+        """
+        self.summary_path = summary_path
+        """
+        摘要文件的路径，error时为None
+        """
+        self.static_dir = static_dir
+        """
+        静态文件的根文件夹，error时为None
+        """
+        self.error = error
+        """
+        指标是否有错误
+        """
+
+
+class U:
+    """
+    工具函数类，隔离SwanLabRunCallback回调与其他工具函数
     """
 
     def __init__(self):
@@ -95,16 +204,26 @@ def _train_finish_print(self):
         """
         swanlog.info("Experiment {} has completed".format(FONT.yellow(self.settings.exp_name)))
 
+
+class SwanLabRunCallback(ABC, U):
+    """
+    SwanLabRunCallback，回调函数注册类，所有以`on_`和`before_`开头的函数都会在对应的时机被调用
+    为了方便管理：
+    1. `_`开头的函数为内部函数，不会被调用，且写在最开头
+    2. 所有回调按照逻辑上的触发顺序排列
+    3. 所有回调不要求全部实现，只需实现需要的回调即可
+    """
+
     def _register_sys_callback(self):
         """
-        注册系统回调
+        注册系统回调，内部使用
         """
         sys.excepthook = self._except_handler
         atexit.register(self._clean_handler)
 
     def _unregister_sys_callback(self):
         """
-        注销系统回调
+        注销系统回调，内部使用
         """
         sys.excepthook = sys.__excepthook__
         atexit.unregister(self._clean_handler)
@@ -121,58 +240,70 @@ def _except_handler(self, tp, val, tb):
         """
         pass
 
-    @abstractmethod
-    def before_init_project(self, *args, **kwargs):
+    def on_init(self, proj_name: str, workspace: str, logdir: str = None):
         """
-        在执行业务逻辑之前调用
+        执行`swanlab.init`时调用
+        此时运行时环境变量没有被设置，此时修改环境变量还是有效的
+        :param logdir: str, 用户设置的日志目录
+        :param proj_name: str, 项目名称
+        :param workspace: str, 工作空间
         """
         pass
 
-    @abstractmethod
-    def on_train_begin(self, *args, **kwargs):
+    def before_init_experiment(
+        self,
+        run_id: str,
+        exp_name: str,
+        description: str,
+        num: int,
+        suffix: str,
+        setter: Callable[[str, str, str, str], None]
+    ):
         """
-        训练开始时的回调函数
+        在初始化实验之前调用，此时SwanLabRun已经初始化完毕
+        :param run_id: str, SwanLabRun的运行id
+        :param exp_name: str, 实验名称
+        :param description: str, 实验描述
+        :param num: int, 历史实验数量
+        :param suffix: str, 实验后缀
+        :param setter: Callable[[str, str, str, str], None], 设置实验信息的函数，在这里设置实验信息
         """
         pass
 
-    @abstractmethod
-    def on_train_end(self, *args, **kwargs):
+    def on_run(self):
         """
-        训练结束时的回调函数
+        SwanLabRun初始化完毕时调用
         """
         pass
 
-    @abstractmethod
-    def on_metric_create(self, *args, **kwargs):
+    def on_log(self):
         """
-        指标创建回调函数,新增指标信息时调用
+        每次执行swanlab.log时调用
         """
         pass
 
-    @abstractmethod
-    def on_column_create(self, *args, **kwargs):
+    def on_column_create(self, column_info: ColumnInfo):
         """
         列创建回调函数,新增列信息时调用
         """
         pass
 
-
-class EmptyCallback(SwanLabRunCallback):
-    """
-    空回调函数，用于适应不需要回调函数的情况
-    """
-
-    def before_init_project(self, *args, **kwargs):
-        pass
-
-    def on_train_begin(self, *args, **kwargs):
-        pass
-
-    def on_train_end(self, *args, **kwargs):
+    def on_metric_create(self, metric_info: MetricInfo):
+        """
+        指标创建回调函数,新增指标信息时调用
+        """
         pass
 
-    def on_metric_create(self, *args, **kwargs):
+    def on_stop(self, error: str = None):
+        """
+        训练结束时的回调函数
+        """
         pass
 
-    def on_column_create(self, *args, **kwargs):
+    @abstractmethod
+    def __str__(self) -> str:
+        """
+        返回当前回调函数的名称，这条应该是一个全局唯一的标识
+        在operator中会用到这个名称，必须唯一
+        """
         pass
diff --git a/swanlab/data/run/exp.py b/swanlab/data/run/exp.py
index 9d367a47..982ba22f 100644
--- a/swanlab/data/run/exp.py
+++ b/swanlab/data/run/exp.py
@@ -2,15 +2,14 @@
 from swanlab.data.settings import SwanDataSettings
 from swanlab.data.modules import BaseType, DataType
 from swanlab.log import swanlog
-from typing import Dict, Tuple, Union, Optional
+from typing import Dict, Union
 from swanlab.utils import create_time
 from swanlab.utils.file import check_tag_format
+from .callback import MetricInfo, ColumnInfo
+from .operator import SwanLabRunOperator
 from urllib.parse import quote
-import ujson
 import os
 import math
-from swanlab.db import Tag, Namespace, Chart, Source, Experiment, Display, ExistedError, ChartTypeError, add_multi_chart
-from .callback import SwanLabRunCallback, NewKeyInfo
 
 
 class SwanLabExp:
@@ -19,31 +18,23 @@ class SwanLabExp:
     save keys when running experiments
     """
 
-    def __init__(self, settings: SwanDataSettings, expid: int, exp: Experiment, callbacks: SwanLabRunCallback) -> None:
+    def __init__(self, settings: SwanDataSettings, operator: SwanLabRunOperator) -> None:
         """初始化实验
 
         Parameters
         ----------
         settings : SwanDataSettings
             全局运行时配置
-        expid : int
-            实验id
-        exp : Experiment
-            数据库实例，代表当前实验行
+        operator : SwanLabRunOperator
+            操作员
         """
         self.settings = settings
-        if not os.path.exists(self.settings.log_dir):
-            os.mkdir(self.settings.log_dir)
         # 当前实验的所有tag数据字段
         self.tags: Dict[str, SwanLabTag] = {}
-        self.id = expid
-        """此实验对应的id，实际上就是db.id"""
-        self.db: Experiment = exp
-        """此实验对应的数据库实例"""
-        self.__callbacks = callbacks
+        self.__operator = operator
         """回调函数"""
 
-    def add(self, key: str, data: DataType, step: int = None):
+    def add(self, key: str, data: DataType, step: int = None) -> MetricInfo:
         """记录一条新的tag数据
 
         Parameters
@@ -75,7 +66,7 @@ def add(self, key: str, data: DataType, step: int = None):
         """
         if tag_obj is None:
             # 将此tag对象添加到实验列表中
-            tag_obj = SwanLabTag(self.id, tag, self.settings.log_dir)
+            tag_obj = SwanLabTag(tag, self.settings.log_dir)
             self.tags[tag] = tag_obj
             """
             由于添加图表的同时会尝试转换data的类型，但这在注入step之前
@@ -85,20 +76,21 @@ def add(self, key: str, data: DataType, step: int = None):
             if isinstance(data, BaseType):
                 data.step = 0 if step is None or not isinstance(step, int) else step
                 data.tag = tag_obj.tag
-            result = tag_obj.create_chart(tag, data)
+            column_info = tag_obj.create_chart(tag, data)
             # 创建新列，生成回调
-            self.__callbacks.on_column_create(*result)
+            self.__operator.on_column_create(column_info)
 
         # 检查tag创建时图表是否创建成功，如果失败则也没有写入数据的必要了，直接退出
         if not tag_obj.is_chart_valid:
-            return swanlog.warning(
-                f"swanlab: Chart '{tag}' creation failed. Reason: The expected value type for the chart '{tag}' is int ,float or BaseType, but the input type is {type(data)}. "
+            swanlog.warning(
+                f"Chart '{tag}' creation failed. "
+                f"Reason: The expected value type for the chart '{tag}' is one of int,"
+                f"float or BaseType, but the input type is {type(data)}."
             )
-
-        # 添加tag信息
+            return MetricInfo(key, tag_obj.column_info)
         key_info = tag_obj.add(data, step)
-        # 调用回调函数
-        self.__callbacks.on_metric_create(tag_obj.tag, key_info, self.settings.static_dir)
+        key_info.static_dir = self.settings.static_dir
+        return key_info
 
 
 class SwanLabTag:
@@ -110,26 +102,22 @@ class SwanLabTag:
     # 每__slice_size个tag数据保存为一个文件
     __slice_size = 1000
 
-    def __init__(self, experiment_id, tag: str, log_dir: str) -> None:
+    def __init__(self, tag: str, log_dir: str) -> None:
         """
         初始化tag对象
 
         Parameters
         ----------
-        experiment_id : int
-            实验id
         tag : str
             tag名称
         log_dir : str
             log文件夹路径
         """
-        self.experiment_id = experiment_id
         self.tag = tag
         self.__steps = set()
-        """此tag已经包含的steps步骤
         """
-        self.__chart = None
-        """当前tag的图表配置"""
+        此tag已经包含的steps步骤
+        """
         self.__log_dir = log_dir
         """存储文件夹路径"""
         self.data_types = [float, int]
@@ -138,18 +126,27 @@ def __init__(self, experiment_id, tag: str, log_dir: str) -> None:
         """数据概要总结"""
         self.__data = self.__new_tags()
         """当前tag的数据"""
-        self.__namespace = None
-        """当前tag自动生成图表的命名空间"""
         self.__error = None
         """此tag在自动生成chart的时候的错误信息"""
         self.data_type = None
         """当前tag的数据类型，如果是BaseType类型，则为BaseType的小写类名，否则为default"""
+        self.__column_info = None
 
     @property
     def sum(self):
         """当前tag的数据总数"""
         return len(self.__steps)
 
+    @property
+    def column_info(self):
+        """获取当前tag对应的ColumnInfo"""
+        return self.__column_info
+
+    @property
+    def chart_created(self):
+        """判断当前tag是否已经创建了图表"""
+        return self.__column_info is not None
+
     @property
     def is_chart_valid(self) -> bool:
         """判断当前tag对应的自动创建图表是否成功
@@ -164,7 +161,7 @@ def __is_nan(data):
         """判断data是否为nan"""
         return isinstance(data, (int, float)) and math.isnan(data)
 
-    def add(self, data: DataType, step: int = None) -> NewKeyInfo:
+    def add(self, data: DataType, step: int = None) -> MetricInfo:
         """添加一个数据，在内部完成数据类型转换
         如果转换失败，打印警告并退出
         并且添加数据，当前的数据保存是直接保存，后面会改成缓存形式
@@ -175,34 +172,34 @@ def add(self, data: DataType, step: int = None) -> NewKeyInfo:
             待添加的数据
         step : int, optional
             步数，如果不传则默认当前步数为'已添加数据数量+1'
+
+        Returns
+        -------
+        metric_info
+            添加数据的信息，如果ok为False，则返回None
+        ok
+            是否添加成功，如果当前step已经存在，或者类型转换失败，返回False
         """
-        # 如果step不是None也不是int，设置为None且打印警告
         if step is not None and not isinstance(step, int):
             swanlog.warning(f"Step {step} is not int, SwanLab will set it automatically.")
             step = None
-        # 转换step，如果step为None，则改为合适的长度，目前step从0开始
         if step is None:
             step = len(self.__steps)
-        # 如果step已经存在，打印警告并退出
         if step in self.__steps:
-            return swanlog.warning(f"Step {step} on tag {self.tag} already exists, ignored.")
-        # 添加数据，首先比较数据，如果数据比之前的数据大，则更新最大值，否则不更新
-        """
-        python环境下data可能是列表、字符串、整型、浮点型等
-        因此下面的summary还需要在未来做好兼容
-        对于整型和浮点型，还存在极大和极小值的问题
-        目前的策略是让python解释器自己处理，在前端完成数据的格式化展示
-        """
+            swanlog.warning(f"Step {step} on tag {self.tag} already exists, ignored.")
+            return MetricInfo(self.tag, self.__column_info)
         more = None if not isinstance(data, BaseType) else data.get_more()
         try:
             data = self.try_convert_after_add_chart(data, step)
         except ValueError:
-            return swanlog.warning(
-                f"Log failed. Reason: Data {data} on tag '{self.tag}' (step {step}) cannot be converted .It should be an int, float, or a DataType, but it is {type(data)}, please check the data type. "
+            swanlog.warning(
+                f"Log failed. Reason: Data {data} on tag '{self.tag}' (step {step}) cannot be converted .It should be "
+                f"an int, float, or a DataType, but it is {type(data)}, please check the data type."
             )
+            return MetricInfo(self.tag, self.__column_info)
         is_nan = self.__is_nan(data)
+        # 更新数据概要
         if not is_nan:
-            # 如果数据比之前的数据小，则更新最小值，否则不更新
             if self._summary.get("max") is None or data > self._summary["max"]:
                 self._summary["max"] = data
                 self._summary["max_step"] = step
@@ -212,28 +209,26 @@ def add(self, data: DataType, step: int = None) -> NewKeyInfo:
         self._summary["num"] = self._summary.get("num", 0) + 1
         self.__steps.add(step)
         swanlog.debug(f"Add data, tag: {self.tag}, step: {step}, data: {data}")
-        # ---------------------------------- 保存数据 ----------------------------------
-        """添加数据到data中"""
         if len(self.__data["data"]) >= self.__slice_size:
-            # 如果当前数据已经达到了__slice_size，重新创建一个新的data
             self.__data = self.__new_tags()
-        # 添加数据
         data = data if not is_nan else "NaN"
         new_data = self.__new_tag(step, data, more=more)
         self.__data["data"].append(new_data)
-        # 优化文件分片，每__slice_size个tag数据保存为一个文件，通过sum来判断
         epoch = len(self.__steps)
         mu = math.ceil(epoch / self.__slice_size)
-        # 存储路径
         file_path = os.path.join(self.save_path, str(mu * self.__slice_size) + ".log")
-        # 更新实验信息总结
-        with open(os.path.join(self.save_path, "_summary.json"), "w+") as f:
-            ujson.dump(self._summary, f, ensure_ascii=False)
-        # 保存数据
-        with open(file_path, "a") as f:
-            f.write(ujson.dumps(new_data, ensure_ascii=False) + "\n")
-        # 深度拷贝，防止数据被修改
-        return json.loads(json.dumps(new_data)), self.data_type, step, epoch
+        return MetricInfo(
+            self.tag,
+            self.__column_info,
+            json.loads(json.dumps(new_data)),
+            json.loads(json.dumps(self._summary)),
+            self.data_type,
+            step,
+            epoch,
+            metric_path=file_path,
+            summary_path=os.path.join(self.save_path, "_summary.json"),
+            error=False
+        )
 
     @property
     def save_path(self):
@@ -245,57 +240,33 @@ def save_path(self):
             保存路径
         """
         path = os.path.join(self.__log_dir, quote(self.tag, safe=""))
-        if not os.path.exists(path):
-            os.mkdir(path)
         return path
 
-    def create_chart(self, tag: str, data: DataType) -> Tuple[str, str, Optional[Dict]]:
+    def create_chart(self, tag: str, data: DataType) -> ColumnInfo:
         """在第一次添加tag的时候，自动创建图表和namespaces，同时写入tag和数据库信息，将创建的信息保存到数据库中
         此方法只能执行一次
         具体步骤是：
         1. 创建tag字段
         2. 如果不是baseType类型，
-
-
         :returns tag, chart_type, error
-        [WARNING] 返回位置需与回调函数入参位置一致
+
+        WARNING 返回位置需与回调函数入参位置一致
         """
-        if self.__namespace is not None or self.__chart is not None:
+        if self.chart_created:
             raise ValueError(f"Chart {tag} has been created, cannot create again.")
-
         # 如果是非BaseType类型，写入默认命名空间，否则写入BaseType指定的命名空间
+        sort = 0
         if not isinstance(data, BaseType):
-            # data_type不变
             namespace, chart_type, reference, config = "default", "default", "step", None
-            sort = 0
             data_type = "default"
         else:
-            # 解构数据
             namespace, types, reference, config = data.__next__()
             chart_type, self.data_types = types
-            sort = None
             data_type = data.__class__.__name__.lower()
-        # 创建chart
-        self.__chart: Chart = Chart.create(
-            tag,
-            experiment_id=self.experiment_id,
-            type=chart_type,
-            reference=reference,
-            config=config,
-        )
-        # 创建命名空间，如果命名空间已经存在，会抛出ExistedError异常，捕获不处理即可
-        # 需要指定sort，default命名空间的sort为0，其他命名空间的sort为None，表示默认添加到最后
-        try:
-            # 对于namespace，如果tag的名称存在斜杠，则使用斜杠前的部分作为namespace的名称
-            if len(tag.split("/")) > 1:
-                namespace = tag.split("/")[0]
-            self.__namespace = Namespace.create(name=namespace, experiment_id=self.experiment_id, sort=sort)
-            swanlog.debug(f"Namespace {namespace} created, id: {self.__namespace.id}")
-        except ExistedError:
-            self.__namespace: Namespace = Namespace.get(name=namespace, experiment_id=self.experiment_id)
-            swanlog.debug(f"Namespace {namespace} exists, id: {self.__namespace.id}")
-        # 创建display，这个必然是成功的，因为display是唯一的，直接添加到最后一条即可
-        Display.create(chart_id=self.__chart.id, namespace_id=self.__namespace.id)
+        # 对于namespace，如果tag的名称存在斜杠，则使用斜杠前的部分作为namespace的名称
+        if "/" in tag and tag[0] != "/":
+            namespace = tag.split("/")[0]
+            sort = None
         """
         接下来判断tag格式的正确性，判断完毕后往source中添加一条tag记录
         在此函数中，只判断tag的格式是否正确，不记录数据
@@ -320,24 +291,12 @@ def create_chart(self, tag: str, data: DataType) -> Tuple[str, str, Optional[Dic
                 swanlog.error(f"Data type error, tag: {tag}, data type: {class_name}, excepted: {excepted}")
                 error = {"data_class": class_name, "excepted": excepted}
         if self.__is_nan(data):
-            """如果data是nan，生成error并保存"""
             error = {"data_class": "NaN", "excepted": [i.__name__ for i in self.data_types]}
-        tag: Tag = Tag.create(
-            experiment_id=self.experiment_id,
-            name=tag,
-            # 如果data是BaseType类型，使用data的小写类名，否则使用default
-            type=data_type,
-        )
-        self.data_type = data_type
-        # 添加一条source记录
-        Source.create(tag_id=tag.id, chart_id=self.__chart.id, error=error)
+        column_info = ColumnInfo(tag, namespace, data_type, chart_type, sort, error, reference, config)
         self.__error = error
-        # 新建多实验对比图表数据
-        try:
-            add_multi_chart(tag_id=tag.id, chart_id=self.__chart.id)
-        except ChartTypeError:
-            swanlog.warning("In the multi-experiment chart, the current type of tag is not as expected.")
-        return tag.name, chart_type, error
+        self.data_type = data_type
+        self.__column_info = column_info
+        return column_info
 
     @staticmethod
     def __new_tag(index, data, more: dict = None) -> dict:
diff --git a/swanlab/data/run/main.py b/swanlab/data/run/main.py
index 84cf3d43..1ed2dd61 100644
--- a/swanlab/data/run/main.py
+++ b/swanlab/data/run/main.py
@@ -8,23 +8,16 @@
     在此处定义SwanLabRun类并导出
 """
 from ..settings import SwanDataSettings
-from ..system import get_system_info, get_requirements
 from swanlab.log import swanlog
-from swanlab.utils.file import (
-    check_exp_name_format,
-    check_desc_format,
-)
-from swanlab.db import Experiment, ExistedError, NotExistedError
 from swanlab.data.modules import BaseType
 from swanlab.data.config import SwanLabConfig
-import time
 import random
-import ujson
 from enum import Enum
 from .exp import SwanLabExp
 from datetime import datetime
-from typing import Tuple, Callable, Optional, Dict
-from .callback import SwanLabRunCallback, EmptyCallback
+from typing import Callable, Optional, Dict
+from .operator import SwanLabRunOperator
+from swanlab.env import get_mode, SwanLabMode
 
 
 class SwanLabRunState(Enum):
@@ -45,19 +38,22 @@ class SwanLabRun:
 
     def __init__(
         self,
+        project_name: str = None,
         experiment_name: str = None,
         description: str = None,
         config: dict = None,
         log_level: str = None,
         suffix: str = None,
         exp_num: int = None,
-        callbacks: SwanLabRunCallback = None
+        operator: SwanLabRunOperator = SwanLabRunOperator(),
     ):
         """
         Initializing the SwanLabRun class involves configuring the settings and initiating other logging processes.
 
         Parameters
         ----------
+        project_name : str, optional
+            项目名称，目前单纯做一个记录
         experiment_name : str, optional
             实验名称，实验名称应该唯一，由0-9，a-z，A-Z，" ","_","-","/"组成
             如果不提供此参数(为None)，SwanLab将自动生成一个实验名称
@@ -76,32 +72,29 @@ def __init__(
             如果不提供此参数(为None)，不会添加后缀
         exp_num : int, optional
             历史实验总数，用于云端颜色与本地颜色的对应
-        callbacks : Dict[str, Callable]
-            回调函数字典，key为回调函数名，value为回调函数
+        operator : SwanLabRunOperator, optional
+            实验操作员，用于批量处理回调函数的调用，如果不提供此参数(为None)，则会自动生成一个实例
         """
         global run
         if run is not None:
             raise RuntimeError("SwanLabRun has been initialized")
         # ---------------------------------- 初始化类内参数 ----------------------------------
+        self.__project_name = project_name
         # 生成一个唯一的id，随机生成一个8位的16进制字符串，小写
         _id = hex(random.randint(0, 2 ** 32 - 1))[2:].zfill(8)
         timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
         self.__run_id = "run-{}-{}".format(timestamp, _id)
-        # 初始化配置
-        self.__settings = SwanDataSettings(run_id=self.__run_id)
-        # 初始化回调
-        self.__callbacks: SwanLabRunCallback = callbacks if callbacks is not None else EmptyCallback()
-        # 注入依赖
-        self.__callbacks.inject(self.__settings)
+        # 操作员初始化
+        self.__operator = SwanLabRunOperator() if operator is None else operator
+        self.__settings = SwanDataSettings(run_id=self.__run_id, should_save=not self.__operator.disabled)
+        self.__operator.inject(self.__settings)
         # ---------------------------------- 初始化日志记录器 ----------------------------------
         # output、console_dir等内容不依赖于实验名称的设置
-        swanlog.install(self.__settings.console_dir, self.__check_log_level(log_level))
+        swanlog.set_level(self.__check_log_level(log_level))
         # ---------------------------------- 初始化配置 ----------------------------------
         # 给外部1个config
         self.__config = SwanLabConfig(config, self.__settings)
         # ---------------------------------- 注册实验 ----------------------------------
-        # 校验描述格式
-        description = self.__check_description(description)
         self.__exp: SwanLabExp = self.__register_exp(experiment_name, description, suffix, num=exp_num)
         # 实验状态标记，如果status不为0，则无法再次调用log方法
         self.__state = SwanLabRunState.RUNNING
@@ -115,18 +108,26 @@ def _(state: SwanLabRunState):
         run = self
 
         # ---------------------------------- 初始化完成 ----------------------------------
-        self.__callbacks.on_train_begin()
+        self.__operator.on_run()
 
     @property
-    def callbacks(self) -> SwanLabRunCallback:
-        return self.__callbacks
+    def operator(self) -> SwanLabRunOperator:
+        return self.__operator
+
+    @property
+    def project_name(self) -> str:
+        return self.__project_name
+
+    @property
+    def mode(self) -> str:
+        return get_mode()
 
     @property
     def state(self) -> SwanLabRunState:
         return self.__state
 
-    @staticmethod
-    def get_state() -> SwanLabRunState:
+    @classmethod
+    def get_state(cls) -> SwanLabRunState:
         """
         获取当前实验状态
         """
@@ -168,16 +169,14 @@ def finish(state: SwanLabRunState = SwanLabRunState.SUCCESS, error=None):
         if state == SwanLabRunState.CRASHED and error is None:
             raise ValueError("When the state is 'CRASHED', the error message cannot be None.")
         _set_run_state(state)
-        # 写入错误信息
-        if state == SwanLabRunState.CRASHED:
-            with open(run.settings.error_path, "a") as fError:
-                print(datetime.now(), file=fError)
-                print(error, file=fError)
-        else:
-            error = None
+        error = error if state == SwanLabRunState.CRASHED else None
         # 退出回调
-        run.callbacks.on_train_end(error)
-        swanlog.uninstall()
+        run.operator.on_stop(error)
+        try:
+            swanlog.uninstall()
+        except RuntimeError:
+            # disabled 模式下没有install，所以会报错
+            pass
         _run, run = run, None
         return _run
 
@@ -224,17 +223,7 @@ def log(self, data: dict, step: int = None):
         """
         if self.__state != SwanLabRunState.RUNNING:
             raise RuntimeError("After experiment finished, you can no longer log data to the current experiment")
-        # 每一次log的时候检查一下数据库中的实验状态
-        # 如果实验状态不为0，说明实验已经结束，不允许再次调用log方法
-        # 这意味着每次log都会进行查询，比较消耗性能，后续考虑采用多进程共享内存的方式进行优化
-        swanlog.debug(f"Check experiment and state...")
-        try:
-            exp = Experiment.get(self.__exp.id)
-        except NotExistedError:
-            raise KeyboardInterrupt("The experiment has been deleted by the user")
-        # 此时self.__state == 0，说明是前端主动停止的
-        if exp.status != 0:
-            raise KeyboardInterrupt("The experiment has been stopped by the user")
+        self.__operator.on_log()
 
         if not isinstance(data, dict):
             return swanlog.error(
@@ -248,6 +237,8 @@ def log(self, data: dict, step: int = None):
                 )
             )
             step = None
+
+        log_return = {}
         # 遍历data，记录data
         for key in data:
             # 遍历字典的key，记录到本地文件中
@@ -257,69 +248,16 @@ def log(self, data: dict, step: int = None):
                 # 将d作为输入，构造一个与d相同类型的实例
                 d = d[0].__class__(d)
             # 数据类型的检查将在创建chart配置的时候完成，因为数据类型错误并不会影响实验进行
-            self.__exp.add(key=key, data=d, step=step)
+            metric_info = self.__exp.add(key=key, data=d, step=step)
+            self.__operator.on_metric_create(metric_info)
+            log_return[metric_info.key] = metric_info
+
+        return log_return
 
     def __str__(self) -> str:
         """此类的字符串表示"""
         return self.__run_id
 
-    @staticmethod
-    def __get_exp_name(experiment_name: str = None, suffix: str = None) -> Tuple[str, str]:
-        """
-        预处理实验名称，如果实验名称过长，截断
-
-        Parameters
-        ----------
-        experiment_name : str
-            实验名称
-        suffix : str
-            实验名称后缀添加方式，可以为None、"default"或自由后缀，前者代表不添加后缀，后者代表添加时间戳后缀
-
-        Returns
-        ----------
-        experiment_name : str
-            校验后的实验名称
-        exp_name : str
-            最终的实验名称
-        """
-        # ---------------------------------- 校验实验名称 ----------------------------------
-        experiment_name = "exp" if experiment_name is None else experiment_name
-        # 校验实验名称
-        experiment_name_checked = check_exp_name_format(experiment_name)
-        # 如果实验名称太长的tip
-        tip = "The experiment name you provided is too long, it has been truncated automatically."
-
-        # 如果前后长度不一样，说明实验名称被截断了，提醒
-        if len(experiment_name_checked) != len(experiment_name) and suffix is None:
-            swanlog.warning(tip)
-
-        # 如果suffix为None, 则不添加后缀，直接返回
-        if suffix is None or suffix is False:
-            return experiment_name_checked, experiment_name
-
-        # 如果suffix为True, 则添加默认后缀
-        if suffix is True:
-            suffix = "default"
-
-        # suffix必须是字符串
-        if not isinstance(suffix, str):
-            raise TypeError("The suffix must be a string, but got {}".format(type(suffix)))
-
-        # 如果suffix_checked为default，则设置为默认后缀
-        if suffix.lower().strip() == "default":
-            # 添加默认后缀
-            default_suffix = "{}".format(datetime.now().strftime("%b%d_%H-%M-%S"))
-            exp_name = "{}_{}".format(experiment_name_checked, default_suffix)
-        else:
-            exp_name = "{}_{}".format(experiment_name_checked, suffix)
-
-        # 校验实验名称，如果实验名称过长，截断
-        experiment_name_checked = check_exp_name_format(exp_name)
-        if len(experiment_name_checked) != len(exp_name):
-            swanlog.warning(tip)
-
-        return experiment_name_checked, exp_name
-
     def __register_exp(
         self,
         experiment_name: str,
@@ -330,34 +268,25 @@ def __register_exp(
         """
         注册实验，将实验配置写入数据库中，完成实验配置的初始化
         """
-        # 这个循环的目的是如果创建失败则等零点五秒重新生成后缀重新创建，直到创建成功
-        # 但是由于需要考虑suffix为none不生成后缀的情况，所以需要在except中判断一下
-        old = experiment_name
-        exp = None
-        while True:
-            experiment_name, exp_name = self.__get_exp_name(old, suffix)
-            try:
-                # 获得数据库实例
-                exp = Experiment.create(name=exp_name, run_id=self.__run_id, description=description, num=num)
-                break
-            except ExistedError:
-                # 如果suffix名为default，说明是自动生成的后缀，需要重新生成后缀
-                if isinstance(suffix, str) and suffix.lower().strip() == "default":
-                    swanlog.debug(f"Experiment {exp_name} has existed, try another name...")
-                    time.sleep(0.5)
-                    continue
-                # 其他情况下，说明是用户自定义的后缀，需要报错
-                else:
-                    Experiment.purely_delete(run_id=self.__run_id)
-                    raise ExistedError(f"Experiment {exp_name} has existed in local, please try another name.")
-
-        # 实验创建成功，设置实验相关信息
-        self.__settings.exp_name = exp_name
-        self.settings.exp_colors = (exp.light, exp.dark)
-        self.settings.description = description
-        # 执行一些记录操作
-        self.__record_exp_config()  # 记录实验配置
-        return SwanLabExp(self.__settings, exp.id, exp=exp, callbacks=self.__callbacks)
+
+        # ---------------------------------- 初始化实验 ----------------------------------
+
+        def setter(exp_name: str, light_color: str, dark_color: str, desc: str):
+            """
+            设置实验相关信息
+            :param exp_name: 实验名称
+            :param light_color: 亮色
+            :param dark_color: 暗色
+            :param desc: 实验描述
+            :return:
+            """
+            # 实验创建成功，设置实验相关信息
+            self.__settings.exp_name = exp_name
+            self.settings.exp_colors = (light_color, dark_color)
+            self.settings.description = desc
+
+        self.__operator.before_init_experiment(self.__run_id, experiment_name, description, num, suffix, setter)
+        return SwanLabExp(self.__settings, operator=self.__operator)
 
     @staticmethod
     def __check_log_level(log_level: str) -> str:
@@ -371,31 +300,6 @@ def __check_log_level(log_level: str) -> str:
             swanlog.warning(f"The log level you provided is not valid, it has been set to {log_level}.")
             return "info"
 
-    @staticmethod
-    def __check_description(description: str) -> str:
-        """检查实验描述是否合法"""
-        if description is None:
-            return ""
-        desc = check_desc_format(description)
-        if desc != description:
-            swanlog.warning("The description has been truncated automatically.")
-        return desc
-
-    def __record_exp_config(self):
-        """创建实验配置目录 files
-        - 创建 files 目录
-        - 将实验环境写入 files/swanlab-metadata.json 中
-        - 将实验依赖写入 files/requirements.txt 中
-        """
-        requirements_path = self.__settings.requirements_path
-        metadata_path = self.__settings.metadata_path
-        # 将实验依赖存入 requirements.txt
-        with open(requirements_path, "w") as f:
-            f.write(get_requirements())
-        # 将实验环境(硬件信息、git信息等等)存入 swanlab-metadata.json
-        with open(metadata_path, "w") as f:
-            ujson.dump(get_system_info(self.__settings), f)
-
 
 run: Optional["SwanLabRun"] = None
 """Global runtime instance. After the user calls finish(), run will be set to None."""
@@ -418,8 +322,6 @@ def _set_run_state(state: SwanLabRunState):
         state = SwanLabRunState.CRASHED
     # 设置state
     _change_run_state(state)
-    # 更新数据库中的实验状态
-    run.exp.db.update_status(state.value)
 
 
 def get_run() -> Optional["SwanLabRun"]:
diff --git a/swanlab/data/run/operator.py b/swanlab/data/run/operator.py
new file mode 100644
index 00000000..dd46280d
--- /dev/null
+++ b/swanlab/data/run/operator.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/5/12 14:20
+@File: operator.py
+@IDE: pycharm
+@Description:
+    回调函数操作员，批量处理回调函数的调用
+"""
+from typing import List, Union, Dict, Any, Callable
+from .callback import SwanLabRunCallback, MetricInfo, ColumnInfo
+from ..settings import SwanDataSettings
+
+OperatorReturnType = Dict[str, Any]
+
+
+class SwanLabRunOperator(SwanLabRunCallback):
+    """
+    The SwanLabRunOperator is used to batch process callback instances, triggering them respectively in the order of
+    injection when each occasion occurs. In design, we aim for isolation between each instance. However, if there are
+    cases of shared scope, parameters, or variables between instances, developers should handle them appropriately,
+    as SwanLabRunOperator itself will not address these situations for now.
+    """
+
+    def __init__(self, callbacks: Union[SwanLabRunCallback, List[SwanLabRunCallback]] = None):
+        super(SwanLabRunOperator, self).__init__()
+        callbacks = [callbacks] if isinstance(callbacks, SwanLabRunCallback) else callbacks
+        self.callbacks = {}
+        if callbacks is not None:
+            for callback in callbacks:
+                self.add_callback(callback)
+
+    @property
+    def disabled(self):
+        """
+        判断是否所有回调函数都已经被禁用
+        FIXME 实际上这里与settings的一些属性有关，目前没问题，但是未来可能要改
+        """
+        return len(self.callbacks) == 0
+
+    def add_callback(self, callback: SwanLabRunCallback):
+        if not isinstance(callback, SwanLabRunCallback):
+            raise TypeError(f"Unsupported callback type: {type(callback)}")
+        if str(callback) == str(self) or callback in self.callbacks:
+            raise ValueError(f"Cannot add the same callback instance: {callback}")
+        self.callbacks[str(callback)] = callback
+
+    def __str__(self):
+        return "SwanLabRunOperator"
+
+    def __run_all(self, method: str, *args, **kwargs):
+        return {name: getattr(callback, method)(*args, **kwargs) for name, callback in self.callbacks.items()}
+
+    @classmethod
+    def parse_return(cls, ret: OperatorReturnType, key: str = None):
+        """
+        解析返回值，选择不为None的返回值
+        如果key不为None，则返回对应key的返回值
+        :param ret: 返回值
+        :param key: 返回值的key
+        :return:
+        """
+        if key is not None:
+            return ret[key]
+        return next((v for v in ret.values() if v is not None), None)
+
+    def on_init(self, proj_name: str, workspace: str, logdir: str = None) -> OperatorReturnType:
+        return self.__run_all("on_init", proj_name, workspace, logdir=logdir)
+
+    def inject(self, settings: SwanDataSettings) -> OperatorReturnType:
+        self.settings = settings
+        return {name: callback.inject(settings) for name, callback in self.callbacks.items()}
+
+    def before_init_experiment(
+        self,
+        run_id: str,
+        exp_name: str,
+        description: str,
+        num: int,
+        suffix: str,
+        setter: Callable[[str, str, str, str], None]
+    ):
+        return self.__run_all(
+            "before_init_experiment",
+            run_id,
+            exp_name,
+            description,
+            num,
+            suffix,
+            setter
+        )
+
+    def on_run(self):
+        return self.__run_all("on_run")
+
+    def on_log(self):
+        return self.__run_all("on_log")
+
+    def on_metric_create(self, metric_info: MetricInfo):
+        return self.__run_all("on_metric_create", metric_info)
+
+    def on_column_create(self, column_info: ColumnInfo):
+        return self.__run_all("on_column_create", column_info)
+
+    def on_stop(self, error: str = None):
+        return self.__run_all("on_stop", error)
diff --git a/swanlab/data/sdk.py b/swanlab/data/sdk.py
index 5a4acba9..c5f70063 100644
--- a/swanlab/data/sdk.py
+++ b/swanlab/data/sdk.py
@@ -8,7 +8,7 @@
     在此处封装swanlab在日志记录模式下的各种接口
 """
 import os
-from typing import Optional, Union, Dict
+from typing import Optional, Union, Dict, Tuple
 from .modules import DataType
 from .run import (
     SwanLabRunState,
@@ -18,12 +18,13 @@
 )
 from .callback_cloud import CloudRunCallback
 from .callback_local import LocalRunCallback
+from .run.operator import SwanLabRunOperator
 from .config import SwanLabConfig
-from ..db import Project, connect
-from ..env import init_env, ROOT
-from ..log import swanlog
-from ..utils import check_load_json_yaml, check_proj_name_format
+from swanlab.env import init_env, get_swanlog_dir, SwanLabMode, MODE
+from swanlab.log import swanlog
+from swanlab.utils import check_load_json_yaml, check_proj_name_format
 from swanlab.api import code_login
+from swanlab.db import GlomCallback
 from swanlab.package import version_limit
 
 _config: Optional["SwanLabConfig"] = SwanLabConfig(None)
@@ -90,7 +91,7 @@ def init(
     config: Union[dict, str] = None,
     logdir: str = None,
     suffix: Union[str, None, bool] = "default",
-    cloud: bool = True,
+    mode: str = None,
     load: str = None,
     **kwargs,
 ) -> SwanLabRun:
@@ -139,10 +140,12 @@ def init(
         If this parameter is None or False, no suffix will be added.
         If this parameter is a string, the suffix will be the string you provided.
         Attention: experiment_name + suffix must be unique, otherwise the experiment will not be created.
-    cloud : bool, optional
-        Whether to use the cloud mode, the default is True.
-        If you use the cloud mode, the log file will be stored in the cloud, which will still be saved locally.
-        If you are not using cloud mode, the `workspace` fields are invalid.
+    mode : str, optional
+        Allowed values are 'cloud', 'cloud-only', 'local', 'disabled'.
+        If the value is 'cloud', the data will be uploaded to the cloud and the local log will be saved.
+        If the value is 'cloud-only', the data will only be uploaded to the cloud and the local log will not be saved.
+        If the value is 'local', the data will only be saved locally and will not be uploaded to the cloud.
+        If the value is 'disabled', the data will not be saved or uploaded, just parsing the data.
     load : str, optional
         If you pass this parameter,SwanLab will search for the configuration file you specified
         (which must be in JSON or YAML format)
@@ -152,55 +155,48 @@ def init(
         SwanLab will attempt to replace them from the configuration file you provided;
         otherwise, it will use the parameters you passed as the definitive ones.
     """
-    # 如果已经初始化过了，直接返回run
     run = get_run()
     if run is not None:
         swanlog.warning("You have already initialized a run, the init function will be ignored")
         return run
     # ---------------------------------- 一些变量、格式检查 ----------------------------------
-    # 如果传入了load，则加载load文件，如果load文件不存在，报错
+    if "cloud" in kwargs:
+        swanlog.warning(
+            "The `cloud` parameter in swanlab.init is deprecated and will be removed in the future"
+            "please use `mode='cloud'` instead."
+        )
+        mode = "cloud" if kwargs["cloud"] else mode
     if load:
         load_data = check_load_json_yaml(load, load)
-        # 尝试更改传入的参数为None的情况，如果传入的参数不是None，不做任何操作
         experiment_name = _load_data(load_data, "experiment_name", experiment_name)
         description = _load_data(load_data, "description", description)
         config = _load_data(load_data, "config", config)
         logdir = _load_data(load_data, "logdir", logdir)
         suffix = _load_data(load_data, "suffix", suffix)
-        cloud = _load_data(load_data, "cloud", cloud)
+        mode = _load_data(load_data, "mode", mode)
         project = _load_data(load_data, "project", project)
         workspace = _load_data(load_data, "workspace", workspace)
-    if not cloud and workspace is not None:
-        swanlog.warning("The `workspace` field is invalid in local mode")
-    # 默认实验名称为当前目录名
-    project = _check_proj_name(project if project else os.path.basename(os.getcwd()))
-    # 初始化logdir参数，接下来logdir被设置为绝对路径且当前程序有写权限
-    logdir = _init_logdir(logdir)
+    operator, c = _create_operator(mode)
+    project = _check_proj_name(project if project else os.path.basename(os.getcwd()))  # 默认实验名称为当前目录名
+    exp_num = SwanLabRunOperator.parse_return(
+        operator.on_init(project, workspace, logdir=logdir), key=c.__str__() if c else None
+    )
     # 初始化confi参数
     config = _init_config(config)
-    # 检查logdir内文件的版本，如果<=0.1.4则报错
-    version_limit(logdir, mode="init")
-    # 初始化环境变量
     init_env()
-    # 定义回调函数
-    callbacks = CloudRunCallback() if cloud else LocalRunCallback()
-    # ---------------------------------- 初始化项目 ----------------------------------
-    # 获取云端历史总数
-    exp_num = callbacks.before_init_project(project, workspace)
-    # 连接本地数据库，要求路径必须存在，但是如果数据库文件不存在，会自动创建
-    connect(autocreate=True)
-    # 初始化项目数据库
-    Project.init(project)
+    # 检查logdir内文件的版本，如果<=0.1.4则报错
+    version_limit(get_swanlog_dir(), mode="init")
     # ---------------------------------- 实例化实验 ----------------------------------
     # 注册实验
     run = register(
+        project_name=project,
         experiment_name=experiment_name,
         description=description,
         config=config,
         log_level=kwargs.get("log_level", "info"),
         suffix=suffix,
         exp_num=exp_num,
-        callbacks=callbacks,
+        operator=operator,
     )
     return run
 
@@ -243,44 +239,28 @@ def finish(state: SwanLabRunState = SwanLabRunState.SUCCESS, error=None):
     run.finish(state, error)
 
 
-def _init_logdir(logdir: str) -> str:
+def _init_mode(mode: str = None):
     """
-    处理通过init传入的logdir存在的一些情况
+    初始化mode参数
+    从环境变量中提取默认的mode参数，如果传入的mode参数不为None，则使用环境变量中的mode参数，否则使用传入的mode参数
+    传入的mode必须为SwanLabMode枚举中的一个值，否则报错ValueError
+    如果环境变量和传入的mode参数都为None，则默认为cloud
+
+    :param mode: str, optional
+        传入的mode参数
+    :return: str mode
+    :raise ValueError: mode参数不合法
     """
-    # 如果传入了logdir，则将logdir设置为环境变量，代表日志文件存放的路径
-    if logdir is not None:
-        try:
-            if not isinstance(logdir, str):
-                raise ValueError("path must be a string")
-            if not os.path.isabs(logdir):
-                logdir = os.path.abspath(logdir)
-            # 如果创建失败，也是抛出IOError
-            try:
-                os.makedirs(logdir, exist_ok=True)
-            except Exception as e:
-                raise IOError(f"create path: {logdir} failed, error: {e}")
-            if not os.access(logdir, os.W_OK):
-                raise IOError(f"no write permission for path: {logdir}")
-        except ValueError:
-            raise ValueError("logdir must be a str.")
-        except IOError:
-            raise IOError("logdir must be a path and have Write permission.")
-        os.environ[ROOT] = logdir
-    # 如果没有传入logdir，则使用默认的logdir, 即当前工作目录下的swanlog文件夹，但是需要保证目录存在
-    else:
-        logdir = os.environ.get(ROOT) or os.path.join(os.getcwd(), "swanlog")
-        logdir = os.path.abspath(logdir)
-        try:
-            os.makedirs(logdir, exist_ok=True)
-            if not os.access(logdir, os.W_OK):
-                raise IOError
-        except IOError:
-            raise IOError("logdir must have Write permission.")
-    # 如果logdir是空的，创建.gitignore文件，写入*
-    if not os.listdir(logdir):
-        with open(os.path.join(logdir, ".gitignore"), "w") as f:
-            f.write("*")
-    return logdir
+    allowed = [m.value for m in SwanLabMode]
+    m = os.environ.get(MODE)
+    if m is not None and mode is not None:
+        swanlog.warning(f"The environment variable {MODE} will be overwritten by the parameter mode")
+    mode = m if mode is None else mode
+    if mode is not None and mode not in allowed:
+        raise ValueError(f"`mode` must be one of {allowed}, but got {mode}")
+    mode = "cloud" if mode is None else mode
+    os.environ[MODE] = mode
+    return mode
 
 
 def _init_config(config: Union[dict, str]):
@@ -293,13 +273,28 @@ def _init_config(config: Union[dict, str]):
 
 
 def _load_data(load_data: dict, key: str, value):
-    """从load_data中加载数据，如果value不是None"""
+    """
+    从load_data中加载数据，如果value不是None，则直接返回value，如果为None，则返回load_data中的key
+    """
     if value is not None:
-        # tip = "The parameter {} is loaded from the configuration file: {}".format(FONT.bold(key), value)
-        # print(FONT.swanlab(tip))
         return value
     d = load_data.get(key, None)
-    # if d is not None:
-    #     tip = "The parameter {} is loaded from the configuration file: {}".format(FONT.bold(key), d)
-    #     print(FONT.swanlab(tip))
     return d
+
+
+def _create_operator(mode) -> Tuple[SwanLabRunOperator, Optional[CloudRunCallback]]:
+    """
+    创建SwanLabRunOperator实例
+    如果mode为disabled，则返回一个空的SwanLabRunOperator实例和None
+
+    :param mode: str
+        运行模式
+    :return: SwanLabRunOperator, CloudRunCallback
+    """
+    mode = _init_mode(mode)
+    if mode == SwanLabMode.DISABLED.value:
+        swanlog.warning("SwanLab run disabled, the data will not be saved or uploaded.")
+        return SwanLabRunOperator(), None
+    c = CloudRunCallback() if mode == SwanLabMode.CLOUD.value else LocalRunCallback()
+    callbacks = [c, GlomCallback()]
+    return SwanLabRunOperator(callbacks), c
diff --git a/swanlab/data/settings.py b/swanlab/data/settings.py
index 8710de49..580aa576 100644
--- a/swanlab/data/settings.py
+++ b/swanlab/data/settings.py
@@ -14,7 +14,7 @@
 
 
 class SwanDataSettings:
-    def __init__(self, run_id: str) -> None:
+    def __init__(self, run_id: str, should_save: bool) -> None:
         """实验名称
 
         Parameters
@@ -35,6 +35,19 @@ def __init__(self, run_id: str) -> None:
         # 实验运行id
         self.__run_id: str = run_id
         self.__version = get_package_version()
+        self.__should_save = should_save
+
+    @property
+    def should_save(self):
+        """
+        是否应该保存实验信息
+        """
+        return self.__should_save
+
+    def mkdir(self, path: str) -> None:
+        """创建目录"""
+        if not os.path.exists(path) and self.should_save:
+            os.makedirs(path, exist_ok=True)
 
     @property
     def version(self) -> str:
@@ -117,16 +130,15 @@ def console_dir(self) -> str:
     def static_dir(self) -> str:
         """静态资源路径"""
         path = os.path.join(self.run_dir, "media")
-        if not os.path.exists(path):
-            os.mkdir(path)
+        self.mkdir(path)
         return path
 
     @property
     def files_dir(self) -> str:
         """实验配置信息路径"""
         path = os.path.join(self.run_dir, "files")
-        os.makedirs(path, exist_ok=True)
-        return os.path.join(self.run_dir, "files")
+        self.mkdir(path)
+        return path
 
     @property
     def requirements_path(self) -> str:
diff --git a/swanlab/db/__init__.py b/swanlab/db/__init__.py
index ffb2b5aa..961c96ad 100644
--- a/swanlab/db/__init__.py
+++ b/swanlab/db/__init__.py
@@ -30,3 +30,4 @@
 from .table_config import tables
 from .db_connect import connect
 from .utils import add_multi_chart
+from .callback import GlomCallback
diff --git a/swanlab/db/callback.py b/swanlab/db/callback.py
new file mode 100644
index 00000000..b78f7a2f
--- /dev/null
+++ b/swanlab/db/callback.py
@@ -0,0 +1,182 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/5/12 15:22
+@File: callback.py
+@IDE: pycharm
+@Description:
+    注册回调函数
+"""
+from swanlab.data.run.callback import SwanLabRunCallback, ColumnInfo
+from swanlab.log import swanlog
+from .models import *
+from .utils.chart import add_multi_chart
+from .db_connect import connect
+from .error import NotExistedError, ExistedError, ChartTypeError
+from typing import Tuple, Callable, Optional
+from swanlab.utils.file import check_exp_name_format, check_desc_format
+from datetime import datetime
+import time
+
+
+class GlomCallback(SwanLabRunCallback):
+    """
+    GlomCallback类，swanlab本体与数据库的连接回调函数
+    """
+
+    def __init__(self):
+        super(GlomCallback, self).__init__()
+        self.exp: Optional[Experiment] = None
+
+    @staticmethod
+    def __get_exp_name(experiment_name: str = None, suffix: str = None) -> Tuple[str, str]:
+        """
+        预处理实验名称，如果实验名称过长，截断
+
+        Parameters
+        ----------
+        experiment_name : str
+            实验名称
+        suffix : str
+            实验名称后缀添加方式，可以为None、"default"或自由后缀，前者代表不添加后缀，后者代表添加时间戳后缀
+
+        Returns
+        ----------
+        experiment_name : str
+            校验后的实验名称
+        exp_name : str
+            最终的实验名称
+        """
+        # ---------------------------------- 校验实验名称 ----------------------------------
+        experiment_name = "exp" if experiment_name is None else experiment_name
+        # 校验实验名称
+        experiment_name_checked = check_exp_name_format(experiment_name)
+        # 如果实验名称太长的tip
+        tip = "The experiment name you provided is too long, it has been truncated automatically."
+
+        # 如果前后长度不一样，说明实验名称被截断了，提醒
+        if len(experiment_name_checked) != len(experiment_name) and suffix is None:
+            swanlog.warning(tip)
+
+        # 如果suffix为None, 则不添加后缀，直接返回
+        if suffix is None or suffix is False:
+            return experiment_name_checked, experiment_name
+
+        # 如果suffix为True, 则添加默认后缀
+        if suffix is True:
+            suffix = "default"
+
+        # suffix必须是字符串
+        if not isinstance(suffix, str):
+            raise TypeError("The suffix must be a string, but got {}".format(type(suffix)))
+
+        # 如果suffix_checked为default，则设置为默认后缀
+        if suffix.lower().strip() == "default":
+            # 添加默认后缀
+            default_suffix = "{}".format(datetime.now().strftime("%b%d_%H-%M-%S"))
+            exp_name = "{}_{}".format(experiment_name_checked, default_suffix)
+        else:
+            exp_name = "{}_{}".format(experiment_name_checked, suffix)
+
+        # 校验实验名称，如果实验名称过长，截断
+        experiment_name_checked = check_exp_name_format(exp_name)
+        if len(experiment_name_checked) != len(exp_name):
+            swanlog.warning(tip)
+
+        return experiment_name_checked, exp_name
+
+    def __str__(self):
+        return "SwanLabGlomCallback"
+
+    def on_init(self, proj_name: str, *args, **kwargs):
+        # 连接本地数据库，要求路径必须存在，但是如果数据库文件不存在，会自动创建
+        connect(autocreate=True)
+        # 初始化项目数据库
+        Project.init(proj_name)
+
+    def before_init_experiment(
+        self,
+        run_id: str,
+        exp_name: str,
+        description: str,
+        num: int,
+        suffix: str,
+        setter: Callable[[str, str, str, str], None]
+    ):
+        # ---------------------------------- 实验描述校验 ----------------------------------
+        description = description if description is not None else ""
+        desc = check_desc_format(description)
+        if desc != description:
+            swanlog.warning("The description has been truncated automatically.")
+        # ---------------------------------- 实验名称校验 ----------------------------------
+        # 这个循环的目的是如果创建失败则等零点五秒重新生成后缀重新创建，直到创建成功
+        # 但是由于需要考虑suffix为none不生成后缀的情况，所以需要在except中判断一下
+        old = exp_name
+        while True:
+            experiment_name, exp_name = self.__get_exp_name(old, suffix)
+            try:
+                # 获得数据库实例
+                self.exp = Experiment.create(name=exp_name, run_id=run_id, description=description, num=num)
+                break
+            except ExistedError:
+                # 如果suffix名为default，说明是自动生成的后缀，需要重新生成后缀
+                if isinstance(suffix, str) and suffix.lower().strip() == "default":
+                    swanlog.debug(f"Experiment {exp_name} has existed, try another name...")
+                    time.sleep(0.5)
+                    continue
+                # 其他情况下，说明是用户自定义的后缀，需要报错
+                else:
+                    Experiment.purely_delete(run_id=run_id)
+                    raise ExistedError(f"Experiment {exp_name} has existed in local, please try another name.")
+        # 执行相关设置
+        setter(experiment_name, self.exp.light, self.exp.dark, self.exp.description)
+
+    def on_log(self):
+        # 每一次log的时候检查一下数据库中的实验状态
+        # 如果实验状态不为0，说明实验已经结束，不允许再次调用log方法
+        # 这意味着每次log都会进行查询，比较消耗性能，后续考虑采用多进程共享内存的方式进行优化
+        swanlog.debug(f"Check experiment and state...")
+        try:
+            exp = Experiment.get(id=self.exp.id)
+        except NotExistedError:
+            raise KeyboardInterrupt("The experiment has been deleted by the user")
+        # 此时self.__state == 0，说明是前端主动停止的
+        if exp.status != 0:
+            raise KeyboardInterrupt("The experiment has been stopped by the user")
+
+    def on_column_create(self, column_info: ColumnInfo):
+        # 创建Chart
+        chart = Chart.create(
+            column_info.key,
+            experiment_id=self.exp,
+            type=column_info.data_type,
+            reference=column_info.reference,
+            config=column_info.config,
+        )
+        # 创建命名空间，如果命名空间已经存在，会抛出ExistedError异常，捕获不处理即可
+        # 需要指定sort，default命名空间的sort为0，其他命名空间的sort为None，表示默认添加到最后
+        namespace = column_info.namespace
+        try:
+            n = Namespace.create(name=namespace, experiment_id=self.exp.id, sort=column_info.sort)
+            swanlog.debug(f"Namespace {namespace} created, id: {n.id}")
+        except ExistedError:
+            n: Namespace = Namespace.get(name=namespace, experiment_id=self.exp.id)
+            swanlog.debug(f"Namespace {namespace} exists, id: {n.id}")
+        # 创建display，这个必然是成功的，因为display是唯一的，直接添加到最后一条即可
+        Display.create(chart_id=chart.id, namespace_id=n.id)
+        tag: Tag = Tag.create(
+            experiment_id=self.exp.id,
+            name=column_info.key,
+            type=column_info.data_type,
+        )
+        # 添加一条source记录
+        Source.create(tag_id=tag.id, chart_id=chart.id, error=column_info.error)
+        # 新建多实验对比图表数据
+        try:
+            add_multi_chart(tag_id=tag.id, chart_id=chart.id)
+        except ChartTypeError:
+            swanlog.warning("In the multi-experiment chart, the current type of tag is not as expected.")
+
+    def on_stop(self, error: str = None):
+        # 更新数据库中的实验状态
+        self.exp.update_status(-1 if error is not None else 1)
diff --git a/swanlab/db/models/experiments.py b/swanlab/db/models/experiments.py
index f1d41aa9..75eac42b 100644
--- a/swanlab/db/models/experiments.py
+++ b/swanlab/db/models/experiments.py
@@ -168,23 +168,6 @@ def create(
 
     @classmethod
     def get(cls, *args, **kwargs) -> "Experiment":
-        """覆写继承的get方法，通过id获取实验实例
-
-        Parameters
-        ----------
-        id : int
-            实验id
-
-        Returns
-        -------
-        Experiment
-            实验实例
-
-        Raises
-        -------
-        NotExistedError
-            实验不存在
-        """
         try:
             return super().get(*args, **kwargs)
         except:
@@ -203,18 +186,18 @@ def update_status(self, status: int):
         self.save()
 
     @classmethod
-    def purely_delete(cls, **kwargs):
+    def purely_delete(cls, run_id: str):
         """
         删除某一个实验，顺便删除实验的文件夹
 
         Parameters
         ----------
-        id : int
-            实验id
+        run_id : int
+            实验run_id
         """
         # 获取实验实例
         try:
-            exp = cls.get(**kwargs)
+            exp = cls.get(run_id=run_id)
             # 删除实验
             exp.delete_instance()
             # 更新项目的实验数量
@@ -222,6 +205,4 @@ def purely_delete(cls, **kwargs):
             # 删除实验文件夹
             shutil.rmtree(os.path.join(get_swanlog_dir(), exp.run_id.__str__()))
         except NotExistedError:
-            # 删除实验文件夹
-            if "run_id" in kwargs:
-                shutil.rmtree(os.path.join(get_swanlog_dir(), kwargs["run_id"]))
+            shutil.rmtree(os.path.join(get_swanlog_dir(), run_id))
diff --git a/swanlab/env.py b/swanlab/env.py
index 2d9d2179..8c5c988e 100644
--- a/swanlab/env.py
+++ b/swanlab/env.py
@@ -11,6 +11,7 @@
 from typing import MutableMapping, Optional
 from .utils.file import is_port, is_ipv4
 from .error import UnKnownSystemError
+import enum
 import sys
 
 Env = Optional[MutableMapping]
@@ -18,8 +19,8 @@
 _env = dict()
 """运行时环境变量参数存储，实际上就是一个字典"""
 
-# '描述' = "key"
 # ---------------------------------- 基础环境变量 ----------------------------------
+# '描述' = "key"
 ROOT = "SWANLAB_LOG_DIR"
 """命令执行目录SWANLAB_LOG_DIR，日志文件存放在这个目录下，如果自动生成，则最后的目录名为swanlog，第一次调用时如果路径不存在，会自动创建路径"""
 
@@ -30,6 +31,41 @@
 """服务端口SWANLAB_SERVER_PORT，服务地址"""
 
 DEV = "SWANLAB_DEV"
+"""是否是开发模式SWANLAB_DEV，开发模式下会打印更多的日志信息，并且切换一些配置"""
+
+MODE = "SWANLAB_MODE"
+"""运行模式SWANLAB_MODE，有cloud、local、disabled等模式，针对Callback回调的不同实例化方式，具体效果为：
+1. disabled: 禁用模式，不会上传日志到云端，也不会记录日志，swanlab只会执行解析log的功能，不会输出任何内容到本地
+2. cloud: 云端模式，是云端与本地的混合模式，会上传日志到云端，也会记录日志到本地
+3. cloud-only: 云端模式，只会上传日志到云端，不会记录日志到本地，但是会生成一些临时文件
+4. local: 本地模式，不会上传日志到云端，使用swanlab本地版本
+此外，SWANLAB_MODE为disabled时，会开启非严格模式，非严格模式不再要求文件路径存在
+"""
+
+
+class SwanLabMode(enum.Enum):
+    DISABLED = "disabled"
+    CLOUD = "cloud"
+    # CLOUD_ONLY = "cloud-only"
+    LOCAL = "local"
+
+
+def get_mode(env: Optional[Env] = None) -> Optional[str]:
+    """
+    获取运行模式，返回值为SwanLabMode枚举value
+    """
+    if _env.get(MODE) is not None:
+        return _env.get(MODE)
+    # 否则从环境变量中提取
+    if env is None:
+        env = os.environ
+    default: Optional[str] = "cloud"
+    mode = env.get(MODE, default)
+    allowed = [mode.value for mode in SwanLabMode]
+    if mode not in allowed:
+        raise ValueError('SWANLAB_MODE must be one of {allowed}, now is "{mode}"'.format(allowed=allowed, mode=mode))
+    _env[MODE] = mode
+    return mode
 
 
 def get_swanlog_dir(env: Optional[Env] = None) -> Optional[str]:
@@ -51,18 +87,16 @@ def get_swanlog_dir(env: Optional[Env] = None) -> Optional[str]:
     # 必须是一个绝对路径
     if not os.path.isabs(path):
         raise ValueError('SWANLAB_LOG_DIR must be an absolute path, now is "{path}"'.format(path=path))
-    # 路径必须存在
-    if not os.path.exists(path):
-        if path == default:
-            raise ValueError(
-                'The log file was not found in the default path "{path}". Please use the "swanlab watch -l <LOG '
-                'PATH>" command to specify the location of the log path."'.format(path=path)
-            )
-        else:
-            raise ValueError('SWANLAB_LOG_DIR must be an existing path, now is "{path}"'.format(path=path))
-    # 路径必须是一个目录
-    if not os.path.isdir(path):
-        raise ValueError('SWANLAB_LOG_DIR must be a directory, now is "{path}"'.format(path=path))
+    # 严格模式路径必须存在
+    assert_exist(
+        path,
+        target_type="folder",
+        desc='The log file was not found in the default path "{path}". '
+             'Please use the "swanlab watch -l <LOG '
+             'PATH>" command to specify the location of the log path."'.format(path=path)
+        if path == default else 'SWANLAB_LOG_DIR must be an existing path, now is "{path}"'.format(path=path),
+        t_desc='SWANLAB_LOG_DIR must be a directory, now is "{path}"'.format(path=path)
+    )
     _env[ROOT] = path
     return path
 
@@ -143,6 +177,7 @@ def is_dev(env: Optional[Env] = None) -> bool:
 
 # 所有的初始化函数
 function_list = [
+    get_mode,
     get_swanlog_dir,
     get_server_port,
     get_server_host,
@@ -150,7 +185,6 @@ def is_dev(env: Optional[Env] = None) -> bool:
 ]
 
 
-# 定义初始化函数
 def init_env(env: Optional[Env] = None):
     """初始化环境变量
 
@@ -159,32 +193,32 @@ def init_env(env: Optional[Env] = None):
     env : Optional[Env], optional
         环境变量map,可以是任意实现了MutableMapping的对象, 默认将使用os.environ
     """
+    reset_env()
     for func in function_list:
         func(env)
 
 
-# ---------------------------------- 计算变量 ----------------------------------
-DATABASE_PATH = "SWANLAB_DB_PATH"
-"""日志目录SWANLAB_LOG_DIR，日志文件存放在这个目录下"""
+def reset_env():
+    """重置
+    """
+    _env.clear()
 
 
-# ---------------------------------- 定义变量访问方法 ----------------------------------
+# ---------------------------------- 定义计算变量访问方法 ----------------------------------
 
 
-def get_db_path() -> Optional[str]:
-    """获取数据库路径，这是一个计算变量，
-    通过`get_swanlog_dir()`返回值得到
+def is_strict_mode() -> bool:
+    """
+    是否是严格模式，严格模式下会要求文件路径存在，否则会抛出异常
+    """
+    return get_mode() != SwanLabMode.DISABLED.value
 
-    Returns
-    -------
-    Optional[str]
-        数据库文件路径
+
+def get_db_path() -> Optional[str]:
     """
-    if _env.get(DATABASE_PATH) is not None:
-        return _env.get(DATABASE_PATH)
-    # 否则从环境变量中提取
-    _env[DATABASE_PATH] = os.path.join(get_swanlog_dir(), "runs.swanlab")
-    return _env.get(DATABASE_PATH)
+    获取数据库路径，这是一个计算变量，每次调用都会重新计算
+    """
+    return os.path.join(get_swanlog_dir(), "runs.swanlab")
 
 
 def is_windows() -> bool:
@@ -227,6 +261,37 @@ def get_swanlab_folder() -> str:
     """
     user_home = get_user_home()
     swanlab_folder = os.path.join(user_home, ".swanlab")
-    if not os.path.exists(swanlab_folder):
+    try:
+        if not assert_exist(swanlab_folder, ra=False, target_type="folder"):
+            os.mkdir(swanlab_folder)
+    except NotADirectoryError:
+        os.remove(swanlab_folder)
         os.mkdir(swanlab_folder)
     return swanlab_folder
+
+
+def assert_exist(path: str, target_type: str = None, ra: bool = True, desc: str = None, t_desc: str = None) -> bool:
+    """
+    检查文件是否存在，严格模式下，文件不存在会抛出异常，或者可以手动通过参数控制，存在则返回True，否则返回False
+    :param path: 文件路径
+    :param target_type: 文件类型(folder, file)，如果文件类型与预期不符，会抛出异常，非严格模式下不检测，为None不检测文件类型
+    :param ra: 文件不存在时是否抛出异常，非严格模式下强制不抛出，此参数无效
+    :param desc: 异常描述信息，非严格模式下强制不抛出，此参数无效
+    :param t_desc: 文件类型描述信息，非严格模式下强制不抛出，此参数无效
+    """
+    if not is_strict_mode():
+        return os.path.exists(path)
+    if not os.path.exists(path):
+        if ra or target_type is not None:
+            raise FileNotFoundError(desc or "{path} not existed".format(path=path))
+        else:
+            return False
+    # 检查文件类型
+    if target_type is not None:
+        if target_type == "folder":
+            if not os.path.isdir(path):
+                raise NotADirectoryError(t_desc or "{path} is not a folder".format(path=path))
+        elif target_type == "file":
+            if not os.path.isfile(path):
+                raise IsADirectoryError(t_desc or "{path} is not a file".format(path=path))
+    return True
diff --git a/swanlab/log/log.py b/swanlab/log/log.py
index 50f91d4f..a62cca76 100644
--- a/swanlab/log/log.py
+++ b/swanlab/log/log.py
@@ -124,24 +124,29 @@ def __init__(self, name=__name__.lower(), level="info"):
         self.prefix = name + ':'
         self.__logger = logging.getLogger(name)
         self.__original_level = self._get_level(level)
-        self.__now_level = None
-        # 设置日志等级
+        self.__installed = False
         self.__logger.setLevel(self.__original_level)
         # 初始化控制台日志处理器
         self.__handler = logging.StreamHandler(sys.stdout)
         # 添加颜色格式化，并在此处设置格式化后的输出流是否可以被其他处理器处理
         colored_formatter = ColoredFormatter("%(name)s: %(message)s")
         self.__handler.setFormatter(colored_formatter)
-        self.__logger.addHandler(self.__handler)
+        self.enable_log()
         # 控制台监控记录器
         self.__consoler = SwanConsoler()
 
+    def disable_log(self):
+        self.__logger.removeHandler(self.__handler)
+
+    def enable_log(self):
+        self.__logger.addHandler(self.__handler)
+
     @property
     def installed(self):
         """
         判断是否已经install
         """
-        return self.__now_level is not None
+        return self.__installed
 
     def install(self, console_dir: str = None, log_level: str = None) -> "SwanLog":
         """
@@ -159,13 +164,13 @@ def install(self, console_dir: str = None, log_level: str = None) -> "SwanLog":
         if self.installed:
             raise RuntimeError("SwanLog has been installed")
         # 设置日志等级
-        log_level = log_level if log_level else "info"
-        self.__now_level = self._get_level(log_level)
-        self.__logger.setLevel(self.__now_level)
+        if log_level is not None:
+            self.set_level(log_level)
         # 初始化控制台记录器
         if console_dir:
             self.debug("Init consoler to record console log")
             self.__consoler.install(console_dir)
+        self.__installed = True
         return self
 
     def uninstall(self):
@@ -178,8 +183,8 @@ def uninstall(self):
             raise RuntimeError("SwanLog has not been installed")
         self.debug("uninstall swanlog, reset consoler")
         self.__logger.setLevel(self.__original_level)
-        self.__now_level = None
         self.__consoler.uninstall()
+        self.__installed = False
 
     @property
     def write_callback(self):
diff --git a/swanlab/package.py b/swanlab/package.py
index 20f7315d..aafa303a 100644
--- a/swanlab/package.py
+++ b/swanlab/package.py
@@ -9,7 +9,7 @@
 """
 import json
 import os
-from .env import is_dev, get_swanlab_folder
+from .env import is_dev, get_swanlab_folder, is_strict_mode
 from .utils.key import get_key
 from .error import KeyFileError
 
@@ -149,6 +149,8 @@ def version_limit(path: str, mode: str) -> None:
     ValueError
         如果版本号低于v0.1.5则抛出异常
     """
+    if not is_strict_mode():
+        return
     # 判断文件夹内是否存在runs.swanlog文件
     if os.path.exists(os.path.join(path, "runs.swanlog")):
         return


Test patch:
diff --git a/test/create_experiment.py b/test/create_experiment.py
index 3aa780f9..fd8fb7c7 100644
--- a/test/create_experiment.py
+++ b/test/create_experiment.py
@@ -28,7 +28,7 @@
     log_level="debug",
     config="test/config/config.json",
     load="test/config/load.yaml",
-    cloud=True,
+    mode="cloud",
 )
 swanlab.config.epoches = epochs
 swanlab.config.learning_rate = lr
diff --git a/test/unit/data/pytest_sdk.py b/test/unit/data/pytest_sdk.py
index 0b6eb8c8..3fd565bf 100644
--- a/test/unit/data/pytest_sdk.py
+++ b/test/unit/data/pytest_sdk.py
@@ -7,3 +7,162 @@
 @Description:
     测试sdk的一些api
 """
+from swanlab import init
+from swanlab.env import (
+    reset_env,
+    MODE,
+    ROOT,
+)
+from tutils import TEMP_PATH
+from swanlab.log import swanlog
+from swanlab.data.run import get_run
+from nanoid import generate
+import pytest
+import os
+
+
+@pytest.fixture(scope="function", autouse=True)
+def setup_function():
+    """
+    在当前测试文件下的每个测试函数执行前后执行
+    """
+    reset_env()
+    swanlog.disable_log()
+    yield
+    swanlog.enable_log()
+    if get_run() is not None:
+        get_run().finish()
+
+
+class TestInitMode:
+    """
+    测试init时函数的mode参数设置行为
+    """
+
+    def test_init_disabled(self):
+        run = init(mode="disabled", logdir=generate())
+        assert os.environ[MODE] == "disabled"
+        run.log({"TestInitMode": 1})  # 不会报错
+        a = run.settings.run_dir
+        assert not os.path.exists(a)
+        assert get_run() is not None
+
+    def test_init_local(self):
+        run = init(mode="local")
+        assert os.environ[MODE] == "local"
+        run.log({"TestInitMode": 1})  # 不会报错
+        assert get_run() is not None
+
+    def test_init_cloud(self):
+        run = init(mode="cloud")
+        assert os.environ[MODE] == "cloud"
+        run.log({"TestInitMode": 1})  # 不会报错
+        assert get_run() is not None
+
+    def test_init_error(self):
+        with pytest.raises(ValueError):
+            init(mode="123456")
+        assert get_run() is None
+
+    # ---------------------------------- 测试环境变量输入 ----------------------------------
+
+    def test_init_disabled_env(self):
+        os.environ[MODE] = "disabled"
+        run = init()
+        assert os.environ[MODE] == "disabled"
+        run.log({"TestInitMode": 1})
+        a = run.settings.run_dir
+        assert not os.path.exists(a)
+        assert get_run() is not None
+
+    def test_init_local_env(self):
+        os.environ[MODE] = "local"
+        run = init()
+        assert os.environ[MODE] == "local"
+        run.log({"TestInitMode": 1})
+
+    def test_init_cloud_env(self):
+        os.environ[MODE] = "cloud"
+        run = init()
+        assert os.environ[MODE] == "cloud"
+        run.log({"TestInitMode": 1})
+
+    # ---------------------------------- 环境变量和mode的情况 ----------------------------------
+
+    def test_init_disabled_env_mode(self):
+        os.environ[MODE] = "local"
+        run = init(mode="disabled")
+        assert os.environ[MODE] == "disabled"
+        run.log({"TestInitMode": 1})
+        a = run.settings.run_dir
+        assert not os.path.exists(a)
+        assert get_run() is not None
+
+
+class TestInitProject:
+    """
+    测试init时函数的project参数设置行为
+    """
+
+    def test_init_project_none(self):
+        """
+        设置project为None
+        """
+        run = init(project=None, mode="disabled")
+        assert run.project_name == os.path.basename(os.getcwd())
+
+    def test_init_project(self):
+        """
+        设置project为字符串
+        """
+        project = "test_project"
+        run = init(project=project, mode="disabled")
+        assert run.project_name == project
+
+
+class TestInitLogdir:
+    """
+    测试init时函数的logdir参数设置行为
+    """
+
+    def test_init_logdir_disabled(self):
+        """
+        disabled模式下设置logdir不生效，采用的是环境变量的设置
+        """
+        logdir = generate()
+        run = init(logdir=logdir, mode="disabled")
+        assert run.settings.swanlog_dir != logdir
+        assert run.settings.swanlog_dir == os.environ[ROOT]
+        run.finish()
+        del os.environ[ROOT]
+        run = init(logdir=logdir, mode="disabled")
+        assert run.settings.swanlog_dir != logdir
+        assert run.settings.swanlog_dir == os.path.join(os.getcwd(), "swanlog")
+
+    def test_init_logdir_enabled(self):
+        """
+        其他模式下设置logdir生效
+        """
+        logdir = os.path.join(TEMP_PATH, generate()).__str__()
+        run = init(logdir=logdir, mode="local")
+        assert run.settings.swanlog_dir == logdir
+        run.finish()
+        del os.environ[ROOT]
+        logdir = os.path.join(TEMP_PATH, generate()).__str__()
+        run = init(logdir=logdir, mode="local")
+        assert run.settings.swanlog_dir == logdir
+
+    def test_init_logdir_env(self):
+        """
+        通过环境变量设置logdir
+        """
+        logdir = os.path.join(TEMP_PATH, generate()).__str__()
+        os.environ[ROOT] = logdir
+        run = init(mode="local")
+        assert run.settings.swanlog_dir == logdir
+        run.finish()
+        del os.environ[ROOT]
+        logdir = os.path.join(TEMP_PATH, generate()).__str__()
+        os.environ[ROOT] = logdir
+        run = init(mode="local")
+        assert run.settings.swanlog_dir == logdir
diff --git a/test/unit/data/run/pytest_main.py b/test/unit/data/run/pytest_main.py
index 2bf4a18e..bb717928 100644
--- a/test/unit/data/run/pytest_main.py
+++ b/test/unit/data/run/pytest_main.py
@@ -9,10 +9,25 @@
 """
 from swanlab.data.run.main import SwanLabRun, get_run, SwanLabRunState, swanlog
 from nanoid import generate
+from tutils import clear
 import pytest
+import random
 
 
-class TestSwanLabRun:
+@pytest.fixture(scope="function", autouse=True)
+def setup_function():
+    """
+    在当前测试文件下的每个测试函数执行前后执行
+    """
+    if get_run() is not None:
+        get_run().finish()
+    swanlog.disable_log()
+    yield
+    clear()
+    swanlog.enable_log()
+
+
+class TestSwanLabRunInit:
 
     def test_before_init(self):
         run = get_run()
@@ -21,7 +36,7 @@ def test_before_init(self):
 
     def test_after_init(self):
         run = SwanLabRun(generate())
-        assert swanlog.installed is True
+        assert swanlog.installed is False
         assert run is not None
         assert get_run().__str__() == run.__str__()
         _run = run.finish()
@@ -33,7 +48,206 @@ def test_duplicate_init(self):
         run = SwanLabRun(generate())
         with pytest.raises(RuntimeError) as e:
             SwanLabRun(generate())
+        assert swanlog.installed is False
         assert str(e.value) == "SwanLabRun has been initialized"
         assert run.__str__() == get_run().__str__()
         _run = run.finish()
         assert swanlog.installed is False
+
+
+class TestSwanLabRunLog:
+    """
+    测试SwanLabRun的日志解析功能
+    1. 输入为字典
+    2. 可包含参数step
+    3. 输入的字典的key必须为字符串
+    4. 输出解析后的数据对象，包含额外的信息，step等信息
+    5. 如果某一个解析失败，对应的key存在，但返回的数据为None
+    TODO 编写和媒体相关的测试脚本
+    """
+
+    @pytest.mark.parametrize("data", [
+        random.randint(1, 100),
+        -random.randint(1, 100),
+        random.random(),
+    ])
+    def test_log_number_ok(self, data):
+        """
+        测试解析一个正常的数字
+        """
+        run = SwanLabRun()
+        metric_dict = run.log({"a": data})
+        assert metric_dict["a"] is not None
+        assert len(metric_dict) == 1
+        a = metric_dict["a"]
+        ac = a.column_info
+        # ---------------------------------- 指标信息 ----------------------------------
+        assert len(a.metric) == 3
+        assert 'index' in a.metric
+        assert 'create_time' in a.metric
+        assert 'data' in a.metric
+        assert a.metric['data'] == data
+        assert a.step == 0
+        assert a.epoch == 1
+        # ---------------------------------- 列信息 ----------------------------------
+        assert ac.data_type == "default"
+        assert ac.error is None
+        # 默认排在最前面
+        assert ac.sort == 0
+        assert ac.config == {}
+        assert ac.key == "a"
+        assert ac.chart_type == "default"
+        assert ac.namespace == "default"
+        assert ac.reference == "step"
+
+    def test_log_number_nan(self):
+        """
+        测试解析一个nan
+        """
+        run = SwanLabRun()
+        metric_dict = run.log({"a": float("nan")})
+        assert metric_dict["a"] is not None
+        assert len(metric_dict) == 1
+        a = metric_dict["a"]
+        ac = a.column_info
+        # ---------------------------------- 指标信息 ----------------------------------
+        assert a.metric is None
+        # ---------------------------------- 列信息 ----------------------------------
+        assert ac.data_type == "default"
+        assert ac.error['data_class'] == "NaN"
+        assert ac.error['excepted'] == ['float', 'int']
+        # 默认排在最前面
+        assert ac.sort == 0
+        assert ac.config == {}
+        assert ac.key == "a"
+        assert ac.chart_type == "default"
+        assert ac.namespace == "default"
+        assert ac.reference == "step"
+
+    def test_log_number_str(self):
+        """
+        测试解析其他字符串
+        """
+        """
+        测试解析一个nan
+        """
+        run = SwanLabRun()
+        metric_dict = run.log({"a": 'abc'})
+        assert metric_dict["a"] is not None
+        assert len(metric_dict) == 1
+        a = metric_dict["a"]
+        ac = a.column_info
+        # ---------------------------------- 指标信息 ----------------------------------
+        assert a.metric is None
+        # ---------------------------------- 列信息 ----------------------------------
+        assert ac.data_type == "default"
+        assert ac.error['data_class'] == "str"
+        assert ac.error['excepted'] == ['float', 'int']
+        # 默认排在最前面
+        assert ac.sort == 0
+        assert ac.config == {}
+        assert ac.key == "a"
+        assert ac.chart_type == "default"
+        assert ac.namespace == "default"
+        assert ac.reference == "step"
+
+    def test_log_number_use_step(self):
+        """
+        测试解析一个数字，使用step
+        """
+        run = SwanLabRun()
+        metric_dict = run.log({"a": 1}, step=1)
+        assert metric_dict["a"] is not None
+        assert len(metric_dict) == 1
+        a = metric_dict["a"]
+        ac = a.column_info
+        # ---------------------------------- 指标信息 ----------------------------------
+        assert len(a.metric) == 3
+        assert 'index' in a.metric
+        assert 'create_time' in a.metric
+        assert 'data' in a.metric
+        assert a.metric['data'] == 1
+        assert a.step == 1
+        assert a.epoch == 1
+        # ---------------------------------- 列信息 ----------------------------------
+        assert ac.data_type == "default"
+        assert ac.error is None
+        # 默认排在最前面
+        assert ac.sort == 0
+        assert ac.config == {}
+        assert ac.key == "a"
+        assert ac.chart_type == "default"
+        assert ac.namespace == "default"
+        assert ac.reference == "step"
+
+    def test_log_number_use_step_duplicate(self):
+        """
+        测试解析一个数字，使用step，但是重复执行
+        """
+        run = SwanLabRun()
+        run.log({"a": 1}, step=1)
+        metric_dict = run.log({"a": 1}, step=1)
+        assert metric_dict["a"] is not None
+        assert len(metric_dict) == 1
+        a = metric_dict["a"]
+        assert a.error is True
+
+    def test_log_number_use_prefix(self):
+        """
+        测试解析一个数字，使用prefix
+        """
+        run = SwanLabRun()
+        prefix_1 = generate()
+        prefix_2 = generate() + '/' + generate()
+        key1 = f"{prefix_1}/a"
+        key2 = f"{prefix_2}/a"
+        metric_dict = run.log({key1: 1, key2: 1})
+        assert len(metric_dict) == 2
+        for key in metric_dict:
+            assert metric_dict[key] is not None
+            a = metric_dict[key]
+            ac = a.column_info
+            # ---------------------------------- 指标信息 ----------------------------------
+            assert len(a.metric) == 3
+            assert 'index' in a.metric
+            assert 'create_time' in a.metric
+            assert 'data' in a.metric
+            assert a.metric['data'] == 1
+            assert a.step == 0
+            assert a.epoch == 1
+            # ---------------------------------- 列信息 ----------------------------------
+            assert ac.data_type == "default"
+            assert ac.error is None
+            # 默认排在最前面
+            assert ac.sort is None
+            assert ac.config == {}
+            assert ac.key == key
+            assert ac.chart_type == "default"
+            assert ac.namespace == key.split('/')[0]
+            assert ac.reference == "step"
+
+
+class TestSwanLabRunState:
+    """
+    测试SwanLabRun的状态变化
+    """
+
+    def test_not_started(self):
+        assert SwanLabRun.get_state() == SwanLabRunState.NOT_STARTED
+
+    def test_running(self):
+        run = SwanLabRun()
+        assert run.state == SwanLabRunState.RUNNING
+        assert run.is_running is True
+
+    def test_crashed(self):
+        run = SwanLabRun()
+        run.finish(SwanLabRunState.CRASHED, error="error")
+        assert run.state == SwanLabRunState.CRASHED
+        assert run.is_crashed is True
+
+    def test_success(self):
+        run = SwanLabRun()
+        run.finish(SwanLabRunState.SUCCESS)
+        assert run.state == SwanLabRunState.SUCCESS
+        assert run.is_success is True
diff --git a/test/unit/pytest_env.py b/test/unit/pytest_env.py
new file mode 100644
index 00000000..dad56716
--- /dev/null
+++ b/test/unit/pytest_env.py
@@ -0,0 +1,147 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/5/24 20:25
+@File: pytest_env.py
+@IDE: pycharm
+@Description:
+    测试swanlab/env.py中的环境变量和工具函数
+"""
+from swanlab.env import (
+    SwanLabMode,
+    get_mode,
+    assert_exist,
+    reset_env,
+    is_strict_mode,
+    MODE
+)
+from nanoid import generate
+import pytest
+import os
+
+
+@pytest.fixture(scope="function", autouse=True)
+def setup_function():
+    """
+    在当前测试文件下的每个测试函数执行前后执行
+    """
+    reset_env()
+    if MODE in os.environ:
+        del os.environ[MODE]
+    yield
+    reset_env()
+    if MODE in os.environ:
+        del os.environ[MODE]
+
+
+def use_strict_mode():
+    """
+    使用严格模式
+    """
+    get_mode({MODE: SwanLabMode.CLOUD.value})
+
+
+def use_not_strict_mode():
+    """
+    使用非严格模式
+    """
+    get_mode({MODE: SwanLabMode.DISABLED.value})
+
+
+class TestStrictMode:
+
+    def test_strict_mode(self):
+        """
+        测试默认严格模式
+        """
+        assert is_strict_mode() is True
+
+    def test_strict_mode_cloud(self):
+        """
+        测试严格模式
+        """
+        use_strict_mode()
+        assert is_strict_mode() is True
+
+    def test_strict_mode_disabled(self):
+        """
+        测试非严格模式
+        """
+        use_not_strict_mode()
+        assert is_strict_mode() is False
+
+    def test_strict_mode_local(self):
+        """
+        测试本地模式
+        """
+        use_strict_mode()
+        assert is_strict_mode() is True
+
+    def test_strict_mode_error(self):
+        """
+        测试重定向模式
+        """
+        with pytest.raises(ValueError):
+            get_mode({MODE: generate()})
+
+
+class TestAssertExist:
+
+    def test_exist_ok(self):
+        """
+        测试严格模式下，文件路径存在
+        """
+        assert assert_exist(__file__) is True
+
+    def test_exist_ok_not_strict(self):
+        """
+        测试非严格模式下，文件路径存在
+        """
+        use_not_strict_mode()
+        assert assert_exist(__file__) is True
+
+    def test_exist_error_strict(self):
+        """
+        测试严格模式下，文件路径不存在
+        """
+        use_strict_mode()
+        with pytest.raises(FileNotFoundError):
+            assert_exist(generate())
+        assert assert_exist(generate(), ra=False) is False
+
+    def test_exist_error_not_strict(self):
+        """
+        测试非严格模式下，文件路径不存在
+        """
+        use_not_strict_mode()
+        assert assert_exist(generate()) is False
+
+    def test_file_error_strict(self):
+        """
+        测试严格模式下，文件路径是文件夹
+        """
+        use_strict_mode()
+        with pytest.raises(NotADirectoryError):
+            assert_exist(__file__, target_type="folder")
+
+    def test_file_error_not_strict(self):
+        """
+        测试非严格模式下，文件路径是文件夹
+        """
+        use_not_strict_mode()
+        assert assert_exist(__file__, target_type="folder") is True
+
+    def test_folder_error_strict(self):
+        """
+        测试严格模式下，文件夹路径是文件
+        """
+        use_strict_mode()
+        with pytest.raises(IsADirectoryError):
+            assert_exist(os.path.dirname(__file__), target_type="file")
+
+    def test_folder_error_not_strict(self):
+        """
+        测试非严格模式下，文件夹路径是文件
+        """
+        use_not_strict_mode()
+        assert assert_exist(os.path.dirname(__file__), target_type="file") is True