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.
Files changed (127) hide show
  1. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/.gitignore +2 -0
  2. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/PKG-INFO +3 -1
  3. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/README.pypi.md +2 -0
  4. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/pyproject.toml +1 -1
  5. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/create.py +5 -0
  6. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/run.py +1 -1
  7. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/utils/package_manager.py +18 -97
  8. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/__init__.py +6 -0
  9. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/errors.py +109 -2
  10. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/kv_builder.py +21 -19
  11. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/module.py +3 -0
  12. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/router.py +3 -1
  13. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/sql_base.py +38 -2
  14. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/base.py +19 -2
  15. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/command.py +197 -47
  16. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/interaction.py +22 -30
  17. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/overrides.py +15 -3
  18. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/__init__.py +6 -0
  19. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/adapter.py +30 -8
  20. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/client.py +35 -10
  21. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/config.py +124 -41
  22. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/constants.py +52 -0
  23. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/i18n/__init__.py +25 -0
  24. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/i18n/locales/en.py +32 -0
  25. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/i18n/locales/ja.py +32 -0
  26. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/i18n/locales/ru.py +32 -0
  27. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/i18n/locales/zh_cn.py +32 -0
  28. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/i18n/locales/zh_tw.py +32 -0
  29. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/lifecycle.py +93 -23
  30. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/module.py +56 -21
  31. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/router.py +31 -12
  32. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/scope.py +10 -5
  33. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/__init__.py +24 -7
  34. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/finders/bases/finder.py +5 -2
  35. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/loaders/adapter.py +20 -4
  36. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/loaders/bases/loader.py +90 -1
  37. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/loaders/module.py +48 -13
  38. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/loaders/plugin_folder.py +29 -1
  39. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/loaders/strict.py +1 -17
  40. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/runtime/__init__.py +8 -0
  41. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/runtime/context.py +33 -0
  42. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/runtime/diagnostics.py +11 -2
  43. {erispulse-2.8.0.dev2/src/ErisPulse/CLI/utils → erispulse-2.8.2/src/ErisPulse/runtime}/file_watcher.py +1 -1
  44. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/runtime/frame_config.py +8 -0
  45. erispulse-2.8.2/src/ErisPulse/runtime/owner_cleanup.py +171 -0
  46. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/runtime/plugin_reload.py +2 -2
  47. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/runtime/tasks.py +19 -1
  48. erispulse-2.8.2/src/ErisPulse/runtime/version.py +166 -0
  49. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/sdk.py +58 -4
  50. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/LICENSE +0 -0
  51. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/__init__.py +0 -0
  52. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/base.py +0 -0
  53. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/cli.py +0 -0
  54. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/__init__.py +0 -0
  55. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/config.py +0 -0
  56. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/doctor.py +0 -0
  57. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/init.py +0 -0
  58. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/install.py +0 -0
  59. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/language.py +0 -0
  60. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/list.py +0 -0
  61. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/list_remote.py +0 -0
  62. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/self_update.py +0 -0
  63. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/types.py +0 -0
  64. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/uninstall.py +0 -0
  65. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/commands/upgrade.py +0 -0
  66. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/console.py +0 -0
  67. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/constants.py +0 -0
  68. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/hints.py +0 -0
  69. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/i18n/__init__.py +0 -0
  70. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/i18n/locales/__init__.py +0 -0
  71. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/i18n/locales/en.py +0 -0
  72. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/i18n/locales/ja.py +0 -0
  73. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/i18n/locales/ru.py +0 -0
  74. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/i18n/locales/zh_cn.py +0 -0
  75. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/i18n/locales/zh_tw.py +0 -0
  76. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/registry.py +0 -0
  77. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/utils/__init__.py +0 -0
  78. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/utils/config_wizard.py +0 -0
  79. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/utils/display.py +0 -0
  80. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/CLI/utils/scaffold_text.py +0 -0
  81. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/adapter.py +0 -0
  82. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/client.py +0 -0
  83. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/config_schema.py +0 -0
  84. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/converter.py +0 -0
  85. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/i18n_schema.py +0 -0
  86. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/manager.py +0 -0
  87. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/send_builder.py +0 -0
  88. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/send_rules.py +0 -0
  89. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/storage.py +0 -0
  90. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Bases/websocket.py +0 -0
  91. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/__init__.py +0 -0
  92. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/message.py +0 -0
  93. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/message_builder.py +0 -0
  94. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/meta.py +0 -0
  95. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/notice.py +0 -0
  96. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/request.py +0 -0
  97. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/session_type.py +0 -0
  98. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/Event/wrapper.py +0 -0
  99. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/assets/__init__.py +0 -0
  100. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/assets/error.css +0 -0
  101. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/assets/error.html +0 -0
  102. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/assets/root.css +0 -0
  103. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/assets/root.html +0 -0
  104. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/i18n/constants.py +0 -0
  105. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/i18n/locales/__init__.py +0 -0
  106. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/logger.py +0 -0
  107. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/master.py +0 -0
  108. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/storage/__init__.py +0 -0
  109. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/storage/mysql.py +0 -0
  110. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/storage/postgres.py +0 -0
  111. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/storage/sqlite.py +0 -0
  112. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/text_match.py +0 -0
  113. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/Core/transcript.py +0 -0
  114. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/__main__.py +0 -0
  115. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/finders/__init__.py +0 -0
  116. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/finders/adapter.py +0 -0
  117. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/finders/bases/__init__.py +0 -0
  118. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/finders/module.py +0 -0
  119. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/loaders/__init__.py +0 -0
  120. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/loaders/bases/__init__.py +0 -0
  121. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/loaders/strategy.py +0 -0
  122. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/py.typed +0 -0
  123. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/runtime/config_schema.py +0 -0
  124. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/runtime/example_config.py +0 -0
  125. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/runtime/exceptions.py +0 -0
  126. {erispulse-2.8.0.dev2 → erispulse-2.8.2}/src/ErisPulse/runtime/hints.py +0 -0
  127. {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.0.dev2
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
  [![PyPI](https://img.shields.io/pypi/v/ErisPulse?style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ErisPulse/)
59
59
  [![Python](https://img.shields.io/badge/Python-3.10+-FFD43B?style=for-the-badge&logo=python&logoColor=blue)](https://pypi.org/project/ErisPulse/)
60
+ [![OneBot 11](https://img.shields.io/badge/OneBot-11-black?style=for-the-badge)](https://github.com/botuniverse/onebot-11)
61
+ [![OneBot 12](https://img.shields.io/badge/OneBot-12-black?style=for-the-badge)](https://12.onebot.dev/)
60
62
  [![License](https://img.shields.io/badge/License-MIT-blue?style=for-the-badge)](https://github.com/ErisPulse/ErisPulse/blob/main/LICENSE)
61
63
  [![Docs](https://img.shields.io/badge/docs-erisdev.com-FF6B9D?style=for-the-badge&logo=bookstack&logoColor=white)](https://www.erisdev.com)
62
64
  [![Stars](https://img.shields.io/github/stars/ErisPulse/ErisPulse?style=for-the-badge&logo=github&color=brightgreen)](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
  [![PyPI](https://img.shields.io/pypi/v/ErisPulse?style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/ErisPulse/)
8
8
  [![Python](https://img.shields.io/badge/Python-3.10+-FFD43B?style=for-the-badge&logo=python&logoColor=blue)](https://pypi.org/project/ErisPulse/)
9
+ [![OneBot 11](https://img.shields.io/badge/OneBot-11-black?style=for-the-badge)](https://github.com/botuniverse/onebot-11)
10
+ [![OneBot 12](https://img.shields.io/badge/OneBot-12-black?style=for-the-badge)](https://12.onebot.dev/)
9
11
  [![License](https://img.shields.io/badge/License-MIT-blue?style=for-the-badge)](https://github.com/ErisPulse/ErisPulse/blob/main/LICENSE)
10
12
  [![Docs](https://img.shields.io/badge/docs-erisdev.com-FF6B9D?style=for-the-badge&logo=bookstack&logoColor=white)](https://www.erisdev.com)
11
13
  [![Stars](https://img.shields.io/github/stars/ErisPulse/ErisPulse?style=for-the-badge&logo=github&color=brightgreen)](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.0-dev.2"
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
- # 版本号解析正则(PEP 440 子集,纯标准库,不依赖 packaging):
30
- # 支持 epoch (1!)、release 段、预发布后缀 (dev/a/alpha/b/beta/rc/c/pre)、
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
- 遵循项目命名规则排序:正式版 > post > rc > beta > alpha > dev;
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
- parsed = _parse_version(version)
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 (parsed["epoch"], padded, pre_key, post_key, local_key)
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
- k1 = self._version_key(version1)
907
- k2 = self._version_key(version2)
908
- if k1 > k2:
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` 复用同一解析口径(:func:`_parse_version`):
1439
- 仅当版本含预发布段 (dev/alpha/beta/rc/c/pre) 时返回 True
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] 是预发布版本返回 True
1362
+ :return: [bool] 是预发布版本时返回 True
1444
1363
  """
1445
- parsed = _parse_version(version)
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
- if not isinstance(self._data, dict):
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(self._data, ensure_ascii=False))
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
- if not isinstance(self._data, list):
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 self._data:
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
- if not isinstance(self._data, dict):
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(self._data)
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
- if not isinstance(self._data, dict):
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(self._data, ensure_ascii=False))
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
- if not isinstance(self._data, list):
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 self._data:
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
- if not isinstance(self._data, dict):
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(self._data)
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 约定一致)。
@@ -590,7 +590,9 @@ class SseEmitter:
590
590
  import json as _json
591
591
 
592
592
  if self._closed:
593
- raise RuntimeError("SSE connection is closed")
593
+ from ..i18n import i18n
594
+
595
+ raise RuntimeError(i18n.t("core.router.sse_closed"))
594
596
 
595
597
  payload_parts = []
596
598
 
@@ -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
- f"{self.dialect.name}: 连接不可达(冷却期内,稍后自动重连)"
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
  """