ErisPulse 2.8.0.dev1__tar.gz → 2.8.0.dev2__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.dev1 → erispulse-2.8.0.dev2}/PKG-INFO +7 -4
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/README.pypi.md +0 -2
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/pyproject.toml +11 -3
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/create.py +23 -2
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/init.py +4 -116
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/utils/config_wizard.py +26 -4
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/utils/scaffold_text.py +10 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/__init__.py +24 -1
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/adapter.py +84 -8
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/config_schema.py +336 -79
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/errors.py +84 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/kv_builder.py +7 -3
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/module.py +10 -5
- erispulse-2.8.0.dev2/src/ErisPulse/Core/Bases/sql_base.py +1576 -0
- erispulse-2.8.0.dev2/src/ErisPulse/Core/Bases/storage.py +1171 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Event/__init__.py +17 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Event/base.py +145 -106
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Event/command.py +57 -82
- erispulse-2.8.0.dev2/src/ErisPulse/Core/Event/interaction.py +941 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Event/overrides.py +99 -7
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Event/wrapper.py +585 -24
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/__init__.py +28 -1
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/adapter.py +139 -2
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/config.py +200 -43
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/constants.py +119 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/i18n/locales/en.py +56 -3
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/i18n/locales/ja.py +56 -3
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/i18n/locales/ru.py +56 -3
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/i18n/locales/zh_cn.py +56 -3
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/i18n/locales/zh_tw.py +56 -3
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/lifecycle.py +71 -12
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/module.py +467 -1
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/scope.py +139 -8
- erispulse-2.8.0.dev2/src/ErisPulse/Core/storage/__init__.py +89 -0
- erispulse-2.8.0.dev2/src/ErisPulse/Core/storage/mysql.py +279 -0
- erispulse-2.8.0.dev2/src/ErisPulse/Core/storage/postgres.py +282 -0
- erispulse-2.8.0.dev2/src/ErisPulse/Core/storage/sqlite.py +261 -0
- erispulse-2.8.0.dev2/src/ErisPulse/Core/transcript.py +375 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/runtime/__init__.py +13 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/runtime/context.py +63 -0
- erispulse-2.8.0.dev2/src/ErisPulse/runtime/example_config.py +266 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/runtime/frame_config.py +68 -19
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/sdk.py +37 -2
- erispulse-2.8.0.dev1/src/ErisPulse/Core/Bases/storage.py +0 -534
- erispulse-2.8.0.dev1/src/ErisPulse/Core/storage.py +0 -1520
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/.gitignore +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/LICENSE +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/__init__.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/base.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/cli.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/__init__.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/config.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/doctor.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/install.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/language.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/list.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/list_remote.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/run.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/self_update.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/types.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/uninstall.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/upgrade.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/console.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/constants.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/hints.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/i18n/__init__.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/i18n/locales/__init__.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/i18n/locales/en.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/i18n/locales/ja.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/i18n/locales/ru.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/i18n/locales/zh_cn.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/i18n/locales/zh_tw.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/registry.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/utils/__init__.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/utils/display.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/utils/file_watcher.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/utils/package_manager.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/client.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/converter.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/i18n_schema.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/manager.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/router.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/send_builder.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/send_rules.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/websocket.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Event/message.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Event/message_builder.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Event/meta.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Event/notice.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Event/request.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Event/session_type.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/assets/__init__.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/assets/error.css +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/assets/error.html +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/assets/root.css +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/assets/root.html +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/client.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/i18n/__init__.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/i18n/constants.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/i18n/locales/__init__.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/logger.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/master.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/router.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/text_match.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/__init__.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/__main__.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/finders/__init__.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/finders/adapter.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/finders/bases/__init__.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/finders/bases/finder.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/finders/module.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/loaders/__init__.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/loaders/adapter.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/loaders/bases/__init__.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/loaders/bases/loader.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/loaders/module.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/loaders/plugin_folder.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/loaders/strategy.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/loaders/strict.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/py.typed +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/runtime/config_schema.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/runtime/diagnostics.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/runtime/exceptions.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/runtime/hints.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/runtime/memory.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/runtime/plugin_reload.py +0 -0
- {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/runtime/tasks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ErisPulse
|
|
3
|
-
Version: 2.8.0.
|
|
3
|
+
Version: 2.8.0.dev2
|
|
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
|
|
@@ -29,11 +29,16 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
29
29
|
Classifier: Typing :: Typed
|
|
30
30
|
Requires-Python: >=3.10
|
|
31
31
|
Requires-Dist: aiohttp>=3.9.0
|
|
32
|
+
Requires-Dist: aiosqlite>=0.20.0
|
|
32
33
|
Requires-Dist: fastapi>=0.116.1
|
|
33
34
|
Requires-Dist: rich>=13.0.0
|
|
34
|
-
Requires-Dist:
|
|
35
|
+
Requires-Dist: tomlkit>=0.15.1
|
|
35
36
|
Requires-Dist: uvicorn>=0.30.0
|
|
36
37
|
Requires-Dist: websockets>=12.0
|
|
38
|
+
Provides-Extra: mysql
|
|
39
|
+
Requires-Dist: aiomysql>=0.2.0; extra == 'mysql'
|
|
40
|
+
Provides-Extra: postgres
|
|
41
|
+
Requires-Dist: asyncpg>=0.29.0; extra == 'postgres'
|
|
37
42
|
Provides-Extra: test
|
|
38
43
|
Requires-Dist: httpx>=0.24.0; extra == 'test'
|
|
39
44
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'test'
|
|
@@ -44,8 +49,6 @@ Requires-Dist: pytest-xdist>=3.0.0; extra == 'test'
|
|
|
44
49
|
Requires-Dist: pytest>=7.0.0; extra == 'test'
|
|
45
50
|
Description-Content-Type: text/markdown
|
|
46
51
|
|
|
47
|
-

|
|
48
|
-
|
|
49
52
|
# ErisPulse
|
|
50
53
|
|
|
51
54
|
**Write once, deploy on multiple platforms.**
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ErisPulse"
|
|
7
|
-
version = "2.8.0-dev.
|
|
7
|
+
version = "2.8.0-dev.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"
|
|
@@ -61,13 +61,14 @@ classifiers = [
|
|
|
61
61
|
|
|
62
62
|
dependencies = [
|
|
63
63
|
"rich>=13.0.0",
|
|
64
|
-
"
|
|
65
|
-
|
|
64
|
+
"tomlkit>=0.15.1",
|
|
66
65
|
# network 相关
|
|
67
66
|
"websockets>=12.0",
|
|
68
67
|
"aiohttp>=3.9.0",
|
|
69
68
|
"fastapi>=0.116.1",
|
|
70
69
|
"uvicorn>=0.30.0",
|
|
70
|
+
# storage 相关(默认 SQLite 后端异步驱动,开箱即用)
|
|
71
|
+
"aiosqlite>=0.20.0",
|
|
71
72
|
]
|
|
72
73
|
|
|
73
74
|
[project.urls]
|
|
@@ -79,6 +80,13 @@ Issues = "https://github.com/ErisPulse/ErisPulse/issues"
|
|
|
79
80
|
Changelog = "https://github.com/ErisPulse/ErisPulse/blob/main/CHANGELOG.md"
|
|
80
81
|
|
|
81
82
|
[project.optional-dependencies]
|
|
83
|
+
# 存储后端可选驱动:pip install ErisPulse[mysql] / ErisPulse[postgres]
|
|
84
|
+
mysql = [
|
|
85
|
+
"aiomysql>=0.2.0",
|
|
86
|
+
]
|
|
87
|
+
postgres = [
|
|
88
|
+
"asyncpg>=0.29.0",
|
|
89
|
+
]
|
|
82
90
|
test = [
|
|
83
91
|
"pytest>=7.0.0",
|
|
84
92
|
"pytest-cov>=4.0.0",
|
|
@@ -86,7 +86,11 @@ class Main(BaseModule):
|
|
|
86
86
|
# {text[module.config_hint]}
|
|
87
87
|
@dataclass
|
|
88
88
|
class ConfigClass(BaseConfig):
|
|
89
|
-
\"\"\"
|
|
89
|
+
\"\"\"
|
|
90
|
+
{text[module.config_doc]}
|
|
91
|
+
|
|
92
|
+
:ivar advanced_threshold: 高级阈值(docstring :ivar: 声明即为字段描述,无需 metadata)
|
|
93
|
+
\"\"\"
|
|
90
94
|
|
|
91
95
|
enabled: bool = field(
|
|
92
96
|
default=True,
|
|
@@ -94,6 +98,11 @@ class Main(BaseModule):
|
|
|
94
98
|
\"description\": {{\"i18n\": \"module.{name}.enabled\", \"default\": \"Enable module\"}},
|
|
95
99
|
}},
|
|
96
100
|
)
|
|
101
|
+
# example 字段:默认不写入 config.toml(仅记录在 config.full.example,用户按需启用)
|
|
102
|
+
advanced_threshold: int = field(
|
|
103
|
+
default=100,
|
|
104
|
+
metadata={{\"example\": True, \"min\": 0}},
|
|
105
|
+
)
|
|
97
106
|
|
|
98
107
|
# {text[module.i18n_hint]}
|
|
99
108
|
class I18nClass(BaseI18n):
|
|
@@ -159,6 +168,9 @@ class Main(BaseModule):
|
|
|
159
168
|
author=\"ErisDev\",
|
|
160
169
|
group=\"default\",
|
|
161
170
|
tags=[\"{name}\"],
|
|
171
|
+
# 对外服务白名单(可选):声明后其他模块可经 sdk.module.call() 调用这些方法;
|
|
172
|
+
# 缺省时公开方法全开放(限制由用户通过 scope.actions 配置)
|
|
173
|
+
# services=[\"get_welcome_message\"],
|
|
162
174
|
)
|
|
163
175
|
|
|
164
176
|
@staticmethod
|
|
@@ -288,7 +300,11 @@ class {name}(BaseAdapter):
|
|
|
288
300
|
# {text[adapter.config_hint]}
|
|
289
301
|
@dataclass
|
|
290
302
|
class ConfigClass(BaseConfig):
|
|
291
|
-
\"\"\"
|
|
303
|
+
\"\"\"
|
|
304
|
+
{text[adapter.config_doc]}
|
|
305
|
+
|
|
306
|
+
:ivar request_timeout: 请求超时秒数(docstring :ivar: 声明即为字段描述,无需 metadata)
|
|
307
|
+
\"\"\"
|
|
292
308
|
|
|
293
309
|
endpoint: str = field(
|
|
294
310
|
default="https://api.example.com",
|
|
@@ -307,6 +323,11 @@ class {name}(BaseAdapter):
|
|
|
307
323
|
"ui": {{"widget": "password", "group": "basic", "order": 2}},
|
|
308
324
|
}},
|
|
309
325
|
)
|
|
326
|
+
# example 字段:默认不写入 config.toml(仅记录在 config.full.example,用户按需启用)
|
|
327
|
+
request_timeout: int = field(
|
|
328
|
+
default=30,
|
|
329
|
+
metadata={{"example": True, "min": 1, "max": 300}},
|
|
330
|
+
)
|
|
310
331
|
|
|
311
332
|
# {text[adapter.i18n_hint]}
|
|
312
333
|
class I18nClass(BaseI18n):
|
|
@@ -175,128 +175,16 @@ class InitCommand(Command):
|
|
|
175
175
|
"""
|
|
176
176
|
生成完整的配置示例文本
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
|
|
178
|
+
生成逻辑收归 ``ErisPulse.runtime.example_config``(框架运行与 CLI 共用:
|
|
179
|
+
含自维护首行标记、静态框架段与已安装组件声明式段)。此处仅保留调用入口。
|
|
180
180
|
|
|
181
181
|
:param adapter_list: [list] 适配器名称列表 (默认: None)
|
|
182
182
|
:return: [str] 完整配置示例字符串
|
|
183
183
|
"""
|
|
184
|
-
from
|
|
185
|
-
|
|
186
|
-
st = ScaffoldText()
|
|
187
|
-
lines = [
|
|
188
|
-
st.t("cfg.header.title"),
|
|
189
|
-
st.t("cfg.header.desc"),
|
|
190
|
-
st.t("cfg.header.usage"),
|
|
191
|
-
"",
|
|
192
|
-
st.t("cfg.section.server"),
|
|
193
|
-
"",
|
|
194
|
-
"[ErisPulse.server]",
|
|
195
|
-
f'host = "0.0.0.0" # {st.t("cfg.server.host")}',
|
|
196
|
-
f"port = 8000 # {st.t('cfg.server.port')}",
|
|
197
|
-
f"auto_start = true # {st.t('cfg.server.auto_start')}",
|
|
198
|
-
f'ssl_certfile = "config/ssl/cert.pem" # {st.t("cfg.server.ssl_certfile")}',
|
|
199
|
-
f'ssl_keyfile = "config/ssl/key.pem" # {st.t("cfg.server.ssl_keyfile")}',
|
|
200
|
-
st.t("cfg.server.ssl_inline_hint"),
|
|
201
|
-
'# ssl_cert = """-----BEGIN CERTIFICATE-----',
|
|
202
|
-
"# ...",
|
|
203
|
-
'# -----END CERTIFICATE-----"""',
|
|
204
|
-
'# ssl_key = """-----BEGIN PRIVATE KEY-----',
|
|
205
|
-
"# ...",
|
|
206
|
-
'# -----END PRIVATE KEY-----"""',
|
|
207
|
-
"",
|
|
208
|
-
st.t("cfg.section.logger"),
|
|
209
|
-
"",
|
|
210
|
-
"[ErisPulse.logger]",
|
|
211
|
-
f'level = "INFO" # {st.t("cfg.logger.level")}',
|
|
212
|
-
f"log_files = [] # {st.t('cfg.logger.log_files')}",
|
|
213
|
-
f'log_dir = "" # {st.t("cfg.logger.log_dir")}',
|
|
214
|
-
f'log_rotation = "size" # {st.t("cfg.logger.log_rotation")}',
|
|
215
|
-
f"log_max_size_mb = 10 # {st.t('cfg.logger.log_max_size_mb')}",
|
|
216
|
-
f"log_backup_count = 5 # {st.t('cfg.logger.log_backup_count')}",
|
|
217
|
-
f'log_rotation_when = "midnight" # {st.t("cfg.logger.log_rotation_when")}',
|
|
218
|
-
f"memory_limit = 1000 # {st.t('cfg.logger.memory_limit')}",
|
|
219
|
-
"",
|
|
220
|
-
st.t("cfg.section.storage"),
|
|
221
|
-
"",
|
|
222
|
-
"[ErisPulse.storage]",
|
|
223
|
-
f"use_global_db = false # {st.t('cfg.storage.use_global_db')}",
|
|
224
|
-
"",
|
|
225
|
-
st.t("cfg.section.event"),
|
|
226
|
-
"",
|
|
227
|
-
"[ErisPulse.event.message]",
|
|
228
|
-
f"ignore_self = true # {st.t('cfg.event.ignore_self')}",
|
|
229
|
-
"",
|
|
230
|
-
"[ErisPulse.event.command]",
|
|
231
|
-
f'prefix = "/" # {st.t("cfg.command.prefix")}',
|
|
232
|
-
f"case_sensitive = true # {st.t('cfg.command.case_sensitive')}",
|
|
233
|
-
f"allow_space_prefix = false # {st.t('cfg.command.allow_space_prefix')}",
|
|
234
|
-
f"must_at_bot = false # {st.t('cfg.command.must_at_bot')}",
|
|
235
|
-
"",
|
|
236
|
-
st.t("cfg.section.framework"),
|
|
237
|
-
"",
|
|
238
|
-
"[ErisPulse.framework]",
|
|
239
|
-
f"enable_lazy_loading = true # {st.t('cfg.framework.enable_lazy_loading')}",
|
|
240
|
-
f'plugins_dir = "plugins" # {st.t("cfg.framework.plugins_dir")}',
|
|
241
|
-
f"uninit_timeout = 30 # {st.t('cfg.framework.uninit_timeout')}",
|
|
242
|
-
f" {st.t('cfg.framework.uninit_timeout_line1')}",
|
|
243
|
-
f" {st.t('cfg.framework.uninit_timeout_line2')}",
|
|
244
|
-
f"strict_mode = false # {st.t('cfg.framework.strict_mode')}",
|
|
245
|
-
f"strict_mode_exceptions = {{ modules = [], adapters = [] }} # {st.t('cfg.framework.strict_mode_exceptions')}",
|
|
246
|
-
f"handler_max_concurrency = 64 # {st.t('cfg.framework.handler_max_concurrency')}",
|
|
247
|
-
f"proactive_gc_interval = 300 # {st.t('cfg.framework.proactive_gc_interval')}",
|
|
248
|
-
f"proactive_gc_generation = 2 # {st.t('cfg.framework.proactive_gc_generation')}",
|
|
249
|
-
f"proactive_gc_full_every = 10 # {st.t('cfg.framework.proactive_gc_full_every')}",
|
|
250
|
-
f"proactive_gc_memory_growth_mb = 100 # {st.t('cfg.framework.proactive_gc_memory_growth_mb')}",
|
|
251
|
-
f"proactive_gc_idle_only = true # {st.t('cfg.framework.proactive_gc_idle_only')}",
|
|
252
|
-
f"proactive_gc_gen0_min = 100 # {st.t('cfg.framework.proactive_gc_gen0_min')}",
|
|
253
|
-
f"offline_bot_expiry = 3600 # {st.t('cfg.framework.offline_bot_expiry')}",
|
|
254
|
-
"",
|
|
255
|
-
st.t("cfg.section.router"),
|
|
256
|
-
"",
|
|
257
|
-
"[ErisPulse.router.cors]",
|
|
258
|
-
"enabled = false",
|
|
259
|
-
'allow_origins = ["*"]',
|
|
260
|
-
'allow_methods = ["*"]',
|
|
261
|
-
'allow_headers = ["*"]',
|
|
262
|
-
"allow_credentials = false",
|
|
263
|
-
"max_age = 600",
|
|
264
|
-
"",
|
|
265
|
-
"[ErisPulse.router.security]",
|
|
266
|
-
"enabled = false",
|
|
267
|
-
"",
|
|
268
|
-
"[ErisPulse.router.security.headers]",
|
|
269
|
-
'X-Content-Type-Options = "nosniff"',
|
|
270
|
-
'X-Frame-Options = "DENY"',
|
|
271
|
-
"",
|
|
272
|
-
st.t("cfg.section.adapter_status"),
|
|
273
|
-
"",
|
|
274
|
-
"[ErisPulse.adapters.status]",
|
|
275
|
-
]
|
|
276
|
-
|
|
277
|
-
if adapter_list:
|
|
278
|
-
lines.extend(f"# {adapter} = false" for adapter in adapter_list)
|
|
279
|
-
else:
|
|
280
|
-
lines.extend(
|
|
281
|
-
[
|
|
282
|
-
"# yunhu = false",
|
|
283
|
-
"# telegram = false",
|
|
284
|
-
"# onebot11 = false",
|
|
285
|
-
]
|
|
286
|
-
)
|
|
184
|
+
from ErisPulse.runtime.example_config import render_full_example
|
|
287
185
|
|
|
288
|
-
|
|
289
|
-
[
|
|
290
|
-
"",
|
|
291
|
-
st.t("cfg.section.module_status"),
|
|
292
|
-
"",
|
|
293
|
-
"[ErisPulse.modules.status]",
|
|
294
|
-
"# MyModule = true",
|
|
295
|
-
"",
|
|
296
|
-
]
|
|
297
|
-
)
|
|
186
|
+
return render_full_example(adapter_list=adapter_list)
|
|
298
187
|
|
|
299
|
-
return "\n".join(lines)
|
|
300
188
|
|
|
301
189
|
async def _fetch_available_adapters(self):
|
|
302
190
|
"""
|
|
@@ -524,6 +524,9 @@ def fill_config_fields(config_class, current_values: dict) -> dict:
|
|
|
524
524
|
"""
|
|
525
525
|
渲染整个配置类的表单并收集用户输入
|
|
526
526
|
|
|
527
|
+
``example`` 字段不进入向导(默认值不落盘,仅记录在 config.full.example);
|
|
528
|
+
嵌套 dataclass 字段(schema 含 ``fields`` 子树)递归渲染为子表单。
|
|
529
|
+
|
|
527
530
|
:param config_class: dataclass 配置类
|
|
528
531
|
:param current_values: 当前存储的配置字典(作为各字段初值)
|
|
529
532
|
:return: 收集后的配置字典
|
|
@@ -531,12 +534,31 @@ def fill_config_fields(config_class, current_values: dict) -> dict:
|
|
|
531
534
|
from ErisPulse.Core.Bases.config_schema import resolve_config_schema
|
|
532
535
|
|
|
533
536
|
schema = resolve_config_schema(config_class)
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
+
values = dict(current_values or {})
|
|
538
|
+
_fill_from_schema_fields(schema.get("fields", {}), values)
|
|
539
|
+
return values
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _fill_from_schema_fields(fields_dict: dict, values: dict) -> None:
|
|
543
|
+
"""
|
|
544
|
+
{!--< internal-use >!--}
|
|
545
|
+
按 schema 字段树渲染表单并就地收集到 values(嵌套子树递归)
|
|
546
|
+
"""
|
|
547
|
+
current_store = dict(values)
|
|
548
|
+
for name, field_schema in _sort_fields(fields_dict):
|
|
549
|
+
if field_schema.get("example") and name not in current_store:
|
|
550
|
+
# example 字段仅当用户已显式设置时保留原值,不主动询问
|
|
551
|
+
continue
|
|
552
|
+
nested_fields = field_schema.get("fields")
|
|
553
|
+
if isinstance(nested_fields, dict):
|
|
554
|
+
sub_values = current_store.get(name)
|
|
555
|
+
if not isinstance(sub_values, dict):
|
|
556
|
+
sub_values = {}
|
|
557
|
+
_fill_from_schema_fields(nested_fields, sub_values)
|
|
558
|
+
values[name] = sub_values
|
|
559
|
+
continue
|
|
537
560
|
has_value = name in current_store
|
|
538
561
|
values[name] = _prompt_field(name, field_schema, values.get(name), has_value)
|
|
539
|
-
return values
|
|
540
562
|
|
|
541
563
|
|
|
542
564
|
def _validate_dataclass(config_class, data: dict) -> list[str]:
|
|
@@ -96,6 +96,8 @@ _EN_FALLBACK: dict[str, str] = {
|
|
|
96
96
|
"cfg.section.router": "# ==================== Router ====================",
|
|
97
97
|
"cfg.section.adapter_status": "# ==================== Adapter Status ====================",
|
|
98
98
|
"cfg.section.module_status": "# ==================== Module Status ====================",
|
|
99
|
+
"cfg.section.components": "# ==================== Installed Components (declarative, auto-generated) ====================",
|
|
100
|
+
"cfg.section.components_hint": "# Rendered from each installed adapter/module ConfigClass; example: fields only live here (not auto-persisted to config.toml)",
|
|
99
101
|
"cfg.server.host": "Listen address",
|
|
100
102
|
"cfg.server.port": "Listen port",
|
|
101
103
|
"cfg.server.auto_start": "Auto start HTTP server (set false for pure WS/polling adapters)",
|
|
@@ -112,6 +114,9 @@ _EN_FALLBACK: dict[str, str] = {
|
|
|
112
114
|
"cfg.logger.log_rotation_when": "date mode: rotation period S/M/H/D/midnight",
|
|
113
115
|
"cfg.logger.memory_limit": "In-memory log entries limit",
|
|
114
116
|
"cfg.storage.use_global_db": "Use global database",
|
|
117
|
+
"cfg.storage.backend": "Storage backend: \"sqlite\" / \"mysql\" / \"postgres\"",
|
|
118
|
+
"cfg.storage.mysql": "MySQL connection params (effective when backend = \"mysql\"), requires: pip install ErisPulse[mysql]",
|
|
119
|
+
"cfg.storage.postgres": "PostgreSQL connection params (effective when backend = \"postgres\"), requires: pip install ErisPulse[postgres]",
|
|
115
120
|
"cfg.event.ignore_self": "Ignore self messages",
|
|
116
121
|
"cfg.command.prefix": "Command prefix",
|
|
117
122
|
"cfg.command.case_sensitive": "Case sensitive",
|
|
@@ -196,6 +201,8 @@ _TRANSLATIONS: dict[str, dict[str, str]] = {
|
|
|
196
201
|
"cfg.section.router": "# ==================== 路由增强 ====================",
|
|
197
202
|
"cfg.section.adapter_status": "# ==================== 适配器状态 ====================",
|
|
198
203
|
"cfg.section.module_status": "# ==================== 模块状态 ====================",
|
|
204
|
+
"cfg.section.components": "# ==================== 已安装组件(声明式自动生成) ====================",
|
|
205
|
+
"cfg.section.components_hint": "# 由各已安装适配器/模块的 ConfigClass 渲染;example: 字段仅记录于此(不会自动写入 config.toml)",
|
|
199
206
|
"cfg.server.host": "监听地址",
|
|
200
207
|
"cfg.server.port": "监听端口",
|
|
201
208
|
"cfg.server.auto_start": "是否自动启动 HTTP 服务器 (纯 WS/轮询适配器可设 false)",
|
|
@@ -211,6 +218,9 @@ _TRANSLATIONS: dict[str, dict[str, str]] = {
|
|
|
211
218
|
"cfg.logger.log_rotation_when": "date 模式轮转周期: S/M/H/D/midnight",
|
|
212
219
|
"cfg.logger.memory_limit": "内存日志条数上限",
|
|
213
220
|
"cfg.storage.use_global_db": "是否使用全局数据库",
|
|
221
|
+
"cfg.storage.backend": "存储后端: \"sqlite\" / \"mysql\" / \"postgres\"",
|
|
222
|
+
"cfg.storage.mysql": "MySQL 连接参数(backend = \"mysql\" 时生效),需安装: pip install ErisPulse[mysql]",
|
|
223
|
+
"cfg.storage.postgres": "PostgreSQL 连接参数(backend = \"postgres\" 时生效),需安装: pip install ErisPulse[postgres]",
|
|
214
224
|
"cfg.event.ignore_self": "忽略自身消息",
|
|
215
225
|
"cfg.command.prefix": "命令前缀",
|
|
216
226
|
"cfg.command.case_sensitive": "区分大小写",
|
|
@@ -9,7 +9,8 @@ from .converter import BaseConverter
|
|
|
9
9
|
from .send_rules import SendContext
|
|
10
10
|
from .send_builder import SendBuilder, BatchContext
|
|
11
11
|
from .module import BaseModule, ModuleMeta
|
|
12
|
-
from .storage import BaseStorage, BaseQueryBuilder
|
|
12
|
+
from .storage import AsyncBridge, BaseStorage, BaseQueryBuilder
|
|
13
|
+
from .sql_base import AlterTableBuilder, SQLDialect, SQLQueryBuilder, SQLStorageBase
|
|
13
14
|
from .kv_builder import KVQueryBuilder
|
|
14
15
|
from .errors import (
|
|
15
16
|
ErisPulseError,
|
|
@@ -17,6 +18,14 @@ from .errors import (
|
|
|
17
18
|
ClientConnectionError,
|
|
18
19
|
ClientTimeoutError,
|
|
19
20
|
HTTPStatusError,
|
|
21
|
+
InteractionError,
|
|
22
|
+
ModuleCallError,
|
|
23
|
+
ModuleCallTimeoutError,
|
|
24
|
+
ModuleError,
|
|
25
|
+
ModuleNotAvailableError,
|
|
26
|
+
ServiceNotProvidedError,
|
|
27
|
+
StorageError,
|
|
28
|
+
StorageUnreachableError,
|
|
20
29
|
WebSocketError,
|
|
21
30
|
WebSocketDisconnect,
|
|
22
31
|
)
|
|
@@ -66,16 +75,30 @@ __all__ = [
|
|
|
66
75
|
"I18nConfig",
|
|
67
76
|
# i18n 单键声明
|
|
68
77
|
"I18nKey",
|
|
78
|
+
"AsyncBridge",
|
|
79
|
+
"InteractionError",
|
|
69
80
|
"KVQueryBuilder",
|
|
81
|
+
"ModuleCallError",
|
|
82
|
+
"ModuleCallTimeoutError",
|
|
83
|
+
"ModuleError",
|
|
70
84
|
"ModuleMeta",
|
|
85
|
+
"ModuleNotAvailableError",
|
|
71
86
|
"RequestDSL",
|
|
72
87
|
"SendBuilder",
|
|
73
88
|
"SendContext",
|
|
74
89
|
"SendDSL",
|
|
90
|
+
"ServiceNotProvidedError",
|
|
75
91
|
"SseEmitter",
|
|
92
|
+
"StorageError",
|
|
93
|
+
"StorageUnreachableError",
|
|
76
94
|
"WSMessage",
|
|
77
95
|
"WebSocketConnection",
|
|
78
96
|
"WebSocketConnectionBase",
|
|
79
97
|
"WebSocketDisconnect",
|
|
80
98
|
"WebSocketError",
|
|
99
|
+
# SQL 存储基类(自定义 SQL 后端扩展用)
|
|
100
|
+
"AlterTableBuilder",
|
|
101
|
+
"SQLDialect",
|
|
102
|
+
"SQLQueryBuilder",
|
|
103
|
+
"SQLStorageBase",
|
|
81
104
|
]
|
|
@@ -162,6 +162,37 @@ def _wrap_to_task(result: Any):
|
|
|
162
162
|
return asyncio.ensure_future(_const())
|
|
163
163
|
|
|
164
164
|
|
|
165
|
+
def _append_send_receipt(ledger: list, response: Any, ctx: dict) -> None:
|
|
166
|
+
"""
|
|
167
|
+
{!--< internal-use >!--}
|
|
168
|
+
从发送响应提取回执并记入消息事务账本
|
|
169
|
+
|
|
170
|
+
仅当响应为 dict 且含非空 ``message_id`` 时记录(标准 API 响应格式);
|
|
171
|
+
消息事务撤回时按账本逆序调用适配器 ``delete_message``。
|
|
172
|
+
|
|
173
|
+
:param ledger: 当前事务的账本列表(send_receipts ContextVar 值)
|
|
174
|
+
:param response: 发送方法返回的响应(标准格式为 dict)
|
|
175
|
+
:param ctx: 发送上下文(platform / bot_id / trace_id 等)
|
|
176
|
+
"""
|
|
177
|
+
if not isinstance(response, dict):
|
|
178
|
+
return
|
|
179
|
+
message_id = response.get("message_id")
|
|
180
|
+
if not message_id:
|
|
181
|
+
data = response.get("data")
|
|
182
|
+
if isinstance(data, dict):
|
|
183
|
+
message_id = data.get("message_id")
|
|
184
|
+
if not message_id:
|
|
185
|
+
return
|
|
186
|
+
ledger.append(
|
|
187
|
+
{
|
|
188
|
+
"platform": str(ctx.get("platform") or ""),
|
|
189
|
+
"bot_id": str(ctx.get("bot_id") or ""),
|
|
190
|
+
"message_id": str(message_id),
|
|
191
|
+
"trace_id": ctx.get("trace_id"),
|
|
192
|
+
}
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
165
196
|
def _wrap_send_method(method_name: str, original_method: Callable, send_dsl: "SendDSL"):
|
|
166
197
|
"""
|
|
167
198
|
为发送方法注入生命周期钩子
|
|
@@ -198,16 +229,50 @@ def _wrap_send_method(method_name: str, original_method: Callable, send_dsl: "Se
|
|
|
198
229
|
if isinstance(result, SendDSL):
|
|
199
230
|
return result
|
|
200
231
|
|
|
232
|
+
# 非 Task 同步返回值:直接记入消息事务账本后原样返回
|
|
201
233
|
if not isinstance(result, asyncio.Task):
|
|
234
|
+
from ...runtime.context import send_receipts as _sr
|
|
235
|
+
|
|
236
|
+
_sync_ledger = _sr.get()
|
|
237
|
+
if _sync_ledger is not None:
|
|
238
|
+
_append_send_receipt(
|
|
239
|
+
_sync_ledger,
|
|
240
|
+
result,
|
|
241
|
+
{
|
|
242
|
+
"platform": getattr(send_dsl._adapter, "_platform", "") or "",
|
|
243
|
+
"method": method_name,
|
|
244
|
+
"detail_type": send_dsl._target_type or "",
|
|
245
|
+
"target_id": send_dsl._target_id or "",
|
|
246
|
+
"bot_id": send_dsl._account_id or "",
|
|
247
|
+
},
|
|
248
|
+
)
|
|
202
249
|
return result
|
|
203
250
|
|
|
204
251
|
platform = getattr(send_dsl._adapter, "_platform", "") or ""
|
|
252
|
+
|
|
253
|
+
# 链路追踪:出站发送携带当前事件的 trace-id(不在事件上下文内时为 None)
|
|
254
|
+
from ...runtime.context import current_trace_id, send_receipts
|
|
255
|
+
|
|
256
|
+
_trace_id = current_trace_id.get()
|
|
257
|
+
# 消息事务账本:仅在 message_tx 事务内记账(None = 事务外零开销)
|
|
258
|
+
_ledger = send_receipts.get()
|
|
259
|
+
|
|
260
|
+
# 消息文本预览(截断):供收件箱(transcript)/ 审计类钩子使用
|
|
261
|
+
_preview = ""
|
|
262
|
+
if args:
|
|
263
|
+
_preview = str(args[0])
|
|
264
|
+
if len(_preview) > LOG_MESSAGE_TRUNCATE_CHARS:
|
|
265
|
+
_preview = _preview[:LOG_MESSAGE_TRUNCATE_CHARS] + "..."
|
|
266
|
+
|
|
205
267
|
send_ctx = {
|
|
206
268
|
"platform": platform,
|
|
207
269
|
"method": method_name,
|
|
208
270
|
"detail_type": send_dsl._target_type or "",
|
|
209
271
|
"target_id": send_dsl._target_id or "",
|
|
210
272
|
"bot_id": send_dsl._account_id or "",
|
|
273
|
+
"trace_id": _trace_id,
|
|
274
|
+
# 消息文本预览(截断),供收件箱 / 审计类钩子使用
|
|
275
|
+
"preview": _preview,
|
|
211
276
|
}
|
|
212
277
|
|
|
213
278
|
# 循环依赖:Core/adapter.py 顶层导入本模块(BaseAdapter)
|
|
@@ -216,13 +281,14 @@ def _wrap_send_method(method_name: str, original_method: Callable, send_dsl: "Se
|
|
|
216
281
|
target_type = send_dsl._target_type or ""
|
|
217
282
|
target_id = send_dsl._target_id or ""
|
|
218
283
|
log_target = f"{target_type}/{target_id}" if target_type and target_id else target_id or "?"
|
|
284
|
+
_trace_tag = f" [trace:{_trace_id}]" if _trace_id else ""
|
|
219
285
|
if method_name in ("Text", "Markdown", "Html") and args:
|
|
220
286
|
content = str(args[0])
|
|
221
287
|
if len(content) > LOG_MESSAGE_TRUNCATE_CHARS:
|
|
222
288
|
content = content[:LOG_MESSAGE_TRUNCATE_CHARS] + "..."
|
|
223
|
-
_msg_logger.event(f"[Send] {platform}/{method_name} -> {log_target}: {content}")
|
|
289
|
+
_msg_logger.event(f"[Send] {platform}/{method_name} -> {log_target}{_trace_tag}: {content}")
|
|
224
290
|
else:
|
|
225
|
-
_msg_logger.event(f"[Send] {platform}/{method_name} -> {log_target}")
|
|
291
|
+
_msg_logger.event(f"[Send] {platform}/{method_name} -> {log_target}{_trace_tag}")
|
|
226
292
|
|
|
227
293
|
# 预判是否有生命周期监听者:无监听时跳过 Task 创建与 emit 调度,
|
|
228
294
|
# 避免每条发送消息都无条件 spawn 两个后台任务
|
|
@@ -242,6 +308,15 @@ def _wrap_send_method(method_name: str, original_method: Callable, send_dsl: "Se
|
|
|
242
308
|
|
|
243
309
|
spawn_background(_emit_hooks())
|
|
244
310
|
|
|
311
|
+
# 消息事务:发送完成(含重试终态)后从响应提取 message_id 记入账本
|
|
312
|
+
if _ledger is not None:
|
|
313
|
+
|
|
314
|
+
def _record_receipt(t: "asyncio.Task") -> None:
|
|
315
|
+
try:
|
|
316
|
+
_append_send_receipt(_ledger, t.result(), send_ctx)
|
|
317
|
+
except Exception:
|
|
318
|
+
pass
|
|
319
|
+
|
|
245
320
|
# 若附加了发送规则,用规则执行器统一包装 Task
|
|
246
321
|
if _has_rules(send_dsl):
|
|
247
322
|
from .send_rules import apply_send_rules
|
|
@@ -275,11 +350,15 @@ def _wrap_send_method(method_name: str, original_method: Callable, send_dsl: "Se
|
|
|
275
350
|
# 不绑定到首次内部 result(避免失败重试时提前触发)
|
|
276
351
|
if _has_sent_hooks:
|
|
277
352
|
wrapped.add_done_callback(lambda t: asyncio.ensure_future(_emit_hooks_done(t)))
|
|
353
|
+
if _ledger is not None:
|
|
354
|
+
wrapped.add_done_callback(_record_receipt)
|
|
278
355
|
return wrapped
|
|
279
356
|
|
|
280
357
|
# 无规则:保持原有行为,message.sent 在单次发送完成后触发
|
|
281
358
|
if _has_sent_hooks:
|
|
282
359
|
result.add_done_callback(lambda t: asyncio.ensure_future(_emit_hooks_done(t)))
|
|
360
|
+
if _ledger is not None:
|
|
361
|
+
result.add_done_callback(_record_receipt)
|
|
283
362
|
return result
|
|
284
363
|
|
|
285
364
|
return hooked
|
|
@@ -2112,18 +2191,15 @@ class BaseAdapter(ABC):
|
|
|
2112
2191
|
|
|
2113
2192
|
if self.ConfigClass is None:
|
|
2114
2193
|
return
|
|
2115
|
-
from .config_schema import
|
|
2116
|
-
dataclass_to_defaults_dict,
|
|
2117
|
-
dataclass_to_toml_with_comments,
|
|
2118
|
-
)
|
|
2194
|
+
from .config_schema import dataclass_to_toml_with_comments
|
|
2119
2195
|
|
|
2120
2196
|
key = self._get_config_key()
|
|
2121
2197
|
data = config_mgr.getConfig(key)
|
|
2122
2198
|
|
|
2123
2199
|
if data is None:
|
|
2124
|
-
|
|
2200
|
+
# 模板以带注释文本直接落盘(注释保留写入),字段描述对用户可见
|
|
2125
2201
|
toml_str = dataclass_to_toml_with_comments(self.ConfigClass)
|
|
2126
|
-
config_mgr.
|
|
2202
|
+
config_mgr.setConfigTemplate(key, toml_str, immediate=True)
|
|
2127
2203
|
self._get_logger().info(i18n.t("core.adapter.config_template_generated", key=key, toml=toml_str))
|
|
2128
2204
|
|
|
2129
2205
|
def _ensure_i18n_registered(self):
|