ErisPulse 2.4.2.dev0__tar.gz → 2.4.2.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 (117) hide show
  1. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/PKG-INFO +1 -1
  2. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/pyproject.toml +1 -1
  3. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/adapter.py +141 -125
  4. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/adapter.pyi +11 -3
  5. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/config.py +89 -39
  6. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/config.pyi +17 -0
  7. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/module.py +30 -19
  8. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/module.pyi +18 -5
  9. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/router.py +8 -3
  10. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/loaders/module.py +12 -4
  11. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/sdk.py +85 -53
  12. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/sdk.pyi +39 -8
  13. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/.gitignore +0 -0
  14. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/LICENSE +0 -0
  15. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/README.md +0 -0
  16. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/__init__.py +0 -0
  17. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/__init__.pyi +0 -0
  18. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/base.py +0 -0
  19. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/base.pyi +0 -0
  20. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/cli.py +0 -0
  21. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/cli.pyi +0 -0
  22. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/commands/__init__.py +0 -0
  23. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/commands/__init__.pyi +0 -0
  24. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/commands/init.py +0 -0
  25. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/commands/init.pyi +0 -0
  26. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/commands/install.py +0 -0
  27. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/commands/install.pyi +0 -0
  28. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/commands/list.py +0 -0
  29. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/commands/list.pyi +0 -0
  30. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/commands/list_remote.py +0 -0
  31. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/commands/list_remote.pyi +0 -0
  32. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/commands/run.py +0 -0
  33. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/commands/run.pyi +0 -0
  34. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/commands/self_update.py +0 -0
  35. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/commands/self_update.pyi +0 -0
  36. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/commands/uninstall.py +0 -0
  37. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/commands/uninstall.pyi +0 -0
  38. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/commands/upgrade.py +0 -0
  39. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/commands/upgrade.pyi +0 -0
  40. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/console.py +0 -0
  41. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/console.pyi +0 -0
  42. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/registry.py +0 -0
  43. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/registry.pyi +0 -0
  44. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/utils/__init__.py +0 -0
  45. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/utils/__init__.pyi +0 -0
  46. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/utils/package_manager.py +0 -0
  47. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/CLI/utils/package_manager.pyi +0 -0
  48. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Bases/__init__.py +0 -0
  49. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Bases/__init__.pyi +0 -0
  50. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Bases/adapter.py +0 -0
  51. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Bases/adapter.pyi +0 -0
  52. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Bases/manager.py +0 -0
  53. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Bases/manager.pyi +0 -0
  54. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Bases/module.py +0 -0
  55. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Bases/module.pyi +0 -0
  56. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Event/__init__.py +0 -0
  57. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Event/__init__.pyi +0 -0
  58. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Event/base.py +0 -0
  59. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Event/base.pyi +0 -0
  60. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Event/command.py +0 -0
  61. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Event/command.pyi +0 -0
  62. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Event/message.py +0 -0
  63. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Event/message.pyi +0 -0
  64. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Event/message_builder.py +0 -0
  65. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Event/message_builder.pyi +0 -0
  66. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Event/meta.py +0 -0
  67. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Event/meta.pyi +0 -0
  68. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Event/notice.py +0 -0
  69. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Event/notice.pyi +0 -0
  70. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Event/request.py +0 -0
  71. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Event/request.pyi +0 -0
  72. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Event/session_type.py +0 -0
  73. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Event/session_type.pyi +0 -0
  74. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Event/wrapper.py +0 -0
  75. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/Event/wrapper.pyi +0 -0
  76. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/__init__.py +0 -0
  77. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/__init__.pyi +0 -0
  78. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/lifecycle.py +0 -0
  79. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/lifecycle.pyi +0 -0
  80. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/logger.py +0 -0
  81. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/logger.pyi +0 -0
  82. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/router.pyi +0 -0
  83. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/storage.py +0 -0
  84. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/Core/storage.pyi +0 -0
  85. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/__init__.py +0 -0
  86. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/__init__.pyi +0 -0
  87. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/__main__.py +0 -0
  88. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/__main__.pyi +0 -0
  89. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/finders/__init__.py +0 -0
  90. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/finders/__init__.pyi +0 -0
  91. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/finders/adapter.py +0 -0
  92. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/finders/adapter.pyi +0 -0
  93. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/finders/bases/__init__.py +0 -0
  94. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/finders/bases/__init__.pyi +0 -0
  95. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/finders/bases/finder.py +0 -0
  96. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/finders/bases/finder.pyi +0 -0
  97. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/finders/module.py +0 -0
  98. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/finders/module.pyi +0 -0
  99. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/loaders/__init__.py +0 -0
  100. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/loaders/__init__.pyi +0 -0
  101. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/loaders/adapter.py +0 -0
  102. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/loaders/adapter.pyi +0 -0
  103. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/loaders/bases/__init__.py +0 -0
  104. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/loaders/bases/__init__.pyi +0 -0
  105. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/loaders/bases/loader.py +0 -0
  106. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/loaders/bases/loader.pyi +0 -0
  107. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/loaders/module.pyi +0 -0
  108. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/loaders/strategy.py +0 -0
  109. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/loaders/strategy.pyi +0 -0
  110. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/runtime/__init__.py +0 -0
  111. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/runtime/__init__.pyi +0 -0
  112. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/runtime/cleanup.py +0 -0
  113. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/runtime/cleanup.pyi +0 -0
  114. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/runtime/exceptions.py +0 -0
  115. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/runtime/exceptions.pyi +0 -0
  116. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/runtime/frame_config.py +0 -0
  117. {erispulse-2.4.2.dev0 → erispulse-2.4.2.dev1}/src/ErisPulse/runtime/frame_config.pyi +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ErisPulse
3
- Version: 2.4.2.dev0
3
+ Version: 2.4.2.dev1
4
4
  Summary: ErisPulse 是一个模块化、可扩展的异步 Python SDK 框架,主要用于构建高效、可维护的机器人应用程序。
5
5
  Author-email: ErisDev <erisdev@88.com>
6
6
  Maintainer-email: "艾莉丝·格雷拉特(WSu2059)" <wsu2059@qq.com>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ErisPulse"
7
- version = "2.4.2-dev.0"
7
+ version = "2.4.2-dev.1"
8
8
  description = "ErisPulse 是一个模块化、可扩展的异步 Python SDK 框架,主要用于构建高效、可维护的机器人应用程序。"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -51,6 +51,9 @@ class AdapterManager(ManagerBase):
51
51
  # Bot状态存储 - {platform: {bot_id: {"status": str, "last_active": float, "info": dict}}}
52
52
  self._bots: dict[str, dict[str, dict]] = {}
53
53
 
54
+ # 标记是否正在关闭,避免重复提交离线事件
55
+ self._is_being_shutdown = False
56
+
54
57
  def set_sdk_ref(self, sdk) -> bool:
55
58
  """
56
59
  设置 SDK 引用
@@ -287,127 +290,134 @@ class AdapterManager(ManagerBase):
287
290
  >>> # 关闭多个适配器
288
291
  >>> await adapter.shutdown(["Platform1", "Platform2"])
289
292
  """
290
- if platforms is None:
291
- platforms = list(self._adapters.keys())
292
- if not isinstance(platforms, list):
293
- platforms = [platforms]
294
- for platform in platforms:
295
- if platform not in self._adapters:
296
- raise ValueError(f"平台 {platform} 未注册")
297
-
298
- logger.info(f"关闭适配器 {platforms}")
299
-
300
- # 提交适配器关闭开始事件
301
- await lifecycle.submit_event(
302
- "adapter.stop", msg="开始关闭适配器", data={"platforms": platforms}
303
- )
304
-
305
- from .router import router
306
-
307
- # 需要收集受影响的 adapter 实例(因为多个平台可能共享同一个实例)
308
- affected_adapters = set()
309
- bots_to_offline = [] # [(platform, bot_id), ...]
293
+ # 设置关闭标志,避免重复提交离线事件
294
+ self._is_being_shutdown = True
310
295
 
311
- # 取消目标平台的后台启动任务
312
- for platform in platforms:
313
- task = self._adapter_tasks.pop(platform, None)
314
- if task and not task.done():
315
- task.cancel()
316
- logger.debug(f"已取消平台 {platform} 的后台启动任务")
317
-
318
- for platform in platforms:
319
- adapter_instance = self._adapters[platform]
320
- affected_adapters.add(adapter_instance)
321
-
322
- # 收集该平台下需要标记为离线的 Bot
323
- if platform in self._bots:
324
- for bot_id, bot_info in self._bots[platform].items():
325
- if bot_info.get("status") != "offline":
326
- bots_to_offline.append((platform, bot_id))
296
+ try:
297
+ if platforms is None:
298
+ platforms = list(self._adapters.keys())
299
+ if not isinstance(platforms, list):
300
+ platforms = [platforms]
301
+ for platform in platforms:
302
+ if platform not in self._adapters:
303
+ raise ValueError(f"平台 {platform} 未注册")
327
304
 
328
- # 对每个受影响的 adapter 实例执行 shutdown(如果尚未关闭)
329
- for adapter_instance in affected_adapters:
330
- if adapter_instance in self._started_instances:
331
- # 找到该实例对应的平台名(用于事件提交)
332
- instance_platforms = [
333
- p for p, a in self._adapters.items() if a is adapter_instance
334
- ]
335
- platform_label = (
336
- instance_platforms[0]
337
- if instance_platforms
338
- else str(id(adapter_instance))
339
- )
305
+ logger.info(f"关闭适配器 {platforms}")
340
306
 
341
- # 提交适配器状态变化事件(stopping)
342
- for p in instance_platforms:
343
- if p in platforms:
344
- await lifecycle.submit_event(
345
- "adapter.status.change",
346
- msg=f"适配器 {p} 状态变化: stopping",
347
- data={"platform": p, "status": "stopping"},
348
- )
307
+ # 提交适配器关闭开始事件
308
+ await lifecycle.submit_event(
309
+ "adapter.stop", msg="开始关闭适配器", data={"platforms": platforms}
310
+ )
349
311
 
350
- try:
351
- await adapter_instance.shutdown()
352
- self._started_instances.remove(adapter_instance)
312
+ from .router import router
313
+
314
+ # 需要收集受影响的 adapter 实例(因为多个平台可能共享同一个实例)
315
+ affected_adapters = set()
316
+ bots_to_offline = [] # [(platform, bot_id), ...]
317
+
318
+ # 取消目标平台的后台启动任务
319
+ for platform in platforms:
320
+ task = self._adapter_tasks.pop(platform, None)
321
+ if task and not task.done():
322
+ task.cancel()
323
+ logger.debug(f"已取消平台 {platform} 的后台启动任务")
324
+
325
+ for platform in platforms:
326
+ adapter_instance = self._adapters[platform]
327
+ affected_adapters.add(adapter_instance)
328
+
329
+ # 收集该平台下需要标记为离线的 Bot
330
+ if platform in self._bots:
331
+ for bot_id, bot_info in self._bots[platform].items():
332
+ if bot_info.get("status") != "offline":
333
+ bots_to_offline.append((platform, bot_id))
334
+
335
+ # 对每个受影响的 adapter 实例执行 shutdown(如果尚未关闭)
336
+ for adapter_instance in affected_adapters:
337
+ if adapter_instance in self._started_instances:
338
+ # 找到该实例对应的平台名(用于事件提交)
339
+ instance_platforms = [
340
+ p for p, a in self._adapters.items() if a is adapter_instance
341
+ ]
342
+ platform_label = (
343
+ instance_platforms[0]
344
+ if instance_platforms
345
+ else str(id(adapter_instance))
346
+ )
353
347
 
354
- # 提交适配器状态变化事件(stopped
348
+ # 提交适配器状态变化事件(stopping
355
349
  for p in instance_platforms:
356
350
  if p in platforms:
357
351
  await lifecycle.submit_event(
358
352
  "adapter.status.change",
359
- msg=f"适配器 {p} 状态变化: stopped",
360
- data={"platform": p, "status": "stopped"},
353
+ msg=f"适配器 {p} 状态变化: stopping",
354
+ data={"platform": p, "status": "stopping"},
361
355
  )
362
- except Exception as e:
363
- logger.error(f"关闭适配器实例 {id(adapter_instance)} 失败: {e}")
364
356
 
365
- # 提交适配器状态变化事件(stop_failed)
366
- for p in instance_platforms:
367
- if p in platforms:
368
- await lifecycle.submit_event(
369
- "adapter.status.change",
370
- msg=f"适配器 {p} 状态变化: stop_failed",
371
- data={
372
- "platform": p,
373
- "status": "stop_failed",
374
- "error": str(e),
375
- },
376
- )
357
+ try:
358
+ await adapter_instance.shutdown()
359
+ self._started_instances.remove(adapter_instance)
360
+
361
+ # 提交适配器状态变化事件(stopped)
362
+ for p in instance_platforms:
363
+ if p in platforms:
364
+ await lifecycle.submit_event(
365
+ "adapter.status.change",
366
+ msg=f"适配器 {p} 状态变化: stopped",
367
+ data={"platform": p, "status": "stopped"},
368
+ )
369
+ except Exception as e:
370
+ logger.error(f"关闭适配器实例 {id(adapter_instance)} 失败: {e}")
371
+
372
+ # 提交适配器状态变化事件(stop_failed)
373
+ for p in instance_platforms:
374
+ if p in platforms:
375
+ await lifecycle.submit_event(
376
+ "adapter.status.change",
377
+ msg=f"适配器 {p} 状态变化: stop_failed",
378
+ data={
379
+ "platform": p,
380
+ "status": "stop_failed",
381
+ "error": str(e),
382
+ },
383
+ )
384
+
385
+ # 清理被关闭平台的路由
386
+ for platform in platforms:
387
+ result = router.unregister_all_by_namespace(platform)
388
+ if result["http_count"] > 0 or result["websocket_count"] > 0:
389
+ logger.debug(
390
+ f"已清理平台 {platform} 的路由: HTTP={result['http_count']}, WebSocket={result['websocket_count']}"
391
+ )
377
392
 
378
- # 清理被关闭平台的路由
379
- for platform in platforms:
380
- result = router.unregister_all_by_namespace(platform)
381
- if result["http_count"] > 0 or result["websocket_count"] > 0:
382
- logger.debug(
383
- f"已清理平台 {platform} 的路由: HTTP={result['http_count']}, WebSocket={result['websocket_count']}"
384
- )
393
+ # 停止路由器(仅当所有适配器都关闭时)
394
+ if not self._started_instances:
395
+ await router.stop()
385
396
 
386
- # 停止路由器(仅当所有适配器都关闭时)
387
- if not self._started_instances:
388
- await router.stop()
389
-
390
- # 将相关 Bot 标记为离线
391
- for platform, bot_id in bots_to_offline:
392
- if platform in self._bots and bot_id in self._bots[platform]:
393
- self._bots[platform][bot_id]["status"] = "offline"
394
- # 提交 Bot 离线事件
395
- await lifecycle.submit_event(
396
- "adapter.bot.offline",
397
- msg=f"Bot {platform}/{bot_id} 离线",
398
- data={"platform": platform, "bot_id": bot_id, "status": "offline"},
399
- )
397
+ # 将相关 Bot 标记为离线
398
+ for platform, bot_id in bots_to_offline:
399
+ if platform in self._bots and bot_id in self._bots[platform]:
400
+ self._bots[platform][bot_id]["status"] = "offline"
401
+ # 提交 Bot 离线事件
402
+ await lifecycle.submit_event(
403
+ "adapter.bot.offline",
404
+ msg=f"Bot {platform}/{bot_id} 离线",
405
+ data={"platform": platform, "bot_id": bot_id, "status": "offline"},
406
+ )
400
407
 
401
- # 如果所有适配器都关闭了,清理事件处理器
402
- if not self._started_instances:
403
- self._onebot_handlers.clear()
404
- self._raw_handlers.clear()
405
- self._onebot_middlewares.clear()
408
+ # 如果所有适配器都关闭了,清理事件处理器
409
+ if not self._started_instances:
410
+ self._onebot_handlers.clear()
411
+ self._raw_handlers.clear()
412
+ self._onebot_middlewares.clear()
406
413
 
407
- # 提交适配器关闭完成事件
408
- await lifecycle.submit_event(
409
- "adapter.stopped", msg="适配器关闭完成", data={"platforms": platforms}
410
- )
414
+ # 提交适配器关闭完成事件
415
+ await lifecycle.submit_event(
416
+ "adapter.stopped", msg="适配器关闭完成", data={"platforms": platforms}
417
+ )
418
+ finally:
419
+ # 清除关闭标志
420
+ self._is_being_shutdown = False
411
421
 
412
422
  def clear(self) -> None:
413
423
  """
@@ -473,12 +483,11 @@ class AdapterManager(ManagerBase):
473
483
 
474
484
  def exists(self, platform: str) -> bool:
475
485
  """
476
- 检查平台是否存在
486
+ 检查平台是否已注册
477
487
 
478
488
  :param platform: 平台名称
479
- :return: [bool] 平台是否存在
489
+ :return: 平台是否已注册(即 adapter.register() 已被调用)
480
490
  """
481
- # 检查平台是否已注册(在 _adapters 中)
482
491
  return platform in self._adapters
483
492
 
484
493
  def is_enabled(self, platform: str) -> bool:
@@ -486,20 +495,25 @@ class AdapterManager(ManagerBase):
486
495
  检查平台适配器是否启用
487
496
 
488
497
  :param platform: 平台名称
489
- :return: [bool] 平台适配器是否启用
498
+ :return: 平台适配器是否启用
499
+
500
+ {!--< tips >!--}
501
+ 适配器启用条件:
502
+ 1. 适配器在配置文件中(ErisPulse.adapters.status.{platform} 存在)
503
+ 2. 配置值为启用状态
504
+
505
+ 如果适配器未在配置中,返回 False
506
+ {!--< /tips >!--}
490
507
  """
491
- # 不使用默认值,如果配置不存在则返回 None
508
+ from .config import parse_bool_config
509
+
492
510
  status = config.getConfig(f"ErisPulse.adapters.status.{platform}")
493
511
 
494
- # 如果状态不存在,说明是新适配器
512
+ # 适配器未在配置中,返回 False
495
513
  if status is None:
496
- return False # 新适配器默认不启用,需要在初始化时处理
497
-
498
- # 处理字符串形式的布尔值
499
- if isinstance(status, str):
500
- return status.lower() not in ("false", "0", "no", "off")
514
+ return False
501
515
 
502
- return bool(status)
516
+ return parse_bool_config(status)
503
517
 
504
518
  def enable(self, platform: str) -> bool:
505
519
  """
@@ -541,7 +555,7 @@ class AdapterManager(ManagerBase):
541
555
  :return: 是否取消成功
542
556
 
543
557
  {!--< internal-use >!--}
544
- 注意:此方法仅取消注册,不关闭已启动的适配器
558
+ 注意: 此方法仅取消注册, 不关闭已启动的适配器
545
559
  {!--< /internal-use >!--}
546
560
  """
547
561
  if platform not in self._adapters:
@@ -837,13 +851,15 @@ class AdapterManager(ManagerBase):
837
851
  )
838
852
 
839
853
  if status == "offline":
840
- asyncio.ensure_future(
841
- lifecycle.submit_event(
842
- "adapter.bot.offline",
843
- msg=f"Bot {platform}/{bot_id} 离线",
844
- data={"platform": platform, "bot_id": bot_id, "status": "offline"},
854
+ # 只有在非主动关闭的情况下才提交事件(避免与 shutdown() 重复)
855
+ if not self._is_being_shutdown:
856
+ asyncio.ensure_future(
857
+ lifecycle.submit_event(
858
+ "adapter.bot.offline",
859
+ msg=f"Bot {platform}/{bot_id} 离线",
860
+ data={"platform": platform, "bot_id": bot_id, "status": "offline"},
861
+ )
845
862
  )
846
- )
847
863
 
848
864
  def _update_bot_heartbeat(self, platform: str, self_info: dict) -> None:
849
865
  """
@@ -104,10 +104,10 @@ class AdapterManager(ManagerBase):
104
104
  ...
105
105
  def exists(self: object, platform: str) -> bool:
106
106
  """
107
- 检查平台是否存在
107
+ 检查平台是否已注册
108
108
 
109
109
  :param platform: 平台名称
110
- :return: [bool] 平台是否存在
110
+ :return: 平台是否已注册(即 adapter.register() 已被调用)
111
111
  """
112
112
  ...
113
113
  def is_enabled(self: object, platform: str) -> bool:
@@ -115,7 +115,15 @@ class AdapterManager(ManagerBase):
115
115
  检查平台适配器是否启用
116
116
 
117
117
  :param platform: 平台名称
118
- :return: [bool] 平台适配器是否启用
118
+ :return: 平台适配器是否启用
119
+
120
+ {!--< tips >!--}
121
+ 适配器启用条件:
122
+ 1. 适配器在配置文件中(ErisPulse.adapters.status.{platform} 存在)
123
+ 2. 配置值为启用状态
124
+
125
+ 如果适配器未在配置中,返回 False
126
+ {!--< /tips >!--}
119
127
  """
120
128
  ...
121
129
  def enable(self: object, platform: str) -> bool:
@@ -24,8 +24,9 @@ class ConfigManager:
24
24
  self._cache_timestamp = 0 # 缓存时间戳
25
25
  self._cache_timeout = 60 # 缓存超时时间(秒)
26
26
  self._write_delay = 5 # 写入延迟(秒)
27
- self._write_timer = None # 写入定时器
27
+ self._write_timer: threading.Timer | None = None # 写入定时器
28
28
  self._lock = threading.RLock() # 线程安全锁
29
+ self._file_lock = threading.RLock() # 文件操作锁,确保原子性
29
30
  self._migrate_config() # 迁移旧配置文件
30
31
  self._load_config() # 初始化时加载配置
31
32
 
@@ -138,56 +139,79 @@ class ConfigManager:
138
139
  def _flush_config(self) -> None:
139
140
  """
140
141
  将待写入的配置刷新到文件
142
+
143
+ 使用文件锁确保多线程环境下的原子性操作
141
144
  """
142
145
  with self._lock:
143
146
  if not self._dirty_keys:
144
147
  return # 没有需要写入的内容
145
148
 
146
- try:
147
- # 从文件读取完整配置
148
- if os.path.exists(self.CONFIG_FILE):
149
- with open(self.CONFIG_FILE, "r", encoding="utf-8") as f:
150
- config = toml.load(f)
151
- else:
152
- config = {}
153
-
154
- # 应用待写入的更改
155
- for key, value in self._dirty_keys.items():
156
- keys = key.split(".")
157
- current = config
158
- for k in keys[:-1]:
159
- if k not in current:
160
- current[k] = {}
161
- current = current[k]
162
- current[keys[-1]] = value
163
-
164
- # 对配置字典进行排序,确保同一模块的配置项排列在一起
165
- sorted_config = self._sort_config_dict(config)
166
-
167
- # 写入文件
168
- with open(self.CONFIG_FILE, "w", encoding="utf-8") as f:
169
- toml.dump(sorted_config, f)
170
-
171
- # 更新缓存并清除待写入队列
172
- self._cache = sorted_config
173
- self._cache_timestamp = time.time()
174
- self._dirty_keys.clear()
149
+ with self._file_lock: # 确保文件操作原子性
150
+ try:
151
+ # 从文件读取完整配置
152
+ if os.path.exists(self.CONFIG_FILE):
153
+ with open(self.CONFIG_FILE, "r", encoding="utf-8") as f:
154
+ config = toml.load(f)
155
+ else:
156
+ config = {}
157
+
158
+ # 应用待写入的更改
159
+ for key, value in self._dirty_keys.items():
160
+ keys = key.split(".")
161
+ current = config
162
+ for k in keys[:-1]:
163
+ if k not in current:
164
+ current[k] = {}
165
+ current = current[k]
166
+ current[keys[-1]] = value
167
+
168
+ # 对配置字典进行排序,确保同一模块的配置项排列在一起
169
+ sorted_config = self._sort_config_dict(config)
170
+
171
+ # 写入临时文件,确保原子性
172
+ temp_file = self.CONFIG_FILE + ".tmp"
173
+ with open(temp_file, "w", encoding="utf-8") as f:
174
+ toml.dump(sorted_config, f)
175
+
176
+ # 原子性重命名(跨平台兼容)
177
+ if os.name == 'nt': # Windows
178
+ if os.path.exists(self.CONFIG_FILE):
179
+ os.replace(temp_file, self.CONFIG_FILE)
180
+ else:
181
+ os.rename(temp_file, self.CONFIG_FILE)
182
+ else: # Unix/Linux/macOS
183
+ os.rename(temp_file, self.CONFIG_FILE)
184
+
185
+ # 更新缓存并清除待写入队列
186
+ self._cache = sorted_config
187
+ self._cache_timestamp = time.time()
188
+ self._dirty_keys.clear()
175
189
 
176
- except Exception as e:
177
- from .logger import logger
190
+ except Exception as e:
191
+ from .logger import logger
178
192
 
179
- logger.error(f"写入配置文件 {self.CONFIG_FILE} 失败: {e}")
193
+ logger.error(f"写入配置文件 {self.CONFIG_FILE} 失败: {e}")
194
+ # 清理临时文件
195
+ temp_file = self.CONFIG_FILE + ".tmp"
196
+ if os.path.exists(temp_file):
197
+ try:
198
+ os.remove(temp_file)
199
+ except Exception:
200
+ pass
180
201
 
181
202
  def _schedule_write(self) -> None:
182
203
  """
183
204
  安排延迟写入
205
+
206
+ 线程安全:使用锁保护 Timer 的取消和创建
184
207
  """
185
- if self._write_timer:
186
- self._write_timer.cancel()
208
+ with self._lock:
209
+ if self._write_timer:
210
+ self._write_timer.cancel()
187
211
 
188
- self._write_timer = threading.Timer(self._write_delay, self._flush_config)
189
- self._write_timer.daemon = True
190
- self._write_timer.start()
212
+ self._write_timer = threading.Timer(self._write_delay, self._flush_config)
213
+ self._write_timer.daemon = True
214
+ self._write_timer.start()
191
215
 
192
216
  def _check_cache_validity(self) -> None:
193
217
  """
@@ -268,4 +292,30 @@ class ConfigManager:
268
292
 
269
293
  config: ConfigManager = ConfigManager()
270
294
 
271
- __all__ = ["config"]
295
+
296
+ def parse_bool_config(value: Any) -> bool:
297
+ """
298
+ 解析配置中的布尔值
299
+
300
+ :param value: 配置值(可以是 bool, int, str 等)
301
+ :return: 解析后的布尔值
302
+
303
+ 支持的值:
304
+ - True: True, 1, "true", "True", "1", "yes", "Yes", "on", "On"
305
+ - False: False, 0, "false", "False", "0", "no", "No", "off", "Off"
306
+ """
307
+ if isinstance(value, bool):
308
+ return value
309
+
310
+ if isinstance(value, int):
311
+ return value != 0
312
+
313
+ if isinstance(value, str):
314
+ normalized = value.lower().strip()
315
+ return normalized in ("true", "1", "yes", "on")
316
+
317
+ # 其他类型尝试转换为布尔值
318
+ return bool(value)
319
+
320
+
321
+ __all__ = ["config", "parse_bool_config"]
@@ -46,11 +46,15 @@ class ConfigManager:
46
46
  def _flush_config(self: object) -> None:
47
47
  """
48
48
  将待写入的配置刷新到文件
49
+
50
+ 使用文件锁确保多线程环境下的原子性操作
49
51
  """
50
52
  ...
51
53
  def _schedule_write(self: object) -> None:
52
54
  """
53
55
  安排延迟写入
56
+
57
+ 线程安全:使用锁保护 Timer 的取消和创建
54
58
  """
55
59
  ...
56
60
  def _check_cache_validity(self: object) -> None:
@@ -87,3 +91,16 @@ class ConfigManager:
87
91
  ...
88
92
 
89
93
  config: ConfigManager
94
+
95
+ def parse_bool_config(value: Any) -> bool:
96
+ """
97
+ 解析配置中的布尔值
98
+
99
+ :param value: 配置值(可以是 bool, int, str 等)
100
+ :return: 解析后的布尔值
101
+
102
+ 支持的值:
103
+ - True: True, 1, "true", "True", "1", "yes", "Yes", "on", "On"
104
+ - False: False, 0, "false", "False", "0", "no", "No", "off", "Off"
105
+ """
106
+ ...
@@ -272,15 +272,17 @@ class ModuleManager(ManagerBase):
272
272
 
273
273
  def exists(self, module_name: str) -> bool:
274
274
  """
275
- 检查模块是否存在(已注册或在配置中)
275
+ 检查模块是否已注册
276
276
 
277
- :param module_name: [str] 模块名称
278
- :return: [bool] 模块是否存在
277
+ :param module_name: 模块名称
278
+ :return: 模块是否已注册(即 module.register() 已被调用)
279
+
280
+ {!--< tips >!--}
281
+ exists() 只检查模块类是否已注册到管理器,用于验证模块是否可以加载。
282
+ 如需检查模块是否启用,请使用 is_enabled()。
283
+ {!--< /tips >!--}
279
284
  """
280
- if module_name in self._module_classes:
281
- return True
282
- module_statuses = config.getConfig("ErisPulse.modules.status", {})
283
- return module_name in module_statuses
285
+ return module_name in self._module_classes
284
286
 
285
287
  def is_loaded(self, module_name: str) -> bool:
286
288
  """
@@ -350,9 +352,9 @@ class ModuleManager(ManagerBase):
350
352
  {!--< internal-use >!--}
351
353
  此方法仅供内部使用
352
354
 
353
- :param module_name: [str] 模块名称
354
- :param enabled: [bool] 是否启用模块 (默认: False)
355
- :return: [bool] 操作是否成功
355
+ :param module_name: 模块名称
356
+ :param enabled: 是否启用模块 (默认: False)
357
+ :return: 操作是否成功
356
358
  """
357
359
  if self.exists(module_name):
358
360
  return True
@@ -367,18 +369,27 @@ class ModuleManager(ManagerBase):
367
369
  """
368
370
  检查模块是否启用
369
371
 
370
- :param module_name: [str] 模块名称
371
- :return: [bool] 模块是否启用
372
+ :param module_name: 模块名称
373
+ :return: 模块是否启用
374
+
375
+ {!--< tips >!--}
376
+ 模块启用条件:
377
+ 1. 模块在配置文件中(ErisPulse.modules.status.{module_name} 存在)
378
+ 2. 配置值为启用状态
379
+
380
+ 如果模块未在配置中,返回 False
381
+ {!--< /tips >!--}
372
382
  """
373
- if (
374
- status := config.getConfig(f"ErisPulse.modules.status.{module_name}")
375
- ) is None:
376
- return False
383
+ from .config import parse_bool_config
384
+
385
+ status = config.getConfig(f"ErisPulse.modules.status.{module_name}")
377
386
 
378
- if isinstance(status, str):
379
- return status.lower() not in ("false", "0", "no", "off")
387
+ # 模块未在配置中,返回 False
388
+ if status is None:
389
+ return False
380
390
 
381
- return bool(status)
391
+ # 解析配置值
392
+ return parse_bool_config(status)
382
393
 
383
394
  def enable(self, module_name: str) -> bool:
384
395
  """