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.
Files changed (127) hide show
  1. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/PKG-INFO +7 -4
  2. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/README.pypi.md +0 -2
  3. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/pyproject.toml +11 -3
  4. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/create.py +23 -2
  5. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/init.py +4 -116
  6. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/utils/config_wizard.py +26 -4
  7. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/utils/scaffold_text.py +10 -0
  8. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/__init__.py +24 -1
  9. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/adapter.py +84 -8
  10. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/config_schema.py +336 -79
  11. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/errors.py +84 -0
  12. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/kv_builder.py +7 -3
  13. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/module.py +10 -5
  14. erispulse-2.8.0.dev2/src/ErisPulse/Core/Bases/sql_base.py +1576 -0
  15. erispulse-2.8.0.dev2/src/ErisPulse/Core/Bases/storage.py +1171 -0
  16. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Event/__init__.py +17 -0
  17. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Event/base.py +145 -106
  18. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Event/command.py +57 -82
  19. erispulse-2.8.0.dev2/src/ErisPulse/Core/Event/interaction.py +941 -0
  20. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Event/overrides.py +99 -7
  21. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Event/wrapper.py +585 -24
  22. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/__init__.py +28 -1
  23. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/adapter.py +139 -2
  24. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/config.py +200 -43
  25. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/constants.py +119 -0
  26. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/i18n/locales/en.py +56 -3
  27. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/i18n/locales/ja.py +56 -3
  28. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/i18n/locales/ru.py +56 -3
  29. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/i18n/locales/zh_cn.py +56 -3
  30. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/i18n/locales/zh_tw.py +56 -3
  31. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/lifecycle.py +71 -12
  32. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/module.py +467 -1
  33. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/scope.py +139 -8
  34. erispulse-2.8.0.dev2/src/ErisPulse/Core/storage/__init__.py +89 -0
  35. erispulse-2.8.0.dev2/src/ErisPulse/Core/storage/mysql.py +279 -0
  36. erispulse-2.8.0.dev2/src/ErisPulse/Core/storage/postgres.py +282 -0
  37. erispulse-2.8.0.dev2/src/ErisPulse/Core/storage/sqlite.py +261 -0
  38. erispulse-2.8.0.dev2/src/ErisPulse/Core/transcript.py +375 -0
  39. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/runtime/__init__.py +13 -0
  40. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/runtime/context.py +63 -0
  41. erispulse-2.8.0.dev2/src/ErisPulse/runtime/example_config.py +266 -0
  42. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/runtime/frame_config.py +68 -19
  43. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/sdk.py +37 -2
  44. erispulse-2.8.0.dev1/src/ErisPulse/Core/Bases/storage.py +0 -534
  45. erispulse-2.8.0.dev1/src/ErisPulse/Core/storage.py +0 -1520
  46. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/.gitignore +0 -0
  47. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/LICENSE +0 -0
  48. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/__init__.py +0 -0
  49. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/base.py +0 -0
  50. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/cli.py +0 -0
  51. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/__init__.py +0 -0
  52. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/config.py +0 -0
  53. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/doctor.py +0 -0
  54. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/install.py +0 -0
  55. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/language.py +0 -0
  56. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/list.py +0 -0
  57. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/list_remote.py +0 -0
  58. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/run.py +0 -0
  59. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/self_update.py +0 -0
  60. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/types.py +0 -0
  61. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/uninstall.py +0 -0
  62. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/commands/upgrade.py +0 -0
  63. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/console.py +0 -0
  64. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/constants.py +0 -0
  65. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/hints.py +0 -0
  66. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/i18n/__init__.py +0 -0
  67. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/i18n/locales/__init__.py +0 -0
  68. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/i18n/locales/en.py +0 -0
  69. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/i18n/locales/ja.py +0 -0
  70. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/i18n/locales/ru.py +0 -0
  71. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/i18n/locales/zh_cn.py +0 -0
  72. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/i18n/locales/zh_tw.py +0 -0
  73. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/registry.py +0 -0
  74. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/utils/__init__.py +0 -0
  75. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/utils/display.py +0 -0
  76. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/utils/file_watcher.py +0 -0
  77. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/CLI/utils/package_manager.py +0 -0
  78. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/client.py +0 -0
  79. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/converter.py +0 -0
  80. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/i18n_schema.py +0 -0
  81. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/manager.py +0 -0
  82. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/router.py +0 -0
  83. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/send_builder.py +0 -0
  84. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/send_rules.py +0 -0
  85. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Bases/websocket.py +0 -0
  86. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Event/message.py +0 -0
  87. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Event/message_builder.py +0 -0
  88. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Event/meta.py +0 -0
  89. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Event/notice.py +0 -0
  90. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Event/request.py +0 -0
  91. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/Event/session_type.py +0 -0
  92. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/assets/__init__.py +0 -0
  93. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/assets/error.css +0 -0
  94. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/assets/error.html +0 -0
  95. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/assets/root.css +0 -0
  96. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/assets/root.html +0 -0
  97. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/client.py +0 -0
  98. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/i18n/__init__.py +0 -0
  99. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/i18n/constants.py +0 -0
  100. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/i18n/locales/__init__.py +0 -0
  101. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/logger.py +0 -0
  102. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/master.py +0 -0
  103. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/router.py +0 -0
  104. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/Core/text_match.py +0 -0
  105. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/__init__.py +0 -0
  106. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/__main__.py +0 -0
  107. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/finders/__init__.py +0 -0
  108. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/finders/adapter.py +0 -0
  109. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/finders/bases/__init__.py +0 -0
  110. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/finders/bases/finder.py +0 -0
  111. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/finders/module.py +0 -0
  112. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/loaders/__init__.py +0 -0
  113. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/loaders/adapter.py +0 -0
  114. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/loaders/bases/__init__.py +0 -0
  115. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/loaders/bases/loader.py +0 -0
  116. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/loaders/module.py +0 -0
  117. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/loaders/plugin_folder.py +0 -0
  118. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/loaders/strategy.py +0 -0
  119. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/loaders/strict.py +0 -0
  120. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/py.typed +0 -0
  121. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/runtime/config_schema.py +0 -0
  122. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/runtime/diagnostics.py +0 -0
  123. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/runtime/exceptions.py +0 -0
  124. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/runtime/hints.py +0 -0
  125. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/runtime/memory.py +0 -0
  126. {erispulse-2.8.0.dev1 → erispulse-2.8.0.dev2}/src/ErisPulse/runtime/plugin_reload.py +0 -0
  127. {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.dev1
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: toml>=0.10.2
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
- ![ErisPulse](https://raw.githubusercontent.com/ErisPulse/ErisPulse/main/.github/assets/mascot-hero.png)
48
-
49
52
  # ErisPulse
50
53
 
51
54
  **Write once, deploy on multiple platforms.**
@@ -1,5 +1,3 @@
1
- ![ErisPulse](https://raw.githubusercontent.com/ErisPulse/ErisPulse/main/.github/assets/mascot-hero.png)
2
-
3
1
  # ErisPulse
4
2
 
5
3
  **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.1"
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
- "toml>=0.10.2",
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
- \"\"\"{text[module.config_doc]}\"\"\"
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
- \"\"\"{text[adapter.config_doc]}\"\"\"
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
- 配置注释跟随 CLI 语言(缺失语言回退英文),
179
- 文案键集中于 ``scaffold_text`` 的 ``cfg.*`` 键族维护。
178
+ 生成逻辑收归 ``ErisPulse.runtime.example_config``(框架运行与 CLI 共用:
179
+ 含自维护首行标记、静态框架段与已安装组件声明式段)。此处仅保留调用入口。
180
180
 
181
181
  :param adapter_list: [list] 适配器名称列表 (默认: None)
182
182
  :return: [str] 完整配置示例字符串
183
183
  """
184
- from ..utils.scaffold_text import ScaffoldText
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
- lines.extend(
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
- current_store = dict(current_values or {})
535
- values = dict(current_store)
536
- for name, field_schema in _sort_fields(schema.get("fields", {})):
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
- data = dataclass_to_defaults_dict(self.ConfigClass)
2200
+ # 模板以带注释文本直接落盘(注释保留写入),字段描述对用户可见
2125
2201
  toml_str = dataclass_to_toml_with_comments(self.ConfigClass)
2126
- config_mgr.setConfig(key, data, immediate=True)
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):