ErisPulse 2.7.1.dev4__tar.gz → 2.8.0.dev1__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 (120) hide show
  1. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/.gitignore +1 -0
  2. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/PKG-INFO +6 -29
  3. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/README.pypi.md +3 -1
  4. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/pyproject.toml +20 -6
  5. erispulse-2.8.0.dev1/src/ErisPulse/CLI/commands/config.py +208 -0
  6. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/commands/create.py +133 -11
  7. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/commands/init.py +106 -104
  8. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/commands/install.py +68 -93
  9. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/commands/run.py +13 -3
  10. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/constants.py +6 -0
  11. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/i18n/locales/en.py +74 -1
  12. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/i18n/locales/ja.py +74 -1
  13. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/i18n/locales/ru.py +74 -1
  14. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/i18n/locales/zh_cn.py +74 -1
  15. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/i18n/locales/zh_tw.py +74 -1
  16. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/utils/__init__.py +2 -0
  17. erispulse-2.8.0.dev1/src/ErisPulse/CLI/utils/config_wizard.py +859 -0
  18. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/utils/scaffold_text.py +100 -0
  19. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Bases/__init__.py +8 -2
  20. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Bases/adapter.py +556 -142
  21. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Bases/client.py +5 -1
  22. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Bases/i18n_schema.py +1 -0
  23. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Bases/module.py +130 -0
  24. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Event/__init__.py +4 -0
  25. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Event/base.py +123 -41
  26. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Event/command.py +346 -94
  27. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Event/message.py +45 -8
  28. erispulse-2.8.0.dev1/src/ErisPulse/Core/Event/overrides.py +727 -0
  29. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Event/session_type.py +45 -1
  30. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Event/wrapper.py +15 -1
  31. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/__init__.py +20 -6
  32. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/adapter.py +610 -39
  33. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/client.py +6 -2
  34. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/config.py +109 -28
  35. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/constants.py +195 -18
  36. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/i18n/__init__.py +10 -6
  37. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/i18n/locales/en.py +72 -0
  38. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/i18n/locales/ja.py +72 -0
  39. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/i18n/locales/ru.py +72 -0
  40. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/i18n/locales/zh_cn.py +72 -0
  41. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/i18n/locales/zh_tw.py +72 -0
  42. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/lifecycle.py +17 -0
  43. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/logger.py +301 -23
  44. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/master.py +143 -13
  45. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/module.py +713 -127
  46. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/router.py +135 -7
  47. erispulse-2.8.0.dev1/src/ErisPulse/Core/scope.py +1171 -0
  48. erispulse-2.8.0.dev1/src/ErisPulse/Core/text_match.py +195 -0
  49. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/__init__.py +5 -2
  50. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/loaders/adapter.py +8 -0
  51. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/loaders/module.py +901 -56
  52. erispulse-2.8.0.dev1/src/ErisPulse/loaders/plugin_folder.py +298 -0
  53. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/runtime/__init__.py +16 -1
  54. erispulse-2.8.0.dev1/src/ErisPulse/runtime/context.py +104 -0
  55. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/runtime/frame_config.py +83 -0
  56. erispulse-2.8.0.dev1/src/ErisPulse/runtime/plugin_reload.py +182 -0
  57. erispulse-2.8.0.dev1/src/ErisPulse/runtime/tasks.py +235 -0
  58. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/sdk.py +194 -8
  59. erispulse-2.7.1.dev4/src/ErisPulse/runtime/context.py +0 -51
  60. erispulse-2.7.1.dev4/src/ErisPulse/runtime/tasks.py +0 -109
  61. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/LICENSE +0 -0
  62. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/__init__.py +0 -0
  63. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/base.py +0 -0
  64. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/cli.py +0 -0
  65. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/commands/__init__.py +0 -0
  66. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/commands/doctor.py +0 -0
  67. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/commands/language.py +0 -0
  68. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/commands/list.py +0 -0
  69. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/commands/list_remote.py +0 -0
  70. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/commands/self_update.py +0 -0
  71. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/commands/types.py +0 -0
  72. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/commands/uninstall.py +0 -0
  73. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/commands/upgrade.py +0 -0
  74. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/console.py +0 -0
  75. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/hints.py +0 -0
  76. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/i18n/__init__.py +0 -0
  77. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/i18n/locales/__init__.py +0 -0
  78. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/registry.py +0 -0
  79. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/utils/display.py +0 -0
  80. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/utils/file_watcher.py +0 -0
  81. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/CLI/utils/package_manager.py +0 -0
  82. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Bases/config_schema.py +0 -0
  83. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Bases/converter.py +0 -0
  84. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Bases/errors.py +0 -0
  85. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Bases/kv_builder.py +0 -0
  86. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Bases/manager.py +0 -0
  87. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Bases/router.py +0 -0
  88. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Bases/send_builder.py +0 -0
  89. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Bases/send_rules.py +0 -0
  90. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Bases/storage.py +0 -0
  91. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Bases/websocket.py +0 -0
  92. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Event/message_builder.py +0 -0
  93. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Event/meta.py +0 -0
  94. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Event/notice.py +0 -0
  95. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/Event/request.py +0 -0
  96. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/assets/__init__.py +0 -0
  97. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/assets/error.css +0 -0
  98. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/assets/error.html +0 -0
  99. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/assets/root.css +0 -0
  100. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/assets/root.html +0 -0
  101. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/i18n/constants.py +0 -0
  102. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/i18n/locales/__init__.py +0 -0
  103. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/Core/storage.py +0 -0
  104. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/__main__.py +0 -0
  105. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/finders/__init__.py +0 -0
  106. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/finders/adapter.py +0 -0
  107. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/finders/bases/__init__.py +0 -0
  108. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/finders/bases/finder.py +0 -0
  109. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/finders/module.py +0 -0
  110. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/loaders/__init__.py +0 -0
  111. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/loaders/bases/__init__.py +0 -0
  112. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/loaders/bases/loader.py +0 -0
  113. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/loaders/strategy.py +0 -0
  114. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/loaders/strict.py +0 -0
  115. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/py.typed +0 -0
  116. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/runtime/config_schema.py +0 -0
  117. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/runtime/diagnostics.py +0 -0
  118. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/runtime/exceptions.py +0 -0
  119. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/runtime/hints.py +0 -0
  120. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev1}/src/ErisPulse/runtime/memory.py +0 -0
@@ -1,6 +1,7 @@
1
1
  # dev gnore ignore file for Python projects
2
2
  devs/videos
3
3
  main.py
4
+ plugins/
4
5
  venvs
5
6
  config.toml
6
7
  uv.lock
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: ErisPulse
3
- Version: 2.7.1.dev4
3
+ Version: 2.8.0.dev1
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
@@ -10,27 +10,7 @@ Project-URL: Bug Tracker, https://github.com/ErisPulse/ErisPulse/blob/main/docs/
10
10
  Project-URL: Changelog, https://github.com/ErisPulse/ErisPulse/blob/main/CHANGELOG.md
11
11
  Author: ErisDev
12
12
  Maintainer-email: "艾莉丝·格雷拉特(WSu2059)" <wsu2059@qq.com>
13
- License: MIT License
14
-
15
- Copyright (c) 2025-2026 ErisPulse
16
-
17
- Permission is hereby granted, free of charge, to any person obtaining a copy
18
- of this software and associated documentation files (the "Software"), to deal
19
- in the Software without restriction, including without limitation the rights
20
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
21
- copies of the Software, and to permit persons to whom the Software is
22
- furnished to do so, subject to the following conditions:
23
-
24
- The above copyright notice and this permission notice shall be included in all
25
- copies or substantial portions of the Software.
26
-
27
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33
- SOFTWARE.
13
+ License-Expression: MIT
34
14
  License-File: LICENSE
35
15
  Keywords: async,asyncio,bot,bot-framework,chatbot,email,extensible,framework,instant-messaging,kook,matrix,modular,multi-platform,onebot,onebot12,qq,robot,sdk,telegram,yunhu
36
16
  Classifier: Development Status :: 4 - Beta
@@ -54,11 +34,6 @@ Requires-Dist: rich>=13.0.0
54
34
  Requires-Dist: toml>=0.10.2
55
35
  Requires-Dist: uvicorn>=0.30.0
56
36
  Requires-Dist: websockets>=12.0
57
- Provides-Extra: dev
58
- Requires-Dist: basedpyright>=1.20.0; extra == 'dev'
59
- Requires-Dist: pyclean>=3.6.0; extra == 'dev'
60
- Requires-Dist: ruff>=0.1.7; extra == 'dev'
61
- Requires-Dist: typing-extensions>=4.7.0; (python_version < '3.11') and extra == 'dev'
62
37
  Provides-Extra: test
63
38
  Requires-Dist: httpx>=0.24.0; extra == 'test'
64
39
  Requires-Dist: pytest-asyncio>=0.21.0; extra == 'test'
@@ -99,7 +74,9 @@ It ships with a flexible plugin system, hot reload, a visual Dashboard, an AI Bu
99
74
  - **Event-driven architecture** — Clear event model based on OneBot12, making message handling intuitive and consistent across platforms.
100
75
  - **Cross-platform compatibility** — Write handlers once, deploy to 15+ platforms without rewriting business logic.
101
76
  - **Modular design** — Flexible plugin system with hot-plug module management and per-module configuration.
102
- - **Hot reload** — Iterate on code during development without restarting the bot process.
77
+ - **Hot reload** — Local plugins reload on save; any module (including PyPI-installed ones) hot-reloads via a single `sdk.reload_module()` call — no process restart, even after pip upgrades.
78
+ - **Unified Event Overrides** — Override message triggers, command params and user permissions for *every* event type with a single config — no module code changes, takes effect instantly.
79
+ - **Scope Control** — Three-dimensional scope (module availability / event admission / outbound limits) with runtime-adjustable fine-grained control.
103
80
  - **Chainable SendDSL** — Express `@mention`, reply, retry, timeout, progress hook and bulk send as a single fluent chain.
104
81
  - **Complete toolchain** — `epsdk` CLI, visual Dashboard, AI Builder and Module Market included out of the box.
105
82
 
@@ -28,7 +28,9 @@ It ships with a flexible plugin system, hot reload, a visual Dashboard, an AI Bu
28
28
  - **Event-driven architecture** — Clear event model based on OneBot12, making message handling intuitive and consistent across platforms.
29
29
  - **Cross-platform compatibility** — Write handlers once, deploy to 15+ platforms without rewriting business logic.
30
30
  - **Modular design** — Flexible plugin system with hot-plug module management and per-module configuration.
31
- - **Hot reload** — Iterate on code during development without restarting the bot process.
31
+ - **Hot reload** — Local plugins reload on save; any module (including PyPI-installed ones) hot-reloads via a single `sdk.reload_module()` call — no process restart, even after pip upgrades.
32
+ - **Unified Event Overrides** — Override message triggers, command params and user permissions for *every* event type with a single config — no module code changes, takes effect instantly.
33
+ - **Scope Control** — Three-dimensional scope (module availability / event admission / outbound limits) with runtime-adjustable fine-grained control.
32
34
  - **Chainable SendDSL** — Express `@mention`, reply, retry, timeout, progress hook and bulk send as a single fluent chain.
33
35
  - **Complete toolchain** — `epsdk` CLI, visual Dashboard, AI Builder and Module Market included out of the box.
34
36
 
@@ -1,14 +1,15 @@
1
1
  [build-system]
2
- requires = ["hatchling"]
2
+ requires = ["hatchling>=1.26"]
3
3
  build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ErisPulse"
7
- version = "2.7.1-dev.4"
7
+ version = "2.8.0-dev.1"
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"
11
- license = { file = "LICENSE" }
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
12
13
 
13
14
  authors = [
14
15
  { name = "ErisDev" }
@@ -87,11 +88,14 @@ test = [
87
88
  "pytest-benchmark>=4.0.0",
88
89
  "httpx>=0.24.0",
89
90
  ]
91
+
92
+ [dependency-groups]
90
93
  dev = [
91
94
  "pyclean>=3.6.0",
92
95
  "ruff>=0.1.7",
93
96
  "basedpyright>=1.20.0",
94
- "typing_extensions>=4.7.0; python_version < '3.11'"]
97
+ "typing_extensions>=4.7.0; python_version < '3.11'",
98
+ ]
95
99
 
96
100
  [project.scripts]
97
101
  ep-init = "ErisPulse.__init__:init_sync"
@@ -118,7 +122,7 @@ include = [
118
122
  target-version = "py310"
119
123
  line-length = 120
120
124
 
121
- include = ["src/**/*.py"]
125
+ include = ["src/**/*.py", "tests/**/*.py"]
122
126
 
123
127
  extend-exclude = [
124
128
  "__pycache__",
@@ -161,6 +165,7 @@ ignore = [
161
165
  "PLR0912", # too-many-branches
162
166
  "PLR0913", # too-many-arguments
163
167
  "PLR0915", # too-many-statements
168
+ "PLR0917", # too-many-positional-arguments —— 同 PLR0913,既有公共 API 签名不可破坏
164
169
  "PLR2004", # magic-value-comparison —— 数值阈值判定过于严格
165
170
  "PLR1714", # repeated-equality-comparison
166
171
  "PLW0602", # global-variable-not-assigned
@@ -169,6 +174,7 @@ ignore = [
169
174
  "PERF203", # try-except-in-loop —— 异步轮询/重试场景的常见且必要写法,性能影响极小
170
175
  "RUF012", # mutable-class-default —— 类属性容器(handlers/事件表)是项目既有 API
171
176
  "RUF005", # collection-literal-concatenation —— `list + list` 有时比 unpack 更清晰
177
+ "RUF022", # unsorted-dunder-all —— __all__ 常按语义分组并附加注释,强制排序破坏可读性
172
178
  "RET504", # unnecessary-assign —— “先 assign 再 return”有时更易调试
173
179
 
174
180
  # 风格偏好(不强加于现有代码)
@@ -194,7 +200,15 @@ ignore = [
194
200
 
195
201
  [tool.ruff.lint.per-file-ignores]
196
202
  "__init__.py" = ["F401", "PLC0415", "I001"] # 导出式 __init__ 经常按需导入
197
- "tests/**/*.py" = ["S101", "PLR2004", "SLF001", "ARG", "ANN", "PTH"]
203
+ "tests/**/*.py" = [
204
+ # 断言/夹具相关(测试代码天然模式)
205
+ "S101", "PLR2004", "SLF001", "ARG", "ANN", "PTH",
206
+ "E712", "B011", "B017", "B018", "RUF043",
207
+ # 测试内常见的宽松写法
208
+ "E702", "E402", "F841", "RUF059", "PLW0108",
209
+ "UP031", "N814", "PERF401", "PLR0917", "RUF015",
210
+ "B007", "PIE810", "PLR0124", "PLW1510", "SIM118",
211
+ ]
198
212
 
199
213
  [tool.ruff.lint.pep8-naming]
200
214
  # 装饰器方法、classmethod 的首参数名允许保留
@@ -0,0 +1,208 @@
1
+ """
2
+ Config 命令实现
3
+
4
+ 交互式配置适配器/模块(schema 驱动向导,含适配器多账户管理)
5
+ """
6
+
7
+ import json
8
+ from argparse import ArgumentParser
9
+
10
+ from rich.box import SIMPLE
11
+ from rich.prompt import Prompt
12
+ from rich.table import Table
13
+
14
+ from ..base import Command
15
+ from ..console import console
16
+ from ..i18n import i18n
17
+ from ..utils import config_wizard
18
+ from ..utils.config_wizard import (
19
+ STATUS_INCOMPLETE,
20
+ STATUS_NONE,
21
+ STATUS_OK,
22
+ STATUS_UNCONFIGURED,
23
+ )
24
+ from ..utils.display import section_header
25
+
26
+
27
+ class ConfigCommand(Command):
28
+ """
29
+ config 命令
30
+
31
+ 交互式配置适配器/模块;适配器支持多账户(bot 账户)管理
32
+ """
33
+
34
+ name = "config"
35
+ description = i18n.t("cli.config.description")
36
+ aliases = ["cfg", "conf"]
37
+
38
+ def add_arguments(self, parser: ArgumentParser):
39
+ parser.add_argument(
40
+ "name",
41
+ nargs="?",
42
+ help=i18n.t("cli.config.name_help"),
43
+ )
44
+ parser.add_argument(
45
+ "--list",
46
+ "-l",
47
+ action="store_true",
48
+ help=i18n.t("cli.config.list_help"),
49
+ )
50
+ parser.add_argument(
51
+ "--json",
52
+ action="store_true",
53
+ help=i18n.t("cli.config.json_help"),
54
+ )
55
+
56
+ def execute(self, args):
57
+ from ErisPulse import config
58
+
59
+ targets = config_wizard.load_config_targets()
60
+ if not targets:
61
+ console.print(f"[dim] {i18n.t('cli.config.no_targets')}[/]")
62
+ console.print(f"[dim] {i18n.t('cli.config.install_hint')}[/]")
63
+ return
64
+
65
+ if getattr(args, "json", False):
66
+ self._print_status_json(targets, config)
67
+ return
68
+
69
+ if args.name:
70
+ self._run_named(targets, args.name, config)
71
+ return
72
+
73
+ self._print_status_table(targets, config)
74
+
75
+ if args.list:
76
+ return
77
+
78
+ if not config_wizard.is_interactive():
79
+ console.print(f"[dim] {i18n.t('cli.config.non_interactive_hint')}[/]")
80
+ return
81
+
82
+ self._interactive_select(targets, config)
83
+
84
+ def _print_status_json(self, targets, config):
85
+ """
86
+ 以 JSON 输出全部目标及其配置状态(供 CI / 脚本消费)
87
+
88
+ :param targets: ConfigTarget 列表
89
+ :param config: ConfigManager 实例
90
+ """
91
+ result = {}
92
+ for target in sorted(targets, key=lambda t: (t.kind != "adapter", t.name.lower())):
93
+ status, errors = config_wizard.get_target_status(target, config)
94
+ result[target.name] = {
95
+ "kind": target.kind,
96
+ "config_key": target.config_key,
97
+ "status": status,
98
+ "errors": errors,
99
+ }
100
+ console.print_json(json.dumps(result, ensure_ascii=False, indent=2))
101
+
102
+ def _run_named(self, targets, name: str, config):
103
+ """
104
+ 按名称定位目标并直接进入向导
105
+
106
+ :param targets: ConfigTarget 列表
107
+ :param name: 目标名(适配器平台名/模块名,或适配器配置键)
108
+ :param config: ConfigManager 实例
109
+ """
110
+ for target in targets:
111
+ if target.name == name or target.config_key == name:
112
+ if not target.configurable:
113
+ console.print(f"[warning] {i18n.t('cli.config.no_declaration', name=target.name)}[/]")
114
+ return
115
+ config_wizard.run_wizard(target, config)
116
+ return
117
+ console.print(f"[error] {i18n.t('cli.config.not_found', name=name)}[/]")
118
+
119
+ def _status_text(self, status: str) -> str:
120
+ """
121
+ 将状态常量渲染为带颜色的显示文本
122
+
123
+ :param status: get_target_status 返回的状态常量
124
+ :return: rich 标记的状态文本
125
+ """
126
+ mapping = {
127
+ STATUS_OK: ("cli.config.status_ok", "green"),
128
+ STATUS_INCOMPLETE: ("cli.config.status_incomplete", "yellow"),
129
+ STATUS_UNCONFIGURED: ("cli.config.status_unconfigured", "cyan"),
130
+ STATUS_NONE: ("cli.config.status_none", "dim"),
131
+ }
132
+ key, style = mapping.get(status, ("cli.config.status_none", "dim"))
133
+ return f"[{style}]{i18n.t(key)}[/]"
134
+
135
+ def _print_status_table(self, targets, config):
136
+ """
137
+ 打印全部目标及其配置状态表
138
+
139
+ :param targets: ConfigTarget 列表
140
+ :param config: ConfigManager 实例
141
+ """
142
+ table = Table(box=SIMPLE, show_lines=False, header_style="bold", pad_edge=False)
143
+ table.add_column(i18n.t("cli.config.header_kind"), width=8)
144
+ table.add_column(i18n.t("cli.config.header_name"), min_width=12)
145
+ table.add_column(i18n.t("cli.config.header_status"), width=10)
146
+ table.add_column(i18n.t("cli.config.header_detail"))
147
+
148
+ for target in sorted(targets, key=lambda t: (t.kind != "adapter", t.name.lower())):
149
+ status, errors = config_wizard.get_target_status(target, config)
150
+ detail = ";".join(errors[:3])
151
+ if len(errors) > 3:
152
+ detail += f" (+{len(errors) - 3})"
153
+ kind_style = "adapter" if target.kind == "adapter" else "module"
154
+ table.add_row(
155
+ f"[{kind_style}]{target.kind_label}[/]",
156
+ target.name,
157
+ self._status_text(status),
158
+ detail,
159
+ )
160
+
161
+ console.print(table)
162
+
163
+ def _interactive_select(self, targets, config):
164
+ """
165
+ 交互式选择目标并进入向导(可连续配置多个)
166
+
167
+ :param targets: ConfigTarget 列表
168
+ :param config: ConfigManager 实例
169
+ """
170
+ selectable = [t for t in targets if t.configurable]
171
+ if not selectable:
172
+ console.print(f"[dim] {i18n.t('cli.config.no_configurable')}[/]")
173
+ return
174
+
175
+ selectable.sort(key=lambda t: (t.kind != "adapter", t.name.lower()))
176
+
177
+ while True:
178
+ section_header(i18n.t("cli.config.select_section"))
179
+ for idx, target in enumerate(selectable, 1):
180
+ status, _ = config_wizard.get_target_status(target, config)
181
+ kind_style = "adapter" if target.kind == "adapter" else "module"
182
+ console.print(f" [bold]{idx}.[/] [{kind_style}]{target.name}[/] {self._status_text(status)}")
183
+
184
+ try:
185
+ raw = Prompt.ask(
186
+ f" {i18n.t('cli.config.select_prompt')}",
187
+ default="",
188
+ show_default=False,
189
+ )
190
+ except (EOFError, KeyboardInterrupt):
191
+ break
192
+
193
+ raw = raw.strip()
194
+ if not raw:
195
+ break
196
+
197
+ try:
198
+ idx = int(raw)
199
+ except ValueError:
200
+ console.print(f"[warning] {i18n.t('cli.config.invalid_index', idx=raw)}[/]")
201
+ continue
202
+
203
+ if not 1 <= idx <= len(selectable):
204
+ console.print(f"[warning] {i18n.t('cli.config.invalid_index', idx=idx)}[/]")
205
+ continue
206
+
207
+ config_wizard.run_wizard(selectable[idx - 1], config)
208
+ # 向导结束后回到选择菜单(状态重新刷新),支持连续配置多个目标
@@ -58,14 +58,23 @@ dependencies = [
58
58
 
59
59
  [project.entry-points]
60
60
  "erispulse.module" = {{ "{name}" = "{name}:Main" }}
61
+
62
+ [build-system]
63
+ requires = ["hatchling"]
64
+ build-backend = "hatchling.build"
65
+
66
+ [tool.hatch.build.targets.wheel]
67
+ packages = ["{name}"]
61
68
  """
62
69
 
63
70
  _MODULE_INIT = """from .Core import Main
64
71
  """
65
72
 
66
73
  _MODULE_CORE = """from dataclasses import dataclass, field
67
- from ErisPulse.Core.Bases import BaseConfig, BaseI18n, BaseModule, I18nKey
68
- from ErisPulse.Core.Event import command, message, notice
74
+
75
+ from ErisPulse import SDK
76
+ from ErisPulse.Core.Bases import BaseConfig, BaseI18n, BaseModule, I18nKey, ModuleMeta
77
+ from ErisPulse.Core.Event import Event, command, message, notice
69
78
  from ErisPulse.Core.i18n import i18n
70
79
 
71
80
 
@@ -90,6 +99,15 @@ class Main(BaseModule):
90
99
  class I18nClass(BaseI18n):
91
100
  \"\"\"{name} translation keys\"\"\"
92
101
 
102
+ meta_description: I18nKey = I18nKey(
103
+ key=\"module.{name}.meta.description\",
104
+ default=\"{name} module\",
105
+ zh_CN=\"{name} 模块\",
106
+ en=\"{name} module\",
107
+ ja=\"{name} モジュール\",
108
+ ru=\"Модуль {name}\",
109
+ zh_TW=\"{name} 模組\",
110
+ )
93
111
  enabled: I18nKey = I18nKey(
94
112
  key=\"module.{name}.enabled\",
95
113
  default=\"Enable module\",
@@ -118,22 +136,40 @@ class Main(BaseModule):
118
136
  zh_TW=\"來自 {name} 的問候!\",
119
137
  )
120
138
 
121
- def __init__(self, sdk=None):
122
- from ErisPulse import sdk as _sdk
123
- self.sdk = _sdk if sdk is None else sdk
139
+ def __init__(self, sdk: SDK = None):
140
+ # sdk 由框架在实例化时自动注入(无需手动导入兜底)
141
+ self.sdk = sdk
124
142
  self.logger = self.sdk.logger.get_child(\"{name}\")
125
143
  self.storage = self.sdk.storage
126
144
  self.adapter = self.sdk.adapter
127
145
  self.client = self.sdk.client
128
146
 
129
- self.logger.info((\"{text[module.log.init_done]}\").format(name=\"{name}\"))
147
+ self.logger.info(\"{text[module.log.init_done]}\")
148
+
149
+ @staticmethod
150
+ def get_meta() -> ModuleMeta:
151
+ \"\"\"
152
+ {text[module.meta_doc]}
153
+ \"\"\"
154
+ return ModuleMeta(
155
+ name=\"{name}\",
156
+ # description 支持 i18n 字典(key 由 I18nClass 注册),也可用纯字符串
157
+ description={{\"i18n\": \"module.{name}.meta.description\", \"default\": \"{name} module\"}},
158
+ version=\"0.1.0\",
159
+ author=\"ErisDev\",
160
+ group=\"default\",
161
+ tags=[\"{name}\"],
162
+ )
130
163
 
131
164
  @staticmethod
132
165
  def get_load_strategy():
133
166
  from ErisPulse.loaders import ModuleLoadStrategy
134
167
  return ModuleLoadStrategy(
135
168
  lazy_load=False,
136
- priority=100
169
+ priority=100,
170
+ # 依赖声明(可选):缺失依赖的模块会被跳过加载;
171
+ # 被依赖模块卸载/热重载时,本模块将级联卸载/重载
172
+ # depends=[],
137
173
  )
138
174
 
139
175
  async def on_load(self, event: dict) -> bool:
@@ -163,21 +199,32 @@ class Main(BaseModule):
163
199
  self.logger.info(\"{text[module.log.config_updated]}\")
164
200
 
165
201
  async def _register_commands(self):
202
+ # 命令权限(可选):permission 为调用函数,返回 True 才执行命令;
203
+ # master=True 限定框架主人(仅开发者默认值,用户可在统一覆写系统
204
+ # ErisPulse.event.overrides.command.<module>.<cmd>.master = true/false 覆写收紧或放开);
205
+ # 跨命令的用户黑白名单用覆写系统 ACL
206
+ # (ErisPulse.event.overrides.acl 或 overrides.acl.set(name, allow/deny),命令名支持 glob);
207
+ # 模块级可用性 / 事件准入 / 出站限制由作用域 scope 管理(用户可控)
166
208
  @command(\"hello\", help=i18n.t(\"module.{name}.command.hello.help\"))
167
- async def hello_command(event):
209
+ async def hello_command(event: Event):
168
210
  await event.reply(i18n.t(\"module.{name}.command.hello.reply\"))
169
211
 
170
212
  async def _register_message_handlers(self):
171
213
  @message.on_private_message()
172
- async def private_message_handler(event):
214
+ async def private_message_handler(event: Event):
173
215
  self.logger.info((\"{text[module.log.private_message]}\").format(content=event.get_text()))
174
216
 
175
217
  @message.on_group_message()
176
- async def group_message_handler(event):
218
+ async def group_message_handler(event: Event):
177
219
  pass
178
220
 
221
+ # 消息装饰器支持 pattern(glob 通配符)/ regex(正则),不匹配则不触发
222
+ @message.on_message(pattern=\"签到*\")
223
+ async def signin_handler(event: Event):
224
+ await event.reply(\"签到成功\")
225
+
179
226
  @notice.on_friend_add()
180
- async def friend_add_handler(event):
227
+ async def friend_add_handler(event: Event):
181
228
  self.logger.info((\"{text[module.log.friend_add]}\").format(nickname=event.get_user_nickname()))
182
229
  """
183
230
 
@@ -198,6 +245,13 @@ dependencies = [
198
245
 
199
246
  [project.entry-points]
200
247
  "erispulse.adapter" = {{ "{entry_key}" = "{name}:{name}" }}
248
+
249
+ [build-system]
250
+ requires = ["hatchling"]
251
+ build-backend = "hatchling.build"
252
+
253
+ [tool.hatch.build.targets.wheel]
254
+ packages = ["{name}"]
201
255
  """
202
256
 
203
257
  _ADAPTER_INIT = """from .Core import {name}
@@ -212,6 +266,7 @@ __all__ = [
212
266
  _ADAPTER_CORE = """import asyncio
213
267
  import json
214
268
  from dataclasses import dataclass, field
269
+ from typing import ClassVar
215
270
  from ErisPulse.Core import BaseAdapter
216
271
  from ErisPulse.Core.Bases import BaseConfig, BaseI18n, I18nKey
217
272
  from ErisPulse.Core import router
@@ -223,6 +278,13 @@ class {name}(BaseAdapter):
223
278
  {text[adapter.doc]}
224
279
  \"\"\"
225
280
 
281
+ # 依赖声明(可选,ErisPulse 2.8.0+):
282
+ # depends = {{"adapters": [], "modules": []}} # 硬依赖:缺失时跳过启动
283
+ # optional_modules = [] # 软依赖:就绪/丢失时收到
284
+ # # on_dependency_ready/lost 回调
285
+ depends: ClassVar[dict] = {{}}
286
+ optional_modules: ClassVar[list] = []
287
+
226
288
  # {text[adapter.config_hint]}
227
289
  @dataclass
228
290
  class ConfigClass(BaseConfig):
@@ -645,6 +707,12 @@ class CreateCommand(Command):
645
707
  parser.add_argument(
646
708
  "--force", "-f", action="store_true", help=i18n.t("cli.create.force_help")
647
709
  )
710
+ parser.add_argument(
711
+ "--local",
712
+ action="store_true",
713
+ default=False,
714
+ help=i18n.t("cli.create.local_module_help"),
715
+ )
648
716
 
649
717
  def execute(self, args):
650
718
  create_type = getattr(args, "create_type", None)
@@ -712,6 +780,11 @@ class CreateCommand(Command):
712
780
  :param name: [str] 模块名称
713
781
  :return: [None] 无返回值
714
782
  """
783
+ # --local:创建本地插件(plugins/ 目录结构,免打包安装)
784
+ if getattr(args, "local", False):
785
+ self._create_module_local(args, name)
786
+ return
787
+
715
788
  description = self._ask_missing(
716
789
  args,
717
790
  "description",
@@ -797,6 +870,55 @@ class CreateCommand(Command):
797
870
  console.print(f"[error] {i18n.t('cli.create.failed', error=e)}")
798
871
  sys.exit(1)
799
872
 
873
+ def _create_module_local(self, args, name: str):
874
+ """
875
+ 创建本地插件(--local):生成 plugins/ 目录结构,免打包安装
876
+
877
+ :param args: [Any] 解析后的命令参数对象
878
+ :param name: [str] 模块名称
879
+ :return: [None] 无返回值
880
+ """
881
+ output = Path(args.output)
882
+ plugins_dir = output / "plugins"
883
+ plugin_dir = plugins_dir / name
884
+
885
+ if plugin_dir.exists() and not args.force:
886
+ console.print(
887
+ f"[error] {i18n.t('cli.create.dir_exists', dir=str(plugin_dir))}"
888
+ )
889
+ sys.exit(1)
890
+
891
+ try:
892
+ plugin_dir.mkdir(parents=True, exist_ok=True)
893
+
894
+ (plugin_dir / "__init__.py").write_text(_MODULE_INIT, encoding="utf-8")
895
+ (plugin_dir / "Core.py").write_text(
896
+ _MODULE_CORE.format(name=name, text=_scaffold_text(name)),
897
+ encoding="utf-8",
898
+ )
899
+
900
+ console.print()
901
+ console.print(
902
+ f"[success] {i18n.t('cli.create.module_created', name=name)}[/]"
903
+ )
904
+ console.print()
905
+ console.print(Text(i18n.t("cli.create.local_structure"), style="bold"))
906
+ console.print(" plugins/")
907
+ console.print(f" └── {name}/")
908
+ console.print(" ├── __init__.py")
909
+ console.print(" └── Core.py")
910
+ console.print()
911
+ console.print(Text(i18n.t("cli.create.next_steps"), style="bold"))
912
+ console.print(f" · {i18n.t('cli.create.cd_to', dir=str(plugin_dir))}")
913
+ console.print(
914
+ f" · {i18n.t('cli.create.edit_module', file=str(plugin_dir) + '/Core.py')}"
915
+ )
916
+ console.print(f" · {i18n.t('cli.create.local_tip')}")
917
+
918
+ except Exception as e:
919
+ console.print(f"[error] {i18n.t('cli.create.failed', error=e)}")
920
+ sys.exit(1)
921
+
800
922
  def _create_adapter(self, args, name: str):
801
923
  """
802
924
  创建 Adapter 项目脚手架