ErisPulse 2.8.0.dev2__tar.gz → 2.8.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/.gitignore +2 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/PKG-INFO +3 -1
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/README.pypi.md +2 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/pyproject.toml +1 -1
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/create.py +5 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/run.py +1 -1
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/utils/package_manager.py +18 -97
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/__init__.py +6 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/errors.py +109 -2
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/kv_builder.py +21 -19
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/module.py +3 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/router.py +3 -1
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/sql_base.py +38 -2
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/base.py +19 -2
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/command.py +197 -47
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/interaction.py +22 -30
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/overrides.py +15 -3
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/__init__.py +6 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/adapter.py +30 -8
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/client.py +35 -10
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/config.py +124 -41
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/constants.py +52 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/i18n/__init__.py +25 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/i18n/locales/en.py +32 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/i18n/locales/ja.py +32 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/i18n/locales/ru.py +32 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/i18n/locales/zh_cn.py +32 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/i18n/locales/zh_tw.py +32 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/lifecycle.py +93 -23
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/module.py +56 -21
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/router.py +31 -12
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/scope.py +10 -5
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/__init__.py +24 -7
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/finders/bases/finder.py +5 -2
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/loaders/adapter.py +20 -4
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/loaders/bases/loader.py +90 -1
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/loaders/module.py +48 -13
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/loaders/plugin_folder.py +29 -1
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/loaders/strict.py +1 -17
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/runtime/__init__.py +8 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/runtime/context.py +33 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/runtime/diagnostics.py +11 -2
- {erispulse-2.8.0.dev2/src/ErisPulse/CLI/utils → erispulse-2.8.2/src/ErisPulse/runtime}/file_watcher.py +1 -1
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/runtime/frame_config.py +8 -0
- erispulse-2.8.2/src/ErisPulse/runtime/owner_cleanup.py +171 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/runtime/plugin_reload.py +2 -2
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/runtime/tasks.py +19 -1
- erispulse-2.8.2/src/ErisPulse/runtime/version.py +166 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/sdk.py +58 -4
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/LICENSE +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/__init__.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/base.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/cli.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/__init__.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/config.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/doctor.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/init.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/install.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/language.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/list.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/list_remote.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/self_update.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/types.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/uninstall.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/upgrade.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/console.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/constants.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/hints.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/i18n/__init__.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/i18n/locales/__init__.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/i18n/locales/en.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/i18n/locales/ja.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/i18n/locales/ru.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/i18n/locales/zh_cn.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/i18n/locales/zh_tw.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/registry.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/utils/__init__.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/utils/config_wizard.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/utils/display.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/utils/scaffold_text.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/adapter.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/client.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/config_schema.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/converter.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/i18n_schema.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/manager.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/send_builder.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/send_rules.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/storage.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/websocket.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/__init__.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/message.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/message_builder.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/meta.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/notice.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/request.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/session_type.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/wrapper.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/assets/__init__.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/assets/error.css +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/assets/error.html +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/assets/root.css +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/assets/root.html +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/i18n/constants.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/i18n/locales/__init__.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/logger.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/master.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/storage/__init__.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/storage/mysql.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/storage/postgres.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/storage/sqlite.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/text_match.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/transcript.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/__main__.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/finders/__init__.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/finders/adapter.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/finders/bases/__init__.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/finders/module.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/loaders/__init__.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/loaders/bases/__init__.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/loaders/strategy.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/py.typed +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/runtime/config_schema.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/runtime/example_config.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/runtime/exceptions.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/runtime/hints.py +0 -0
- {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/runtime/memory.py +0 -0
|
@@ -3,6 +3,7 @@ devs/videos
|
|
|
3
3
|
main.py
|
|
4
4
|
plugins/
|
|
5
5
|
venvs
|
|
6
|
+
config.full.example
|
|
6
7
|
config.toml
|
|
7
8
|
uv.lock
|
|
8
9
|
snapshots/
|
|
@@ -16,6 +17,7 @@ config/config.db
|
|
|
16
17
|
config/config.db-journal
|
|
17
18
|
config/config.db-wal
|
|
18
19
|
config/config.db-shm
|
|
20
|
+
.erispulse_config.lock
|
|
19
21
|
|
|
20
22
|
# site-packages
|
|
21
23
|
config/.packages
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ErisPulse
|
|
3
|
-
Version: 2.8.
|
|
3
|
+
Version: 2.8.2
|
|
4
4
|
Summary: Event-driven modular async bot framework — write once, deploy to any platform | 事件驱动的模块化异步机器人框架,一次编写,多平台部署
|
|
5
5
|
Project-URL: Homepage, https://github.com/ErisPulse/ErisPulse
|
|
6
6
|
Project-URL: Documentation, https://www.erisdev.com#docs
|
|
@@ -57,6 +57,8 @@ An event-driven, multi-platform chatbot development framework based on the OneBo
|
|
|
57
57
|
|
|
58
58
|
[](https://pypi.org/project/ErisPulse/)
|
|
59
59
|
[](https://pypi.org/project/ErisPulse/)
|
|
60
|
+
[](https://github.com/botuniverse/onebot-11)
|
|
61
|
+
[](https://12.onebot.dev/)
|
|
60
62
|
[](https://github.com/ErisPulse/ErisPulse/blob/main/LICENSE)
|
|
61
63
|
[](https://www.erisdev.com)
|
|
62
64
|
[](https://github.com/ErisPulse/ErisPulse)
|
|
@@ -6,6 +6,8 @@ An event-driven, multi-platform chatbot development framework based on the OneBo
|
|
|
6
6
|
|
|
7
7
|
[](https://pypi.org/project/ErisPulse/)
|
|
8
8
|
[](https://pypi.org/project/ErisPulse/)
|
|
9
|
+
[](https://github.com/botuniverse/onebot-11)
|
|
10
|
+
[](https://12.onebot.dev/)
|
|
9
11
|
[](https://github.com/ErisPulse/ErisPulse/blob/main/LICENSE)
|
|
10
12
|
[](https://www.erisdev.com)
|
|
11
13
|
[](https://github.com/ErisPulse/ErisPulse)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ErisPulse"
|
|
7
|
-
version = "2.8.
|
|
7
|
+
version = "2.8.2"
|
|
8
8
|
description = "Event-driven modular async bot framework — write once, deploy to any platform | 事件驱动的模块化异步机器人框架,一次编写,多平台部署"
|
|
9
9
|
readme = "README.pypi.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -166,6 +166,8 @@ class Main(BaseModule):
|
|
|
166
166
|
description={{\"i18n\": \"module.{name}.meta.description\", \"default\": \"{name} module\"}},
|
|
167
167
|
version=\"0.1.0\",
|
|
168
168
|
author=\"ErisDev\",
|
|
169
|
+
# 最低 SDK 版本(可选):不满足时框架在加载期明确报错并跳过本模块
|
|
170
|
+
min_sdk_version=\"2.8.0\",
|
|
169
171
|
group=\"default\",
|
|
170
172
|
tags=[\"{name}\"],
|
|
171
173
|
# 对外服务白名单(可选):声明后其他模块可经 sdk.module.call() 调用这些方法;
|
|
@@ -297,6 +299,9 @@ class {name}(BaseAdapter):
|
|
|
297
299
|
depends: ClassVar[dict] = {{}}
|
|
298
300
|
optional_modules: ClassVar[list] = []
|
|
299
301
|
|
|
302
|
+
# 声明所需最低 SDK 版本:不满足时框架在加载期明确报错并跳过本适配器(可选)
|
|
303
|
+
min_sdk_version = "2.8.0"
|
|
304
|
+
|
|
300
305
|
# {text[adapter.config_hint]}
|
|
301
306
|
@dataclass
|
|
302
307
|
class ConfigClass(BaseConfig):
|
|
@@ -16,11 +16,11 @@ from pathlib import Path
|
|
|
16
16
|
|
|
17
17
|
from rich.panel import Panel
|
|
18
18
|
|
|
19
|
+
from ...runtime.file_watcher import FileSystemEventHandler, PollingObserver
|
|
19
20
|
from ..base import Command
|
|
20
21
|
from ..console import console
|
|
21
22
|
from ..constants import ENV_SUPERVISED, HARD_RESTART_EXIT_CODE
|
|
22
23
|
from ..i18n import i18n
|
|
23
|
-
from ..utils.file_watcher import FileSystemEventHandler, PollingObserver
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class ReloadHandler(FileSystemEventHandler):
|
|
@@ -26,54 +26,8 @@ from ..console import console
|
|
|
26
26
|
from ..constants import PYPI_PACKAGE_JSON_URL_TEMPLATE
|
|
27
27
|
from ..i18n import i18n
|
|
28
28
|
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
# post 版本 (1.0.postN) 与本地版本 (1.0+local)。
|
|
32
|
-
# 例如 2.5.0-dev.1 / 2.5.0a1 / 1.0rc1 / 1.0.post1 / 1.0+local / 1!1.0 均可解析。
|
|
33
|
-
# 不支持的格式(如 1.0.post1.dev1 这类 post 后跟 dev)由 _parse_version 返回 None 退化处理。
|
|
34
|
-
_VERSION_RE = re.compile(
|
|
35
|
-
r"^\s*v?(?P<epoch>\d+!)?(?P<release>\d+(?:\.\d+)*)"
|
|
36
|
-
r"(?:[-._]?(?P<pre>dev|alpha|beta|rc|a|b|c|pre)[-._]?(?P<num>\d*))?"
|
|
37
|
-
r"(?:[-._]?post[-._]?(?P<post>\d+))?"
|
|
38
|
-
r"(?:\+(?P<local>[a-zA-Z0-9]+(?:[.-][a-zA-Z0-9]+)*))?",
|
|
39
|
-
re.IGNORECASE,
|
|
40
|
-
)
|
|
41
|
-
# 预发布类型排序权重:正式版 > rc > beta > alpha > dev
|
|
42
|
-
_PRE_RELEASE_RANK = {
|
|
43
|
-
"dev": 0,
|
|
44
|
-
"alpha": 1,
|
|
45
|
-
"a": 1,
|
|
46
|
-
"beta": 2,
|
|
47
|
-
"b": 2,
|
|
48
|
-
"rc": 3,
|
|
49
|
-
"c": 3,
|
|
50
|
-
"pre": 3,
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _parse_version(version: str) -> dict | None:
|
|
55
|
-
"""
|
|
56
|
-
将版本号解析为结构化组件(PEP 440 子集,纯标准库)。
|
|
57
|
-
|
|
58
|
-
与 :meth:`PackageManager._version_key` / :meth:`PackageManager._is_pre_release`
|
|
59
|
-
共用同一解析口径,避免不同正则导致判定分歧(如 ``1.0c2`` 此前在
|
|
60
|
-
``_is_pre_release`` 与 ``_version_key`` 间判定口径不一致)。
|
|
61
|
-
|
|
62
|
-
:param version: [str] 版本号字符串
|
|
63
|
-
:return: [Optional[dict]] 含 epoch/release/pre_type/pre_num/post/local 的字典,
|
|
64
|
-
无法解析时返回 None
|
|
65
|
-
"""
|
|
66
|
-
match = _VERSION_RE.match(str(version).strip().lstrip("vV"))
|
|
67
|
-
if not match:
|
|
68
|
-
return None
|
|
69
|
-
return {
|
|
70
|
-
"epoch": int((match.group("epoch") or "0").rstrip("!") or 0),
|
|
71
|
-
"release": match.group("release"),
|
|
72
|
-
"pre_type": match.group("pre"),
|
|
73
|
-
"pre_num": match.group("num"),
|
|
74
|
-
"post": match.group("post"),
|
|
75
|
-
"local": match.group("local"),
|
|
76
|
-
}
|
|
29
|
+
# 版本号解析与比较的唯一实现在 runtime/version.py(PEP 440 子集,纯标准库);
|
|
30
|
+
# 本模块经下方方法级委托复用同一解析口径,避免双正则导致判定分歧
|
|
77
31
|
|
|
78
32
|
|
|
79
33
|
class PackageManager:
|
|
@@ -853,63 +807,28 @@ class PackageManager:
|
|
|
853
807
|
"""
|
|
854
808
|
将版本号解析为可比较的元组键
|
|
855
809
|
|
|
856
|
-
|
|
857
|
-
epoch 优先于一切 release 段;本地版本 (+local) 不影响主排序,
|
|
858
|
-
但同一版本号带 local 段者 > 不带 local 段者。
|
|
859
|
-
例如 2.4.5-dev.1 先于 2.4.5 正式版,1.0 < 1.0.post1 < 1.1。
|
|
810
|
+
委托 :func:`ErisPulse.runtime.version.version_key`(框架内唯一实现)。
|
|
860
811
|
|
|
861
812
|
:param version: [str] 版本号字符串
|
|
862
|
-
:return: [tuple]
|
|
813
|
+
:return: [tuple] 逐段可比较的比较键元组
|
|
863
814
|
"""
|
|
864
|
-
|
|
865
|
-
if parsed is None:
|
|
866
|
-
# 无法解析时退化为基础键,保证不抛异常
|
|
867
|
-
return (0, (0, 0, 0, 0), (1,), (0,), ((1, str(version).lower()),))
|
|
868
|
-
|
|
869
|
-
release = tuple(int(x) for x in parsed["release"].split("."))
|
|
870
|
-
# release 段对齐到固定长度,确保 (2.5) 与 (2.5.0) 可正确比较
|
|
871
|
-
padded = release + (0,) * max(0, 4 - len(release))
|
|
872
|
-
|
|
873
|
-
pre_type = parsed["pre_type"]
|
|
874
|
-
if pre_type is None:
|
|
875
|
-
# 正式版:预发布键高于任何预发布版本
|
|
876
|
-
pre_key = (1,)
|
|
877
|
-
else:
|
|
878
|
-
rank = _PRE_RELEASE_RANK.get(pre_type.lower(), 1)
|
|
879
|
-
pre_num = int(parsed["pre_num"] or 0)
|
|
880
|
-
pre_key = (0, rank, pre_num)
|
|
881
|
-
|
|
882
|
-
# post 段:无 post 视为 post0,保证 1.0 == 1.0.post0 < 1.0.post1
|
|
883
|
-
post_num = int(parsed["post"] or 0)
|
|
884
|
-
post_key = (post_num,)
|
|
885
|
-
|
|
886
|
-
local = parsed["local"]
|
|
887
|
-
if local:
|
|
888
|
-
# 本地段按 "数值段 < 字母段" 拆分,避免 int/str 直接比较抛异常
|
|
889
|
-
local_key = tuple(
|
|
890
|
-
(0, int(part)) if part.isdigit() else (1, part.lower())
|
|
891
|
-
for part in local.split(".")
|
|
892
|
-
)
|
|
893
|
-
else:
|
|
894
|
-
local_key = ()
|
|
815
|
+
from ...runtime.version import version_key
|
|
895
816
|
|
|
896
|
-
return (
|
|
817
|
+
return version_key(version)
|
|
897
818
|
|
|
898
819
|
def _compare_versions(self, version1: str, version2: str) -> int:
|
|
899
820
|
"""
|
|
900
821
|
比较两个版本号的大小
|
|
901
822
|
|
|
823
|
+
委托 :func:`ErisPulse.runtime.version.compare_versions`(框架内唯一实现)。
|
|
824
|
+
|
|
902
825
|
:param version1: [str] 第一个版本号
|
|
903
826
|
:param version2: [str] 第二个版本号
|
|
904
827
|
:return: [int] version1 大于/等于/小于 version2 时分别返回 1/0/-1
|
|
905
828
|
"""
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
return 1
|
|
910
|
-
if k1 < k2:
|
|
911
|
-
return -1
|
|
912
|
-
return 0
|
|
829
|
+
from ...runtime.version import compare_versions
|
|
830
|
+
|
|
831
|
+
return compare_versions(version1, version2)
|
|
913
832
|
|
|
914
833
|
def _check_sdk_compatibility(self, min_sdk_version: str) -> tuple[bool, str]:
|
|
915
834
|
"""
|
|
@@ -1435,14 +1354,16 @@ class PackageManager:
|
|
|
1435
1354
|
"""
|
|
1436
1355
|
判断版本号是否为预发布版本
|
|
1437
1356
|
|
|
1438
|
-
与 :meth:`_version_key`
|
|
1439
|
-
|
|
1440
|
-
post 版本 (1.0.post1) 与本地版本 (1.0+local)
|
|
1357
|
+
与 :meth:`_version_key` 共用同一解析口径(:func:`ErisPulse.runtime.version.parse_version`)。
|
|
1358
|
+
当版本号含预发布段 (dev/alpha/beta/rc/c/pre) 时返回 True。
|
|
1359
|
+
post 版本 (1.0.post1) 与本地版本 (1.0+local) 不视为预发布版本
|
|
1441
1360
|
|
|
1442
1361
|
:param version: [str] 版本号字符串
|
|
1443
|
-
:return: [bool]
|
|
1362
|
+
:return: [bool] 是预发布版本时返回 True
|
|
1444
1363
|
"""
|
|
1445
|
-
|
|
1364
|
+
from ...runtime.version import parse_version
|
|
1365
|
+
|
|
1366
|
+
parsed = parse_version(version)
|
|
1446
1367
|
return parsed is not None and parsed["pre_type"] is not None
|
|
1447
1368
|
|
|
1448
1369
|
def update_self(self, target_version: str | None = None, force: bool = False) -> bool:
|
|
@@ -18,14 +18,17 @@ from .errors import (
|
|
|
18
18
|
ClientConnectionError,
|
|
19
19
|
ClientTimeoutError,
|
|
20
20
|
HTTPStatusError,
|
|
21
|
+
InteractionCancelled,
|
|
21
22
|
InteractionError,
|
|
22
23
|
ModuleCallError,
|
|
23
24
|
ModuleCallTimeoutError,
|
|
24
25
|
ModuleError,
|
|
25
26
|
ModuleNotAvailableError,
|
|
26
27
|
ServiceNotProvidedError,
|
|
28
|
+
SessionOccupiedError,
|
|
27
29
|
StorageError,
|
|
28
30
|
StorageUnreachableError,
|
|
31
|
+
StrictModeError,
|
|
29
32
|
WebSocketError,
|
|
30
33
|
WebSocketDisconnect,
|
|
31
34
|
)
|
|
@@ -76,6 +79,7 @@ __all__ = [
|
|
|
76
79
|
# i18n 单键声明
|
|
77
80
|
"I18nKey",
|
|
78
81
|
"AsyncBridge",
|
|
82
|
+
"InteractionCancelled",
|
|
79
83
|
"InteractionError",
|
|
80
84
|
"KVQueryBuilder",
|
|
81
85
|
"ModuleCallError",
|
|
@@ -88,9 +92,11 @@ __all__ = [
|
|
|
88
92
|
"SendContext",
|
|
89
93
|
"SendDSL",
|
|
90
94
|
"ServiceNotProvidedError",
|
|
95
|
+
"SessionOccupiedError",
|
|
91
96
|
"SseEmitter",
|
|
92
97
|
"StorageError",
|
|
93
98
|
"StorageUnreachableError",
|
|
99
|
+
"StrictModeError",
|
|
94
100
|
"WSMessage",
|
|
95
101
|
"WebSocketConnection",
|
|
96
102
|
"WebSocketConnectionBase",
|
|
@@ -10,7 +10,6 @@ ErisPulse 异常体系
|
|
|
10
10
|
3. 底层库异常(aiohttp.ClientError 等)不会泄露到业务代码
|
|
11
11
|
{!--< /tips >!--}
|
|
12
12
|
"""
|
|
13
|
-
|
|
14
13
|
from __future__ import annotations
|
|
15
14
|
|
|
16
15
|
|
|
@@ -30,14 +29,31 @@ class ClientError(ErisPulseError):
|
|
|
30
29
|
所有 HTTP/WS 客户端请求相关的异常基类。
|
|
31
30
|
可用于统一捕获所有客户端错误。
|
|
32
31
|
|
|
32
|
+
:attribute url: 请求 URL(构建异常时提供则非空)
|
|
33
|
+
:attribute method: 请求方法(GET / POST 等,构建异常时提供则非空)
|
|
34
|
+
:attribute attempts: 已尝试的请求次数(重试耗尽时提供)
|
|
35
|
+
|
|
33
36
|
:example:
|
|
34
37
|
>>> from ErisPulse.Core.Bases.errors import ClientError
|
|
35
38
|
>>> try:
|
|
36
39
|
... resp = await sdk.client.get("https://example.com")
|
|
37
40
|
... except ClientError as e:
|
|
38
|
-
... print(f"请求失败: {e}")
|
|
41
|
+
... print(f"请求失败: {e} url={e.url}")
|
|
39
42
|
"""
|
|
40
43
|
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
message: str = "",
|
|
47
|
+
*,
|
|
48
|
+
url: str | None = None,
|
|
49
|
+
method: str | None = None,
|
|
50
|
+
attempts: int | None = None,
|
|
51
|
+
):
|
|
52
|
+
self.url = url
|
|
53
|
+
self.method = method
|
|
54
|
+
self.attempts = attempts
|
|
55
|
+
super().__init__(message)
|
|
56
|
+
|
|
41
57
|
|
|
42
58
|
|
|
43
59
|
|
|
@@ -115,8 +131,26 @@ class StorageUnreachableError(StorageError):
|
|
|
115
131
|
|
|
116
132
|
后端连接池创建在自动重试耗尽后仍失败(数据库不可达 / 凭据错误 /
|
|
117
133
|
网络隔离等)。框架保持运行,存储操作在冷却期内快速失败并自动重连。
|
|
134
|
+
|
|
135
|
+
连接状态变化的运行时感知推荐订阅生命周期事件
|
|
136
|
+
``storage.unreachable`` / ``storage.recovered``(本异常主要供
|
|
137
|
+
直接操作存储底层的场景捕获)。
|
|
138
|
+
|
|
139
|
+
:attribute backend: 存储后端名(sqlite / mysql / postgres)
|
|
140
|
+
:attribute cooldown: 冷却时长(秒,进入冷却时提供)
|
|
118
141
|
"""
|
|
119
142
|
|
|
143
|
+
def __init__(
|
|
144
|
+
self,
|
|
145
|
+
message: str = "",
|
|
146
|
+
*,
|
|
147
|
+
backend: str = "",
|
|
148
|
+
cooldown: float | None = None,
|
|
149
|
+
):
|
|
150
|
+
self.backend = backend
|
|
151
|
+
self.cooldown = cooldown
|
|
152
|
+
super().__init__(message or "storage unreachable")
|
|
153
|
+
|
|
120
154
|
|
|
121
155
|
class InteractionError(ErisPulseError):
|
|
122
156
|
"""
|
|
@@ -176,8 +210,21 @@ class ModuleCallTimeoutError(ModuleCallError):
|
|
|
176
210
|
模块间调用超时
|
|
177
211
|
|
|
178
212
|
目标方法在超时时限内未返回时抛出。
|
|
213
|
+
|
|
214
|
+
:attribute timeout: 超时时限(秒)
|
|
179
215
|
"""
|
|
180
216
|
|
|
217
|
+
def __init__(
|
|
218
|
+
self,
|
|
219
|
+
module: str = "",
|
|
220
|
+
method: str = "",
|
|
221
|
+
message: str = "",
|
|
222
|
+
*,
|
|
223
|
+
timeout: float | None = None,
|
|
224
|
+
):
|
|
225
|
+
self.timeout = timeout
|
|
226
|
+
super().__init__(module, method, message)
|
|
227
|
+
|
|
181
228
|
|
|
182
229
|
|
|
183
230
|
class WebSocketDisconnect(WebSocketError):
|
|
@@ -204,18 +251,78 @@ class WebSocketDisconnect(WebSocketError):
|
|
|
204
251
|
super().__init__(f"code={self.code}, reason={self.reason}")
|
|
205
252
|
|
|
206
253
|
|
|
254
|
+
class InteractionCancelled(InteractionError):
|
|
255
|
+
"""
|
|
256
|
+
交互会话被取消
|
|
257
|
+
|
|
258
|
+
挂起的 ``wait_reply`` / 租约因非超时原因终止时设置到 future 上,
|
|
259
|
+
等待方可捕获本异常获取原因;上层 ``wait_reply`` 将其转换为返回 None。
|
|
260
|
+
|
|
261
|
+
:attribute reason: 取消原因(conflict / owner_unload / platform_stop / revoked / cancelled / cleared)
|
|
262
|
+
:attribute wait_key: 关联的会话键
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
def __init__(self, reason: str, wait_key: str = ""):
|
|
266
|
+
self.reason = reason
|
|
267
|
+
self.wait_key = wait_key
|
|
268
|
+
super().__init__(f"{reason}: {wait_key}" if wait_key else reason)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class SessionOccupiedError(InteractionError):
|
|
272
|
+
"""
|
|
273
|
+
会话已被其他模块占用
|
|
274
|
+
|
|
275
|
+
:meth:`InteractionManager.hold` 获取互斥租约失败时抛出。
|
|
276
|
+
|
|
277
|
+
:attribute wait_key: 会话键
|
|
278
|
+
:attribute owner: 当前占用者(模块名 / 平台名,未知时为 None)
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
def __init__(
|
|
282
|
+
self,
|
|
283
|
+
wait_key: str = "",
|
|
284
|
+
owner: str | None = None,
|
|
285
|
+
message: str = "",
|
|
286
|
+
):
|
|
287
|
+
self.wait_key = wait_key
|
|
288
|
+
self.owner = owner
|
|
289
|
+
super().__init__(
|
|
290
|
+
message or f"session occupied by {owner!r}: {wait_key}"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class StrictModeError(ErisPulseError):
|
|
295
|
+
"""
|
|
296
|
+
严格模式致命错误
|
|
297
|
+
|
|
298
|
+
当严格模式级别为 2(致命)且检测到违规时,在检查点抛出此异常,
|
|
299
|
+
用于中止整个启动流程。
|
|
300
|
+
|
|
301
|
+
{!--< tips >!--}
|
|
302
|
+
此异常不应被加载器捕获吞掉,应向上传播至初始化协调器
|
|
303
|
+
{!--< /tips >!--}
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
def __init__(self, message: str, violations: list | None = None):
|
|
307
|
+
super().__init__(message)
|
|
308
|
+
self.violations: list = violations if violations is not None else []
|
|
309
|
+
|
|
310
|
+
|
|
207
311
|
__all__ = [
|
|
208
312
|
"ClientConnectionError",
|
|
209
313
|
"ClientError",
|
|
210
314
|
"ClientTimeoutError",
|
|
211
315
|
"ErisPulseError",
|
|
212
316
|
"HTTPStatusError",
|
|
317
|
+
"InteractionCancelled",
|
|
213
318
|
"InteractionError",
|
|
214
319
|
"ModuleCallError",
|
|
215
320
|
"ModuleCallTimeoutError",
|
|
216
321
|
"ModuleError",
|
|
217
322
|
"ModuleNotAvailableError",
|
|
218
323
|
"ServiceNotProvidedError",
|
|
324
|
+
"SessionOccupiedError",
|
|
325
|
+
"StrictModeError",
|
|
219
326
|
"WebSocketDisconnect",
|
|
220
327
|
"WebSocketError",
|
|
221
328
|
]
|
|
@@ -22,11 +22,13 @@ ErisPulse KV 查询构建器
|
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
24
|
import json
|
|
25
|
-
from typing import Any
|
|
25
|
+
from typing import Any, TypeVar
|
|
26
26
|
|
|
27
27
|
from ..i18n import i18n
|
|
28
28
|
from .storage import BaseQueryBuilder
|
|
29
29
|
|
|
30
|
+
T = TypeVar("T")
|
|
31
|
+
|
|
30
32
|
_TABLE_PREFIX = "__erispulse_sql__"
|
|
31
33
|
|
|
32
34
|
|
|
@@ -200,18 +202,22 @@ class KVQueryBuilder(BaseQueryBuilder):
|
|
|
200
202
|
return self._exec_delete()
|
|
201
203
|
raise ValueError(i18n.t("core.kvbuilder.unknown_operation", op=self._operation))
|
|
202
204
|
|
|
205
|
+
def _require_data(self, expected: type[T], key: str) -> T:
|
|
206
|
+
"""{!--< internal-use >!--} 校验构建器数据形态并返回(同步/异步执行路径共用,兼作类型收窄)"""
|
|
207
|
+
if not isinstance(self._data, expected):
|
|
208
|
+
raise ValueError(i18n.t(key))
|
|
209
|
+
return self._data
|
|
210
|
+
|
|
203
211
|
def _exec_insert(self) -> int:
|
|
204
|
-
|
|
205
|
-
raise ValueError(i18n.t("core.kvbuilder.insert_needs_dict"))
|
|
212
|
+
data = self._require_data(dict, "core.kvbuilder.insert_needs_dict")
|
|
206
213
|
row_id = self._get_next_id()
|
|
207
|
-
self._storage.set(self._row_key(row_id), json.dumps(
|
|
214
|
+
self._storage.set(self._row_key(row_id), json.dumps(data, ensure_ascii=False))
|
|
208
215
|
return 1
|
|
209
216
|
|
|
210
217
|
def _exec_insert_multi(self) -> int:
|
|
211
|
-
|
|
212
|
-
raise ValueError(i18n.t("core.kvbuilder.insert_multi_needs_list"))
|
|
218
|
+
rows = self._require_data(list, "core.kvbuilder.insert_multi_needs_list")
|
|
213
219
|
count = 0
|
|
214
|
-
for row in
|
|
220
|
+
for row in rows:
|
|
215
221
|
row_id = self._get_next_id()
|
|
216
222
|
self._storage.set(self._row_key(row_id), json.dumps(row, ensure_ascii=False))
|
|
217
223
|
count += 1
|
|
@@ -245,12 +251,11 @@ class KVQueryBuilder(BaseQueryBuilder):
|
|
|
245
251
|
return [tuple(r.values()) for _, r in rows]
|
|
246
252
|
|
|
247
253
|
def _exec_update(self) -> int:
|
|
248
|
-
|
|
249
|
-
raise ValueError(i18n.t("core.kvbuilder.update_needs_dict"))
|
|
254
|
+
data = self._require_data(dict, "core.kvbuilder.update_needs_dict")
|
|
250
255
|
count = 0
|
|
251
256
|
for row_id, row in self._scan_rows():
|
|
252
257
|
if self._match_row(row):
|
|
253
|
-
row.update(
|
|
258
|
+
row.update(data)
|
|
254
259
|
self._storage.set(self._row_key(row_id), json.dumps(row, ensure_ascii=False))
|
|
255
260
|
count += 1
|
|
256
261
|
return count
|
|
@@ -308,16 +313,14 @@ class KVQueryBuilder(BaseQueryBuilder):
|
|
|
308
313
|
async def aExecute(self) -> list[tuple] | int:
|
|
309
314
|
"""异步执行查询"""
|
|
310
315
|
if self._operation == "insert":
|
|
311
|
-
|
|
312
|
-
raise ValueError(i18n.t("core.kvbuilder.insert_needs_dict"))
|
|
316
|
+
data = self._require_data(dict, "core.kvbuilder.insert_needs_dict")
|
|
313
317
|
row_id = self._get_next_id()
|
|
314
|
-
await self._storage.aset(self._row_key(row_id), json.dumps(
|
|
318
|
+
await self._storage.aset(self._row_key(row_id), json.dumps(data, ensure_ascii=False))
|
|
315
319
|
return 1
|
|
316
320
|
if self._operation == "insert_multi":
|
|
317
|
-
|
|
318
|
-
raise ValueError(i18n.t("core.kvbuilder.insert_multi_needs_list"))
|
|
321
|
+
rows = self._require_data(list, "core.kvbuilder.insert_multi_needs_list")
|
|
319
322
|
count = 0
|
|
320
|
-
for row in
|
|
323
|
+
for row in rows:
|
|
321
324
|
row_id = self._get_next_id()
|
|
322
325
|
await self._storage.aset(self._row_key(row_id), json.dumps(row, ensure_ascii=False))
|
|
323
326
|
count += 1
|
|
@@ -325,12 +328,11 @@ class KVQueryBuilder(BaseQueryBuilder):
|
|
|
325
328
|
if self._operation == "select":
|
|
326
329
|
return await self._aexec_select()
|
|
327
330
|
if self._operation == "update":
|
|
328
|
-
|
|
329
|
-
raise ValueError(i18n.t("core.kvbuilder.update_needs_dict"))
|
|
331
|
+
data = self._require_data(dict, "core.kvbuilder.update_needs_dict")
|
|
330
332
|
count = 0
|
|
331
333
|
for row_id, row in await self._ascan_rows():
|
|
332
334
|
if self._match_row(row):
|
|
333
|
-
row.update(
|
|
335
|
+
row.update(data)
|
|
334
336
|
await self._storage.aset(self._row_key(row_id), json.dumps(row, ensure_ascii=False))
|
|
335
337
|
count += 1
|
|
336
338
|
return count
|
|
@@ -51,6 +51,7 @@ class ModuleMeta:
|
|
|
51
51
|
tags: list[str] = field(default_factory=list)
|
|
52
52
|
commands: list[str] | None = None
|
|
53
53
|
services: "list[str | dict[str, Any]] | None" = None
|
|
54
|
+
min_sdk_version: str | None = None
|
|
54
55
|
|
|
55
56
|
def to_dict(self) -> dict[str, Any]:
|
|
56
57
|
"""
|
|
@@ -112,6 +113,8 @@ class BaseModule(ABC):
|
|
|
112
113
|
- 简单形态:``["get_history", "translate"]``(方法名,介绍自动取 docstring 首行)
|
|
113
114
|
- 完整形态:``[{"name": "get_history", "description": "查询会话历史"}]``
|
|
114
115
|
(``description`` 支持纯字符串或 i18n 字典,声明后覆盖 docstring)
|
|
116
|
+
- ``min_sdk_version``: 运行时所需最低 SDK 版本(如 ``"2.8.1"``;
|
|
117
|
+
SDK 低于该版本时框架在加载期明确报错并跳过本模块)
|
|
115
118
|
|
|
116
119
|
**i18n 支持**:字段值可为纯字符串,或 i18n 字典
|
|
117
120
|
``{"i18n": "key.path", "default": "兜底文本"}``(与配置 description 约定一致)。
|
|
@@ -26,12 +26,16 @@ from typing import Any
|
|
|
26
26
|
|
|
27
27
|
from ..constants import (
|
|
28
28
|
DEFAULT_KV_TABLE_NAME,
|
|
29
|
+
EVENT_STORAGE_READY,
|
|
30
|
+
EVENT_STORAGE_RECOVERED,
|
|
31
|
+
EVENT_STORAGE_UNREACHABLE,
|
|
29
32
|
STORAGE_MAX_LIST_INDEX,
|
|
30
33
|
STORAGE_POOL_CREATE_BACKOFF_SECS,
|
|
31
34
|
STORAGE_POOL_CREATE_RETRIES,
|
|
32
35
|
STORAGE_POOL_FAIL_COOLDOWN_SECS,
|
|
33
36
|
)
|
|
34
37
|
from ..i18n import i18n
|
|
38
|
+
from ..lifecycle import lifecycle
|
|
35
39
|
from ..logger import logger
|
|
36
40
|
from .errors import StorageUnreachableError
|
|
37
41
|
from .storage import _SENTINEL, BaseQueryBuilder, BaseStorage, _current_txn
|
|
@@ -694,9 +698,14 @@ class SQLStorageBase(BaseStorage):
|
|
|
694
698
|
|
|
695
699
|
# 冷却期内快速失败:连接不可达时不阻塞调用方(框架继续运行,仅存储暂不可用)
|
|
696
700
|
failed_until = getattr(self, "_resource_failed_until", 0.0)
|
|
701
|
+
was_cooling = failed_until > 0
|
|
697
702
|
if failed_until > time.monotonic():
|
|
703
|
+
logger.trace(
|
|
704
|
+
i18n.t("core.storage.cooldown_fast_fail", backend=self.dialect.name)
|
|
705
|
+
)
|
|
698
706
|
raise StorageUnreachableError(
|
|
699
|
-
|
|
707
|
+
i18n.t("core.storage.cooldown", backend=self.dialect.name),
|
|
708
|
+
backend=self.dialect.name,
|
|
700
709
|
)
|
|
701
710
|
|
|
702
711
|
# 指数退避重试,规避远端瞬时拒绝(连接数限流、数据库重启窗口等)
|
|
@@ -732,8 +741,16 @@ class SQLStorageBase(BaseStorage):
|
|
|
732
741
|
error=last_error,
|
|
733
742
|
)
|
|
734
743
|
)
|
|
744
|
+
await self._emit_storage_event(
|
|
745
|
+
EVENT_STORAGE_UNREACHABLE,
|
|
746
|
+
backend=self.dialect.name,
|
|
747
|
+
error=str(last_error),
|
|
748
|
+
cooldown=self._RESOURCE_FAIL_COOLDOWN_SECS,
|
|
749
|
+
)
|
|
735
750
|
raise StorageUnreachableError(
|
|
736
|
-
f"{self.dialect.name}: {last_error}"
|
|
751
|
+
f"{self.dialect.name}: {last_error}",
|
|
752
|
+
backend=self.dialect.name,
|
|
753
|
+
cooldown=self._RESOURCE_FAIL_COOLDOWN_SECS,
|
|
737
754
|
) from last_error
|
|
738
755
|
|
|
739
756
|
with self._resources_lock:
|
|
@@ -744,8 +761,27 @@ class SQLStorageBase(BaseStorage):
|
|
|
744
761
|
self._loop_resources[loop] = resource
|
|
745
762
|
# 建池成功:清除失败冷却(若曾进入冷却,此番重连试探成功即恢复)
|
|
746
763
|
self._resource_failed_until = 0.0
|
|
764
|
+
logger.debug(i18n.t("core.storage.pool_ready", backend=self.dialect.name))
|
|
765
|
+
await self._emit_storage_event(EVENT_STORAGE_READY, backend=self.dialect.name)
|
|
766
|
+
if was_cooling:
|
|
767
|
+
logger.info(i18n.t("core.storage.recovered", backend=self.dialect.name))
|
|
768
|
+
await self._emit_storage_event(EVENT_STORAGE_RECOVERED, backend=self.dialect.name)
|
|
747
769
|
return resource
|
|
748
770
|
|
|
771
|
+
async def _emit_storage_event(self, event: str, **data: Any) -> None:
|
|
772
|
+
"""
|
|
773
|
+
{!--< internal-use >!--}
|
|
774
|
+
发出存储生命周期事件(``storage.ready`` / ``storage.unreachable`` /
|
|
775
|
+
``storage.recovered``),供外部感知连接状态变化(如 Dashboard 告警)。
|
|
776
|
+
|
|
777
|
+
后台发射(fire):存储操作绝不等待观测者;lifecycle 未就绪或
|
|
778
|
+
处理异常时静默跳过,不影响存储操作本身。
|
|
779
|
+
"""
|
|
780
|
+
try:
|
|
781
|
+
lifecycle.fire(event, data)
|
|
782
|
+
except Exception:
|
|
783
|
+
pass
|
|
784
|
+
|
|
749
785
|
@asynccontextmanager
|
|
750
786
|
async def _acquire(self) -> AsyncIterator[Any]:
|
|
751
787
|
"""
|