ErisPulse 2.7.1.dev4__tar.gz → 2.8.0.dev0__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 (116) hide show
  1. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/PKG-INFO +2 -2
  2. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/pyproject.toml +1 -1
  3. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/commands/create.py +108 -11
  4. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/commands/init.py +69 -26
  5. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/commands/run.py +13 -3
  6. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/constants.py +6 -0
  7. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/i18n/locales/en.py +5 -1
  8. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/i18n/locales/ja.py +5 -1
  9. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/i18n/locales/ru.py +5 -1
  10. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/i18n/locales/zh_cn.py +5 -1
  11. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/i18n/locales/zh_tw.py +5 -1
  12. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/utils/scaffold_text.py +100 -0
  13. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Bases/__init__.py +8 -2
  14. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Bases/adapter.py +36 -1
  15. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Bases/client.py +5 -1
  16. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Bases/module.py +130 -0
  17. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Event/base.py +48 -3
  18. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Event/command.py +19 -0
  19. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/__init__.py +14 -5
  20. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/adapter.py +420 -35
  21. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/client.py +6 -2
  22. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/constants.py +92 -2
  23. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/i18n/locales/en.py +54 -4
  24. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/i18n/locales/ja.py +54 -4
  25. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/i18n/locales/ru.py +54 -4
  26. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/i18n/locales/zh_cn.py +54 -4
  27. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/i18n/locales/zh_tw.py +54 -4
  28. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/lifecycle.py +17 -0
  29. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/logger.py +301 -23
  30. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/module.py +549 -18
  31. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/router.py +67 -3
  32. erispulse-2.8.0.dev0/src/ErisPulse/Core/scope.py +537 -0
  33. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/__init__.py +5 -2
  34. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/loaders/adapter.py +8 -0
  35. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/loaders/module.py +732 -8
  36. erispulse-2.8.0.dev0/src/ErisPulse/loaders/plugin_folder.py +298 -0
  37. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/runtime/__init__.py +14 -1
  38. erispulse-2.8.0.dev0/src/ErisPulse/runtime/context.py +104 -0
  39. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/runtime/frame_config.py +32 -0
  40. erispulse-2.8.0.dev0/src/ErisPulse/runtime/plugin_reload.py +182 -0
  41. erispulse-2.8.0.dev0/src/ErisPulse/runtime/tasks.py +235 -0
  42. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/sdk.py +162 -8
  43. erispulse-2.7.1.dev4/src/ErisPulse/runtime/context.py +0 -51
  44. erispulse-2.7.1.dev4/src/ErisPulse/runtime/tasks.py +0 -109
  45. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/.gitignore +0 -0
  46. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/LICENSE +0 -0
  47. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/README.pypi.md +0 -0
  48. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/__init__.py +0 -0
  49. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/base.py +0 -0
  50. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/cli.py +0 -0
  51. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/commands/__init__.py +0 -0
  52. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/commands/doctor.py +0 -0
  53. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/commands/install.py +0 -0
  54. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/commands/language.py +0 -0
  55. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/commands/list.py +0 -0
  56. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/commands/list_remote.py +0 -0
  57. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/commands/self_update.py +0 -0
  58. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/commands/types.py +0 -0
  59. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/commands/uninstall.py +0 -0
  60. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/commands/upgrade.py +0 -0
  61. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/console.py +0 -0
  62. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/hints.py +0 -0
  63. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/i18n/__init__.py +0 -0
  64. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/i18n/locales/__init__.py +0 -0
  65. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/registry.py +0 -0
  66. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/utils/__init__.py +0 -0
  67. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/utils/display.py +0 -0
  68. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/utils/file_watcher.py +0 -0
  69. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/CLI/utils/package_manager.py +0 -0
  70. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Bases/config_schema.py +0 -0
  71. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Bases/converter.py +0 -0
  72. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Bases/errors.py +0 -0
  73. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Bases/i18n_schema.py +0 -0
  74. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Bases/kv_builder.py +0 -0
  75. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Bases/manager.py +0 -0
  76. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Bases/router.py +0 -0
  77. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Bases/send_builder.py +0 -0
  78. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Bases/send_rules.py +0 -0
  79. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Bases/storage.py +0 -0
  80. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Bases/websocket.py +0 -0
  81. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Event/__init__.py +0 -0
  82. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Event/message.py +0 -0
  83. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Event/message_builder.py +0 -0
  84. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Event/meta.py +0 -0
  85. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Event/notice.py +0 -0
  86. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Event/request.py +0 -0
  87. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Event/session_type.py +0 -0
  88. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/Event/wrapper.py +0 -0
  89. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/assets/__init__.py +0 -0
  90. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/assets/error.css +0 -0
  91. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/assets/error.html +0 -0
  92. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/assets/root.css +0 -0
  93. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/assets/root.html +0 -0
  94. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/config.py +0 -0
  95. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/i18n/__init__.py +0 -0
  96. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/i18n/constants.py +0 -0
  97. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/i18n/locales/__init__.py +0 -0
  98. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/master.py +0 -0
  99. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/Core/storage.py +0 -0
  100. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/__main__.py +0 -0
  101. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/finders/__init__.py +0 -0
  102. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/finders/adapter.py +0 -0
  103. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/finders/bases/__init__.py +0 -0
  104. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/finders/bases/finder.py +0 -0
  105. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/finders/module.py +0 -0
  106. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/loaders/__init__.py +0 -0
  107. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/loaders/bases/__init__.py +0 -0
  108. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/loaders/bases/loader.py +0 -0
  109. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/loaders/strategy.py +0 -0
  110. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/loaders/strict.py +0 -0
  111. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/py.typed +0 -0
  112. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/runtime/config_schema.py +0 -0
  113. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/runtime/diagnostics.py +0 -0
  114. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/runtime/exceptions.py +0 -0
  115. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/runtime/hints.py +0 -0
  116. {erispulse-2.7.1.dev4 → erispulse-2.8.0.dev0}/src/ErisPulse/runtime/memory.py +0 -0
@@ -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.dev0
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
@@ -4,7 +4,7 @@ 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.0"
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"
@@ -64,8 +64,10 @@ _MODULE_INIT = """from .Core import Main
64
64
  """
65
65
 
66
66
  _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
67
+
68
+ from ErisPulse import SDK
69
+ from ErisPulse.Core.Bases import BaseConfig, BaseI18n, BaseModule, I18nKey, ModuleMeta
70
+ from ErisPulse.Core.Event import Event, command, message, notice
69
71
  from ErisPulse.Core.i18n import i18n
70
72
 
71
73
 
@@ -90,6 +92,15 @@ class Main(BaseModule):
90
92
  class I18nClass(BaseI18n):
91
93
  \"\"\"{name} translation keys\"\"\"
92
94
 
95
+ meta_description: I18nKey = I18nKey(
96
+ key=\"module.{name}.meta.description\",
97
+ default=\"{name} module\",
98
+ zh_CN=\"{name} 模块\",
99
+ en=\"{name} module\",
100
+ ja=\"{name} モジュール\",
101
+ ru=\"Модуль {name}\",
102
+ zh_TW=\"{name} 模組\",
103
+ )
93
104
  enabled: I18nKey = I18nKey(
94
105
  key=\"module.{name}.enabled\",
95
106
  default=\"Enable module\",
@@ -118,22 +129,40 @@ class Main(BaseModule):
118
129
  zh_TW=\"來自 {name} 的問候!\",
119
130
  )
120
131
 
121
- def __init__(self, sdk=None):
122
- from ErisPulse import sdk as _sdk
123
- self.sdk = _sdk if sdk is None else sdk
132
+ def __init__(self, sdk: SDK = None):
133
+ # sdk 由框架在实例化时自动注入(无需手动导入兜底)
134
+ self.sdk = sdk
124
135
  self.logger = self.sdk.logger.get_child(\"{name}\")
125
136
  self.storage = self.sdk.storage
126
137
  self.adapter = self.sdk.adapter
127
138
  self.client = self.sdk.client
128
139
 
129
- self.logger.info((\"{text[module.log.init_done]}\").format(name=\"{name}\"))
140
+ self.logger.info(\"{text[module.log.init_done]}\")
141
+
142
+ @staticmethod
143
+ def get_meta() -> ModuleMeta:
144
+ \"\"\"
145
+ {text[module.meta_doc]}
146
+ \"\"\"
147
+ return ModuleMeta(
148
+ name=\"{name}\",
149
+ # description 支持 i18n 字典(key 由 I18nClass 注册),也可用纯字符串
150
+ description={{\"i18n\": \"module.{name}.meta.description\", \"default\": \"{name} module\"}},
151
+ version=\"0.1.0\",
152
+ author=\"ErisDev\",
153
+ group=\"default\",
154
+ tags=[\"{name}\"],
155
+ )
130
156
 
131
157
  @staticmethod
132
158
  def get_load_strategy():
133
159
  from ErisPulse.loaders import ModuleLoadStrategy
134
160
  return ModuleLoadStrategy(
135
161
  lazy_load=False,
136
- priority=100
162
+ priority=100,
163
+ # 依赖声明(可选):缺失依赖的模块会被跳过加载;
164
+ # 被依赖模块卸载/热重载时,本模块将级联卸载/重载
165
+ # depends=[],
137
166
  )
138
167
 
139
168
  async def on_load(self, event: dict) -> bool:
@@ -164,20 +193,20 @@ class Main(BaseModule):
164
193
 
165
194
  async def _register_commands(self):
166
195
  @command(\"hello\", help=i18n.t(\"module.{name}.command.hello.help\"))
167
- async def hello_command(event):
196
+ async def hello_command(event: Event):
168
197
  await event.reply(i18n.t(\"module.{name}.command.hello.reply\"))
169
198
 
170
199
  async def _register_message_handlers(self):
171
200
  @message.on_private_message()
172
- async def private_message_handler(event):
201
+ async def private_message_handler(event: Event):
173
202
  self.logger.info((\"{text[module.log.private_message]}\").format(content=event.get_text()))
174
203
 
175
204
  @message.on_group_message()
176
- async def group_message_handler(event):
205
+ async def group_message_handler(event: Event):
177
206
  pass
178
207
 
179
208
  @notice.on_friend_add()
180
- async def friend_add_handler(event):
209
+ async def friend_add_handler(event: Event):
181
210
  self.logger.info((\"{text[module.log.friend_add]}\").format(nickname=event.get_user_nickname()))
182
211
  """
183
212
 
@@ -212,6 +241,7 @@ __all__ = [
212
241
  _ADAPTER_CORE = """import asyncio
213
242
  import json
214
243
  from dataclasses import dataclass, field
244
+ from typing import ClassVar
215
245
  from ErisPulse.Core import BaseAdapter
216
246
  from ErisPulse.Core.Bases import BaseConfig, BaseI18n, I18nKey
217
247
  from ErisPulse.Core import router
@@ -223,6 +253,13 @@ class {name}(BaseAdapter):
223
253
  {text[adapter.doc]}
224
254
  \"\"\"
225
255
 
256
+ # 依赖声明(可选,ErisPulse 2.8.0+):
257
+ # depends = {{"adapters": [], "modules": []}} # 硬依赖:缺失时跳过启动
258
+ # optional_modules = [] # 软依赖:就绪/丢失时收到
259
+ # # on_dependency_ready/lost 回调
260
+ depends: ClassVar[dict] = {{}}
261
+ optional_modules: ClassVar[list] = []
262
+
226
263
  # {text[adapter.config_hint]}
227
264
  @dataclass
228
265
  class ConfigClass(BaseConfig):
@@ -645,6 +682,12 @@ class CreateCommand(Command):
645
682
  parser.add_argument(
646
683
  "--force", "-f", action="store_true", help=i18n.t("cli.create.force_help")
647
684
  )
685
+ parser.add_argument(
686
+ "--local",
687
+ action="store_true",
688
+ default=False,
689
+ help=i18n.t("cli.create.local_module_help"),
690
+ )
648
691
 
649
692
  def execute(self, args):
650
693
  create_type = getattr(args, "create_type", None)
@@ -712,6 +755,11 @@ class CreateCommand(Command):
712
755
  :param name: [str] 模块名称
713
756
  :return: [None] 无返回值
714
757
  """
758
+ # --local:创建本地插件(plugins/ 目录结构,免打包安装)
759
+ if getattr(args, "local", False):
760
+ self._create_module_local(args, name)
761
+ return
762
+
715
763
  description = self._ask_missing(
716
764
  args,
717
765
  "description",
@@ -797,6 +845,55 @@ class CreateCommand(Command):
797
845
  console.print(f"[error] {i18n.t('cli.create.failed', error=e)}")
798
846
  sys.exit(1)
799
847
 
848
+ def _create_module_local(self, args, name: str):
849
+ """
850
+ 创建本地插件(--local):生成 plugins/ 目录结构,免打包安装
851
+
852
+ :param args: [Any] 解析后的命令参数对象
853
+ :param name: [str] 模块名称
854
+ :return: [None] 无返回值
855
+ """
856
+ output = Path(args.output)
857
+ plugins_dir = output / "plugins"
858
+ plugin_dir = plugins_dir / name
859
+
860
+ if plugin_dir.exists() and not args.force:
861
+ console.print(
862
+ f"[error] {i18n.t('cli.create.dir_exists', dir=str(plugin_dir))}"
863
+ )
864
+ sys.exit(1)
865
+
866
+ try:
867
+ plugin_dir.mkdir(parents=True, exist_ok=True)
868
+
869
+ (plugin_dir / "__init__.py").write_text(_MODULE_INIT, encoding="utf-8")
870
+ (plugin_dir / "Core.py").write_text(
871
+ _MODULE_CORE.format(name=name, text=_scaffold_text(name)),
872
+ encoding="utf-8",
873
+ )
874
+
875
+ console.print()
876
+ console.print(
877
+ f"[success] {i18n.t('cli.create.module_created', name=name)}[/]"
878
+ )
879
+ console.print()
880
+ console.print(Text(i18n.t("cli.create.local_structure"), style="bold"))
881
+ console.print(" plugins/")
882
+ console.print(f" └── {name}/")
883
+ console.print(" ├── __init__.py")
884
+ console.print(" └── Core.py")
885
+ console.print()
886
+ console.print(Text(i18n.t("cli.create.next_steps"), style="bold"))
887
+ console.print(f" · {i18n.t('cli.create.cd_to', dir=str(plugin_dir))}")
888
+ console.print(
889
+ f" · {i18n.t('cli.create.edit_module', file=str(plugin_dir) + '/Core.py')}"
890
+ )
891
+ console.print(f" · {i18n.t('cli.create.local_tip')}")
892
+
893
+ except Exception as e:
894
+ console.print(f"[error] {i18n.t('cli.create.failed', error=e)}")
895
+ sys.exit(1)
896
+
800
897
  def _create_adapter(self, args, name: str):
801
898
  """
802
899
  创建 Adapter 项目脚手架
@@ -124,6 +124,17 @@ class InitCommand(Command):
124
124
  try:
125
125
  for dir_name in ["config", "logs"]:
126
126
  (project_path / dir_name).mkdir(exist_ok=True)
127
+ # SSL 证书默认目录:跟随项目运行目录走,配置里用相对路径引用
128
+ ssl_dir = project_path / "config" / "ssl"
129
+ ssl_dir.mkdir(exist_ok=True)
130
+ for ssl_name in ("cert.pem", "key.pem"):
131
+ ssl_file = ssl_dir / ssl_name
132
+ if not ssl_file.exists():
133
+ ssl_file.write_text(
134
+ "# 将你的证书/密钥 PEM 内容粘贴到本文件,"
135
+ "或在配置中改用 ssl_cert/ssl_key 内联填写\n",
136
+ encoding="utf-8",
137
+ )
127
138
 
128
139
  config_file = project_path / "config" / "config.toml"
129
140
  if not config_file.exists():
@@ -187,52 +198,84 @@ class InitCommand(Command):
187
198
  """
188
199
  生成完整的配置示例文本
189
200
 
201
+ 配置注释跟随 CLI 语言(缺失语言回退英文),
202
+ 文案键集中于 ``scaffold_text`` 的 ``cfg.*`` 键族维护。
203
+
190
204
  :param adapter_list: [list] 适配器名称列表 (默认: None)
191
205
  :return: [str] 完整配置示例字符串
192
206
  """
207
+ from ..utils.scaffold_text import ScaffoldText
208
+
209
+ st = ScaffoldText()
193
210
  lines = [
194
- "# ErisPulse 完整配置示例",
195
- "# 此文件展示所有可用配置项及其默认值",
196
- "# 如需使用,将所需配置复制到 config.toml 并按需修改",
211
+ st.t("cfg.header.title"),
212
+ st.t("cfg.header.desc"),
213
+ st.t("cfg.header.usage"),
197
214
  "",
198
- "# ==================== 服务器 ====================",
215
+ st.t("cfg.section.server"),
199
216
  "",
200
217
  "[ErisPulse.server]",
201
- 'host = "0.0.0.0" # 监听地址',
202
- "port = 8000 # 监听端口",
203
- "auto_start = true # 是否自动启动 HTTP 服务器 (纯 WS/轮询适配器可设 false)",
204
- "ssl_certfile = null # SSL 证书路径",
205
- "ssl_keyfile = null # SSL 密钥路径",
218
+ f'host = "0.0.0.0" # {st.t("cfg.server.host")}',
219
+ f"port = 8000 # {st.t('cfg.server.port')}",
220
+ f"auto_start = true # {st.t('cfg.server.auto_start')}",
221
+ f'ssl_certfile = "config/ssl/cert.pem" # {st.t("cfg.server.ssl_certfile")}',
222
+ f'ssl_keyfile = "config/ssl/key.pem" # {st.t("cfg.server.ssl_keyfile")}',
223
+ st.t("cfg.server.ssl_inline_hint"),
224
+ '# ssl_cert = """-----BEGIN CERTIFICATE-----',
225
+ "# ...",
226
+ '# -----END CERTIFICATE-----"""',
227
+ '# ssl_key = """-----BEGIN PRIVATE KEY-----',
228
+ "# ...",
229
+ '# -----END PRIVATE KEY-----"""',
206
230
  "",
207
- "# ==================== 日志 ====================",
231
+ st.t("cfg.section.logger"),
208
232
  "",
209
233
  "[ErisPulse.logger]",
210
- 'level = "INFO" # 日志级别: DEBUG/INFO/WARNING/ERROR',
211
- 'log_files = [] # 日志文件列表, 如 ["logs/app.log"]',
212
- "memory_limit = 1000 # 内存日志条数上限",
234
+ f'level = "INFO" # {st.t("cfg.logger.level")}',
235
+ f"log_files = [] # {st.t('cfg.logger.log_files')}",
236
+ f'log_dir = "" # {st.t("cfg.logger.log_dir")}',
237
+ f'log_rotation = "size" # {st.t("cfg.logger.log_rotation")}',
238
+ f"log_max_size_mb = 10 # {st.t('cfg.logger.log_max_size_mb')}",
239
+ f"log_backup_count = 5 # {st.t('cfg.logger.log_backup_count')}",
240
+ f'log_rotation_when = "midnight" # {st.t("cfg.logger.log_rotation_when")}',
241
+ f"memory_limit = 1000 # {st.t('cfg.logger.memory_limit')}",
213
242
  "",
214
- "# ==================== 存储 ====================",
243
+ st.t("cfg.section.storage"),
215
244
  "",
216
245
  "[ErisPulse.storage]",
217
- "use_global_db = false # 是否使用全局数据库",
246
+ f"use_global_db = false # {st.t('cfg.storage.use_global_db')}",
218
247
  "",
219
- "# ==================== 事件系统 ====================",
248
+ st.t("cfg.section.event"),
220
249
  "",
221
250
  "[ErisPulse.event.message]",
222
- "ignore_self = true # 忽略自身消息",
251
+ f"ignore_self = true # {st.t('cfg.event.ignore_self')}",
223
252
  "",
224
253
  "[ErisPulse.event.command]",
225
- 'prefix = "/" # 命令前缀',
226
- "case_sensitive = true # 区分大小写",
227
- "allow_space_prefix = false # 允许前缀前有空格",
228
- "must_at_bot = false # 必须艾特Bot才触发",
254
+ f'prefix = "/" # {st.t("cfg.command.prefix")}',
255
+ f"case_sensitive = true # {st.t('cfg.command.case_sensitive')}",
256
+ f"allow_space_prefix = false # {st.t('cfg.command.allow_space_prefix')}",
257
+ f"must_at_bot = false # {st.t('cfg.command.must_at_bot')}",
229
258
  "",
230
- "# ==================== 框架 ====================",
259
+ st.t("cfg.section.framework"),
231
260
  "",
232
261
  "[ErisPulse.framework]",
233
- "enable_lazy_loading = true # 启用模块懒加载",
262
+ f"enable_lazy_loading = true # {st.t('cfg.framework.enable_lazy_loading')}",
263
+ f'plugins_dir = "plugins" # {st.t("cfg.framework.plugins_dir")}',
264
+ f"uninit_timeout = 30 # {st.t('cfg.framework.uninit_timeout')}",
265
+ f" {st.t('cfg.framework.uninit_timeout_line1')}",
266
+ f" {st.t('cfg.framework.uninit_timeout_line2')}",
267
+ f"strict_mode = false # {st.t('cfg.framework.strict_mode')}",
268
+ f"strict_mode_exceptions = {{ modules = [], adapters = [] }} # {st.t('cfg.framework.strict_mode_exceptions')}",
269
+ f"handler_max_concurrency = 64 # {st.t('cfg.framework.handler_max_concurrency')}",
270
+ f"proactive_gc_interval = 300 # {st.t('cfg.framework.proactive_gc_interval')}",
271
+ f"proactive_gc_generation = 2 # {st.t('cfg.framework.proactive_gc_generation')}",
272
+ f"proactive_gc_full_every = 10 # {st.t('cfg.framework.proactive_gc_full_every')}",
273
+ f"proactive_gc_memory_growth_mb = 100 # {st.t('cfg.framework.proactive_gc_memory_growth_mb')}",
274
+ f"proactive_gc_idle_only = true # {st.t('cfg.framework.proactive_gc_idle_only')}",
275
+ f"proactive_gc_gen0_min = 100 # {st.t('cfg.framework.proactive_gc_gen0_min')}",
276
+ f"offline_bot_expiry = 3600 # {st.t('cfg.framework.offline_bot_expiry')}",
234
277
  "",
235
- "# ==================== 路由增强 ====================",
278
+ st.t("cfg.section.router"),
236
279
  "",
237
280
  "[ErisPulse.router.cors]",
238
281
  "enabled = false",
@@ -249,7 +292,7 @@ class InitCommand(Command):
249
292
  'X-Content-Type-Options = "nosniff"',
250
293
  'X-Frame-Options = "DENY"',
251
294
  "",
252
- "# ==================== 适配器状态 ====================",
295
+ st.t("cfg.section.adapter_status"),
253
296
  "",
254
297
  "[ErisPulse.adapters.status]",
255
298
  ]
@@ -268,7 +311,7 @@ class InitCommand(Command):
268
311
  lines.extend(
269
312
  [
270
313
  "",
271
- "# ==================== 模块状态 ====================",
314
+ st.t("cfg.section.module_status"),
272
315
  "",
273
316
  "[ErisPulse.modules.status]",
274
317
  "# MyModule = true",
@@ -5,6 +5,7 @@ Run 命令实现
5
5
  """
6
6
 
7
7
  import asyncio
8
+ import os
8
9
  import runpy
9
10
  import subprocess
10
11
  import sys
@@ -17,7 +18,7 @@ from rich.panel import Panel
17
18
 
18
19
  from ..base import Command
19
20
  from ..console import console
20
- from ..constants import HARD_RESTART_EXIT_CODE
21
+ from ..constants import ENV_SUPERVISED, HARD_RESTART_EXIT_CODE
21
22
  from ..i18n import i18n
22
23
  from ..utils.file_watcher import FileSystemEventHandler, PollingObserver
23
24
 
@@ -186,7 +187,11 @@ class RunCommand(Command):
186
187
  process = None
187
188
  try:
188
189
  while True:
189
- process = subprocess.Popen(cmd)
190
+ # 注入监督者标记:子进程据此判断"有父进程会在退出码 42 时重新拉起我",
191
+ # 从而 hard_restart() 走退出码契约而非 self-respawn。
192
+ child_env = dict(os.environ)
193
+ child_env[ENV_SUPERVISED] = "cli"
194
+ process = subprocess.Popen(cmd, env=child_env)
190
195
  process.wait()
191
196
 
192
197
  if process.returncode == self._RESTART_EXIT_CODE:
@@ -195,7 +200,12 @@ class RunCommand(Command):
195
200
  time.sleep(0.5)
196
201
  continue
197
202
 
198
- # 非硬重启退出码:模块/适配器内部错误导致子进程异常终止
203
+ if process.returncode == 0:
204
+ # 子进程正常退出:finish,不再拉起
205
+ console.print(f"[info]{i18n.t('cli.run.process_exited')}[/]")
206
+ break
207
+
208
+ # 非硬重启/非正常退出码:模块/适配器内部错误导致子进程异常终止
199
209
  # 不退出主进程,等待后自动重试
200
210
  crash_count += 1
201
211
  backoff = min(self._MAX_CRASH_BACKOFF, 3.0 * crash_count)
@@ -23,6 +23,11 @@ StorageManager 等单例)。因此 CLI 需要的常量在此独立维护,**
23
23
  # 镜像于: ErisPulse.Core.constants.HARD_RESTART_EXIT_CODE
24
24
  HARD_RESTART_EXIT_CODE: int = 42
25
25
 
26
+ # 监督者标记环境变量名。
27
+ # CLI run 命令启动子进程时注入;SDK 据此判断是否被监督(决定硬重启是否会被拉起)。
28
+ # 镜像于: ErisPulse.Core.constants.ENV_SUPERVISED
29
+ ENV_SUPERVISED: str = "ERISPULSE_SUPERVISED"
30
+
26
31
  # 模块入口点组名(loader / finder / create / types 共用)。
27
32
  # 镜像于: ErisPulse.Core.constants.MODULE_ENTRY_POINT_GROUP
28
33
  MODULE_ENTRY_POINT_GROUP: str = "erispulse.module"
@@ -40,6 +45,7 @@ PYPI_PACKAGE_JSON_URL_TEMPLATE: str = "https://pypi.org/pypi/{package}/json"
40
45
 
41
46
  __all__ = [
42
47
  "ADAPTER_ENTRY_POINT_GROUP",
48
+ "ENV_SUPERVISED",
43
49
  "HARD_RESTART_EXIT_CODE",
44
50
  "MODULE_ENTRY_POINT_GROUP",
45
51
  "PYPI_PACKAGE_JSON_URL_TEMPLATE",
@@ -1,4 +1,4 @@
1
- """
1
+ """
2
2
  CLI English translations (en)
3
3
 
4
4
  Independent CLI translation data, decoupled from Core i18n.
@@ -48,6 +48,9 @@ TRANSLATIONS = {
48
48
  "cli.create.homepage_help": "Project homepage URL",
49
49
  "cli.create.output_help": "Output directory (default: current directory)",
50
50
  "cli.create.force_help": "Force overwrite existing directory",
51
+ "cli.create.local_module_help": "Create a local plugin (plugins/ structure, no packaging/install; module only)",
52
+ "cli.create.local_structure": "Local plugin structure",
53
+ "cli.create.local_tip": "Place the plugin in the project's plugins/ directory; auto-discovered on startup and hot-reloaded on change",
51
54
  "cli.create.select_type_title": "Select creation type",
52
55
  "cli.create.select_type_module": "Custom functional Module",
53
56
  "cli.create.select_type_adapter": "Platform Adapter",
@@ -276,6 +279,7 @@ TRANSLATIONS = {
276
279
  "cli.run.file_changed_restart": "File change detected ([cmd]{file}[/]), restarting...",
277
280
  "cli.run.process_exited": "Process exited normally, waiting for file changes to restart",
278
281
  "cli.run.process_crashed": "Process crashed (exit code {code}), fix and save to restart",
282
+ "cli.run.process_exited": "Child process exited normally, not respawning",
279
283
  "cli.run.subprocess_crashed_retry": "Subprocess crashed, will auto-retry in {seconds}s...",
280
284
  "cli.run.terminating_child": "Terminating the bot subprocess to release ports and resources...",
281
285
  # ==================== uninstall command ====================
@@ -1,4 +1,4 @@
1
- """
1
+ """
2
2
  CLI 日本語翻訳 (ja)
3
3
 
4
4
  Core i18n から独立した CLI 翻訳データ。
@@ -48,6 +48,9 @@ TRANSLATIONS = {
48
48
  "cli.create.homepage_help": "プロジェクトのホームページ URL",
49
49
  "cli.create.output_help": "出力ディレクトリ (デフォルト: カレントディレクトリ)",
50
50
  "cli.create.force_help": "既存のディレクトリを強制的に上書き",
51
+ "cli.create.local_module_help": "ローカルプラグインを作成(plugins/ 構造、パッケージ不要。module のみ)",
52
+ "cli.create.local_structure": "ローカルプラグイン構造",
53
+ "cli.create.local_tip": "プラグインをプロジェクトの plugins/ ディレクトリに配置すると起動時に自動検出され、変更時はホットリロードされます",
51
54
  "cli.create.select_type_title": "作成タイプを選択",
52
55
  "cli.create.select_type_module": "カスタム機能 Module",
53
56
  "cli.create.select_type_adapter": "プラットフォーム Adapter",
@@ -276,6 +279,7 @@ TRANSLATIONS = {
276
279
  "cli.run.file_changed_restart": "ファイル変更を検出 ([cmd]{file}[/])、再起動中...",
277
280
  "cli.run.process_exited": "プロセスが正常に終了しました。ファイルの変更を待機して再起動",
278
281
  "cli.run.process_crashed": "プロセスが異常終了しました(終了コード {code})。修正して保存すると自動再起動",
282
+ "cli.run.process_exited": "子プロセスが正常終了しました、再起動しません",
279
283
  "cli.run.subprocess_crashed_retry": "サブプロセスが異常終了しました。{seconds} 秒後に自動再試行します...",
280
284
  "cli.run.terminating_child": "ポートなどのリソースを解放するため、ボットの子プロセスを終了しています...",
281
285
  # ==================== uninstall コマンド ====================
@@ -1,4 +1,4 @@
1
- """
1
+ """
2
2
  CLI Русский перевод (ru)
3
3
 
4
4
  Независимые переводы CLI, отдельно от Core i18n.
@@ -48,6 +48,9 @@ TRANSLATIONS = {
48
48
  "cli.create.homepage_help": "URL домашней страницы проекта",
49
49
  "cli.create.output_help": "Выходной каталог (по умолчанию: текущий каталог)",
50
50
  "cli.create.force_help": "Принудительно перезаписать существующий каталог",
51
+ "cli.create.local_module_help": "Создать локальный плагин (структура plugins/, без упаковки и установки; только для module)",
52
+ "cli.create.local_structure": "Структура локального плагина",
53
+ "cli.create.local_tip": "Поместите плагин в каталог plugins/ проекта — он будет автоматически обнаружен при запуске и перезагружен при изменении",
51
54
  "cli.create.select_type_title": "Выберите тип создания",
52
55
  "cli.create.select_type_module": "Пользовательский функциональный Module",
53
56
  "cli.create.select_type_adapter": "Платформенный Adapter",
@@ -276,6 +279,7 @@ TRANSLATIONS = {
276
279
  "cli.run.file_changed_restart": "Обнаружено изменение файла ([cmd]{file}[/]), перезапуск...",
277
280
  "cli.run.process_exited": "Процесс завершен нормально, ожидание изменений файлов для перезапуска",
278
281
  "cli.run.process_crashed": "Процесс аварийно завершен (код {code}), исправьте и сохраните для перезапуска",
282
+ "cli.run.process_exited": "Дочерний процесс завершился нормально, не перезапускаем",
279
283
  "cli.run.subprocess_crashed_retry": "Подпроцесс аварийно завершен, автоматическая перезагрузка через {seconds}с...",
280
284
  "cli.run.terminating_child": "Завершение дочернего процесса бота для освобождения портов и ресурсов...",
281
285
  # ==================== команда uninstall ====================
@@ -1,4 +1,4 @@
1
- """
1
+ """
2
2
  CLI 简体中文翻译 (zh-CN)
3
3
 
4
4
  独立于 Core i18n 的 CLI 翻译数据。
@@ -48,6 +48,9 @@ TRANSLATIONS = {
48
48
  "cli.create.homepage_help": "项目主页 URL",
49
49
  "cli.create.output_help": "输出目录 (默认当前目录)",
50
50
  "cli.create.force_help": "强制覆盖已存在的目录",
51
+ "cli.create.local_module_help": "创建本地插件(plugins/ 目录结构,免打包安装,仅 module 可用)",
52
+ "cli.create.local_structure": "本地插件结构",
53
+ "cli.create.local_tip": "将插件放入项目的 plugins/ 目录,框架启动时自动发现;修改后热重载自动生效",
51
54
  "cli.create.select_type_title": "选择创建类型",
52
55
  "cli.create.select_type_module": "自定义功能模块",
53
56
  "cli.create.select_type_adapter": "平台适配器",
@@ -276,6 +279,7 @@ TRANSLATIONS = {
276
279
  "cli.run.file_changed_restart": "检测到文件变更 ([cmd]{file}[/]),正在重启...",
277
280
  "cli.run.process_exited": "进程已正常退出,等待文件变更后重启",
278
281
  "cli.run.process_crashed": "进程异常退出(退出码 {code}),修复后保存文件将自动重启",
282
+ "cli.run.process_exited": "子进程正常退出,不再拉起",
279
283
  "cli.run.subprocess_crashed_retry": "子进程异常终止,将在 {seconds} 秒后自动重试...",
280
284
  "cli.run.terminating_child": "正在终止机器人子进程,释放端口等资源...",
281
285
  # ==================== uninstall 命令 ====================
@@ -1,4 +1,4 @@
1
- """
1
+ """
2
2
  CLI 繁體中文翻譯 (zh-TW)
3
3
 
4
4
  獨立于 Core i18n 的 CLI 翻譯資料。
@@ -48,6 +48,9 @@ TRANSLATIONS = {
48
48
  "cli.create.homepage_help": "專案首頁 URL",
49
49
  "cli.create.output_help": "輸出目錄 (預設為當前目錄)",
50
50
  "cli.create.force_help": "強制覆蓋已存在的目錄",
51
+ "cli.create.local_module_help": "建立本地外掛(plugins/ 目錄結構,免打包安裝,僅 module 可用)",
52
+ "cli.create.local_structure": "本地外掛結構",
53
+ "cli.create.local_tip": "將外掛放入專案的 plugins/ 目錄,框架啟動時自動發現;修改後熱重載自動生效",
51
54
  "cli.create.select_type_title": "選擇建立類型",
52
55
  "cli.create.select_type_module": "自訂功能 Module",
53
56
  "cli.create.select_type_adapter": "平台 Adapter",
@@ -276,6 +279,7 @@ TRANSLATIONS = {
276
279
  "cli.run.file_changed_restart": "檢測到檔案變更 ([cmd]{file}[/]),正在重啟...",
277
280
  "cli.run.process_exited": "程式已正常退出,等待檔案變更後重啟",
278
281
  "cli.run.process_crashed": "程式異常退出(退出碼 {code}),修復後儲存檔案將自動重啟",
282
+ "cli.run.process_exited": "子程序正常退出,不再拉起",
279
283
  "cli.run.subprocess_crashed_retry": "子程序異常終止,將在 {seconds} 秒後自動重試...",
280
284
  "cli.run.terminating_child": "正在終止機器人子程序,釋放連接埠等資源...",
281
285
  # ==================== uninstall 命令 ====================