ErisPulse 2.7.1.dev2__tar.gz → 2.7.1.dev3__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 (111) hide show
  1. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/PKG-INFO +1 -1
  2. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/pyproject.toml +1 -1
  3. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/base.py +4 -4
  4. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/command.py +16 -8
  5. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/wrapper.py +68 -5
  6. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/constants.py +26 -3
  7. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/locales/en.py +2 -0
  8. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/locales/ja.py +2 -0
  9. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/locales/ru.py +2 -0
  10. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/locales/zh_cn.py +2 -0
  11. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/locales/zh_tw.py +2 -0
  12. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/frame_config.py +6 -0
  13. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/memory.py +13 -4
  14. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/sdk.py +216 -36
  15. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/.gitignore +0 -0
  16. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/LICENSE +0 -0
  17. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/README.pypi.md +0 -0
  18. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/__init__.py +0 -0
  19. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/base.py +0 -0
  20. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/cli.py +0 -0
  21. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/__init__.py +0 -0
  22. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/create.py +0 -0
  23. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/doctor.py +0 -0
  24. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/init.py +0 -0
  25. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/install.py +0 -0
  26. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/language.py +0 -0
  27. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/list.py +0 -0
  28. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/list_remote.py +0 -0
  29. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/run.py +0 -0
  30. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/self_update.py +0 -0
  31. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/types.py +0 -0
  32. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/uninstall.py +0 -0
  33. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/upgrade.py +0 -0
  34. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/console.py +0 -0
  35. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/constants.py +0 -0
  36. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/hints.py +0 -0
  37. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/i18n/__init__.py +0 -0
  38. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/i18n/locales/__init__.py +0 -0
  39. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/i18n/locales/en.py +0 -0
  40. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/i18n/locales/ja.py +0 -0
  41. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/i18n/locales/ru.py +0 -0
  42. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/i18n/locales/zh_cn.py +0 -0
  43. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/i18n/locales/zh_tw.py +0 -0
  44. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/registry.py +0 -0
  45. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/utils/__init__.py +0 -0
  46. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/utils/display.py +0 -0
  47. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/utils/file_watcher.py +0 -0
  48. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/utils/package_manager.py +0 -0
  49. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/utils/scaffold_text.py +0 -0
  50. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/__init__.py +0 -0
  51. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/adapter.py +0 -0
  52. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/client.py +0 -0
  53. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/config_schema.py +0 -0
  54. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/converter.py +0 -0
  55. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/errors.py +0 -0
  56. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/i18n_schema.py +0 -0
  57. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/kv_builder.py +0 -0
  58. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/manager.py +0 -0
  59. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/module.py +0 -0
  60. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/router.py +0 -0
  61. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/send_builder.py +0 -0
  62. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/send_rules.py +0 -0
  63. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/storage.py +0 -0
  64. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/websocket.py +0 -0
  65. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/__init__.py +0 -0
  66. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/message.py +0 -0
  67. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/message_builder.py +0 -0
  68. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/meta.py +0 -0
  69. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/notice.py +0 -0
  70. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/request.py +0 -0
  71. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/session_type.py +0 -0
  72. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/__init__.py +0 -0
  73. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/adapter.py +0 -0
  74. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/assets/__init__.py +0 -0
  75. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/assets/error.css +0 -0
  76. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/assets/error.html +0 -0
  77. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/assets/root.css +0 -0
  78. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/assets/root.html +0 -0
  79. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/client.py +0 -0
  80. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/config.py +0 -0
  81. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/__init__.py +0 -0
  82. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/constants.py +0 -0
  83. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/locales/__init__.py +0 -0
  84. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/lifecycle.py +0 -0
  85. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/logger.py +0 -0
  86. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/master.py +0 -0
  87. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/module.py +0 -0
  88. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/router.py +0 -0
  89. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/storage.py +0 -0
  90. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/__init__.py +0 -0
  91. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/__main__.py +0 -0
  92. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/finders/__init__.py +0 -0
  93. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/finders/adapter.py +0 -0
  94. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/finders/bases/__init__.py +0 -0
  95. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/finders/bases/finder.py +0 -0
  96. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/finders/module.py +0 -0
  97. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/loaders/__init__.py +0 -0
  98. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/loaders/adapter.py +0 -0
  99. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/loaders/bases/__init__.py +0 -0
  100. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/loaders/bases/loader.py +0 -0
  101. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/loaders/module.py +0 -0
  102. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/loaders/strategy.py +0 -0
  103. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/loaders/strict.py +0 -0
  104. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/py.typed +0 -0
  105. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/__init__.py +0 -0
  106. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/config_schema.py +0 -0
  107. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/context.py +0 -0
  108. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/diagnostics.py +0 -0
  109. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/exceptions.py +0 -0
  110. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/hints.py +0 -0
  111. {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/tasks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ErisPulse
3
- Version: 2.7.1.dev2
3
+ Version: 2.7.1.dev3
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.2"
7
+ version = "2.7.1-dev.3"
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"
@@ -288,7 +288,7 @@ class BaseEventHandler:
288
288
  "elapsed_ms": round(_elapsed_0 * 1000, 2),
289
289
  "processed": event.is_processed(),
290
290
  })
291
- if event.is_processed():
291
+ if event.is_stopped():
292
292
  break
293
293
  continue
294
294
 
@@ -325,8 +325,8 @@ class BaseEventHandler:
325
325
  "handler": _h_name,
326
326
  "owner": _h_owner,
327
327
  })
328
- if copy.is_processed():
329
- event.mark_processed()
328
+ # _processed / _propagation_stopped 已由上方字段合并循环传播,
329
+ # 此处不再调用 mark_processed()(其默认会触发 _propagation_stopped 副作用)
330
330
 
331
331
  # 冲突告警:同一 field 被多个同优先级 handler 修改
332
332
  for field, mods in _modified_tracker.items():
@@ -345,7 +345,7 @@ class BaseEventHandler:
345
345
  )
346
346
  )
347
347
 
348
- if event.is_processed():
348
+ if event.is_stopped():
349
349
  break
350
350
 
351
351
  # 输出事件链路追踪日志
@@ -14,7 +14,10 @@ ErisPulse 命令处理模块
14
14
  import asyncio
15
15
  import inspect
16
16
  from collections.abc import Awaitable, Callable
17
- from typing import Any
17
+ from typing import TYPE_CHECKING, Any
18
+
19
+ if TYPE_CHECKING:
20
+ from .wrapper import Event
18
21
 
19
22
  from ...runtime import get_event_config
20
23
  from ...runtime.context import current_owner, handler_waits
@@ -375,6 +378,11 @@ class CommandHandler:
375
378
 
376
379
  :param event: 消息事件数据
377
380
  """
381
+ # 防御性归一化:确保 event 为 Event 实例,使 mark_processed 等方法可用
382
+ from .wrapper import Event as _Event
383
+
384
+ if not isinstance(event, _Event):
385
+ event = _Event(event)
378
386
 
379
387
  # 检查是否已经被其他处理器标记为已处理
380
388
  if event.get("_processed"):
@@ -399,7 +407,7 @@ class CommandHandler:
399
407
  )
400
408
  return
401
409
 
402
- async def _process_text_for_command(event: dict[str, Any], text: str) -> bool:
410
+ async def _process_text_for_command(event: "Event", text: str) -> bool:
403
411
  """
404
412
  处理文本内容,尝试匹配并执行命令
405
413
 
@@ -501,7 +509,7 @@ class CommandHandler:
501
509
  return
502
510
 
503
511
  async def _try_execute_command(
504
- self, event: dict[str, Any], original_text: str, check_text: str, prefix: str
512
+ self, event: "Event", original_text: str, check_text: str, prefix: str
505
513
  ) -> bool:
506
514
  """
507
515
  尝试执行命令
@@ -611,8 +619,8 @@ class CommandHandler:
611
619
 
612
620
  event["command"] = command_info
613
621
 
614
- # 标记事件已被处理
615
- event["_processed"] = True
622
+ # 标记事件已被处理(认领 + 阻断,阻止低优先级处理器再介入)
623
+ event.mark_processed()
616
624
 
617
625
  # 钩子: 命令匹配
618
626
  from ..lifecycle import lifecycle
@@ -694,7 +702,7 @@ class CommandHandler:
694
702
  )
695
703
  return False
696
704
 
697
- async def _check_pending_reply(self, event: dict[str, Any]):
705
+ async def _check_pending_reply(self, event: "Event"):
698
706
  """
699
707
  检查是否是等待回复的消息
700
708
 
@@ -745,8 +753,8 @@ class CommandHandler:
745
753
  # 清理等待信息
746
754
  del self._waiting_replies[wait_key]
747
755
 
748
- # 标记事件已被处理
749
- event["_processed"] = True
756
+ # 标记事件已被处理(认领 + 阻断,阻止低优先级处理器再介入)
757
+ event.mark_processed()
750
758
 
751
759
  async def _send_permission_denied(self, event: dict[str, Any]):
752
760
  """
@@ -1684,11 +1684,14 @@ class Event(dict):
1684
1684
 
1685
1685
  def to_dict(self) -> dict[str, Any]:
1686
1686
  """
1687
- 转换为字典
1687
+ 转换为字典(过滤内部键)
1688
+
1689
+ 过滤以 ``_`` 开头的内部键(如 ``_processed``/``_propagation_stopped``),
1690
+ 只返回事件数据。
1688
1691
 
1689
1692
  :return: 事件数据字典
1690
1693
  """
1691
- return dict(self)
1694
+ return {k: v for k, v in self.items() if not k.startswith("_")}
1692
1695
 
1693
1696
  def is_processed(self) -> bool:
1694
1697
  """
@@ -1698,11 +1701,71 @@ class Event(dict):
1698
1701
  """
1699
1702
  return self.get("_processed", False)
1700
1703
 
1701
- def mark_processed(self):
1704
+ def mark_processed(self, claim: bool = True, stop: bool = True):
1702
1705
  """
1703
- 标记为已处理
1706
+ 标记事件为已处理
1707
+
1708
+ 认领与阻断是两个**正交**的语义,可独立控制:
1709
+
1710
+ - **认领(claim)**:标记事件已被本处理器处理(写入 ``_processed``)。
1711
+ 命令分发器看到已认领的事件会跳过去重,避免同一消息被多个命令处理器
1712
+ 重复处理。典型场景:命令匹配成功后认领,阻止命令分发器再介入。
1713
+ - **阻断(stop)**:阻止事件向更低优先级处理器传播(写入
1714
+ ``_propagation_stopped``)。低优先级处理器(如 ``on_message``)将不再
1715
+ 看到该事件。典型场景:高优先级处理器已完整处理事件,不希望低优先级再执行。
1716
+
1717
+ 参数组合:
1718
+ - ``mark_processed()``:认领 + 阻断(默认)
1719
+ - ``mark_processed(stop=False)``:仅认领,不阻断 —— 事件继续流向低优先级观察者
1720
+ - ``mark_processed(claim=False)``:仅阻断,不认领 —— 低优先级不执行,但不做去重
1721
+
1722
+ :param claim: 是否认领事件(标记已处理,命令分发去重)(默认 True)
1723
+ :param stop: 是否阻断向低优先级处理器传播(默认 True)
1724
+
1725
+ :example:
1726
+ >>> event.mark_processed() # 认领 + 阻断(默认)
1727
+ >>> event.mark_processed(stop=False) # 仅认领,低优先级仍能看到
1728
+ >>> event.mark_processed(claim=False) # 仅阻断,不标记已处理
1729
+ """
1730
+ if claim:
1731
+ self["_processed"] = True
1732
+ if stop:
1733
+ self["_propagation_stopped"] = True
1734
+
1735
+ def done(self, claim: bool = True, stop: bool = True):
1736
+ """
1737
+ 标记事件完成(:meth:`mark_processed` 的别名)
1738
+
1739
+ 与 :meth:`mark_processed` 完全等价,提供更简洁的写法。
1740
+
1741
+ :param claim: 是否认领事件(标记已处理,命令分发去重)(默认 True)
1742
+ :param stop: 是否阻断向低优先级处理器传播(默认 True)
1743
+
1744
+ :example:
1745
+ >>> @command("help")
1746
+ ... async def help_cmd(event):
1747
+ ... event.done() # 认领 + 阻断(命令处理完的标准做法)
1748
+ >>>
1749
+ >>> @message.on_message(priority=50)
1750
+ ... async def observer(event):
1751
+ ... event.done(stop=False) # 仅认领,低优先级仍能看到(日志/统计)
1752
+ >>>
1753
+ >>> @message.on_message(priority=100)
1754
+ ... async def firewall(event):
1755
+ ... if denied(event):
1756
+ ... event.done(claim=False) # 仅阻断,不标记已处理
1757
+ """
1758
+ self.mark_processed(claim=claim, stop=stop)
1759
+
1760
+ def is_stopped(self) -> bool:
1761
+ """
1762
+ 事件传播是否已被阻断(是否已停止向低优先级处理器传播)
1763
+
1764
+ 对应 ``done(stop=True)`` / ``mark_processed(stop=True)`` 的阻断效果。
1765
+
1766
+ :return: 是否已阻断传播
1704
1767
  """
1705
- self["_processed"] = True
1768
+ return self.get("_propagation_stopped", False)
1706
1769
 
1707
1770
  # ==================== 魔术方法 ====================
1708
1771
 
@@ -635,14 +635,34 @@ DEFAULT_PROACTIVE_GC_INTERVAL_SECS: Final[int] = 300
635
635
 
636
636
  # 主动 GC 分代(generation)。
637
637
  # 运行时行为。gc.collect(generation) 只回收指定代及更年轻的对象:0=最年轻代(最轻量),
638
- # 1=中年代,2=最老代(覆盖最全)。默认 2 兼顾覆盖面与频率可控。
638
+ # 1=中年代,2=最老代(覆盖最全,等价于全量 gc.collect())。默认 0 使常规轮次保持轻量;
639
+ # 深度回收由 proactive_gc_full_every 周期性触发。
639
640
  # 修改影响: 设低减少单次扫描开销但回收不彻底,设高(2)覆盖更全但单次更重。
640
- DEFAULT_PROACTIVE_GC_GENERATION: Final[int] = 2
641
+ DEFAULT_PROACTIVE_GC_GENERATION: Final[int] = 0
641
642
 
642
643
  # 主动 GC 全量收集周期(每 N 轮做一次全量 gc.collect())。
643
644
  # 运行时行为。0 表示禁用(仅按 generation 回收),N>0 表示每 N 轮额外触发一次全量回收。
645
+ # 全量回收受 proactive_gc_memory_growth_mb 内存增长门限约束,无增长时跳过。
644
646
  # 修改影响: 设低(如 5)确保周期性深度回收,设高或 0 减少长尾暂停。
645
- DEFAULT_PROACTIVE_GC_FULL_EVERY: Final[int] = 0
647
+ DEFAULT_PROACTIVE_GC_FULL_EVERY: Final[int] = 20
648
+
649
+ # 主动 GC 全量回收的内存增长门限(MB)。
650
+ # 运行时行为。周期性全量回收前,对比上次全量后的内存基线(优先 tracemalloc,其次 RSS),
651
+ # 仅当增长超过此值才真正触发 gc.collect(),避免内存稳定时空转。0 表示不设门限(每轮都回收)。
652
+ # 修改影响: 设小更频繁深度回收(更多暂停),设大更保守(内存峰值略高但 CPU 开销更低)。
653
+ DEFAULT_PROACTIVE_GC_MEMORY_GROWTH_MB: Final[int] = 32
654
+
655
+ # 主动 GC 空闲门控(仅在事件处理器空闲时执行 Python GC)。
656
+ # 运行时行为。开启后,若 adapter 存在未完成的 pending handler task(事件洪峰),
657
+ # 本轮跳过 Python GC,避免 GC 停顿与消息处理竞争。内部资源回收不受影响。
658
+ # 修改影响: 开启降低洪峰期延迟,但可能推迟内存回收。
659
+ DEFAULT_PROACTIVE_GC_IDLE_ONLY: Final[bool] = False
660
+
661
+ # 主动 GC 常规轮次的 gen0 垃圾量下限。
662
+ # 运行时行为。gc.get_count()[0] 低于此值时跳过 Python GC(无意义空转),
663
+ # 高于此值才按 generation 回收。0 表示始终回收。
664
+ # 修改影响: 设高(如 700=跟随 Python 自动回收节奏)更多轮次被跳过,设低更积极回收。
665
+ DEFAULT_PROACTIVE_GC_GEN0_MIN: Final[int] = 500
646
666
 
647
667
  # shutdown 时等待在途事件处理器完成的最长时间(秒)。
648
668
  # 运行时行为。适配器关闭时取消所有 pending handler tasks 后等待它们退出的耐心时间。
@@ -755,8 +775,11 @@ __all__ = [
755
775
  "DEFAULT_MODULE_PRIORITY",
756
776
  "DEFAULT_OFFLINE_BOT_EXPIRY_SECS",
757
777
  "DEFAULT_PROACTIVE_GC_FULL_EVERY",
778
+ "DEFAULT_PROACTIVE_GC_GEN0_MIN",
758
779
  "DEFAULT_PROACTIVE_GC_GENERATION",
780
+ "DEFAULT_PROACTIVE_GC_IDLE_ONLY",
759
781
  "DEFAULT_PROACTIVE_GC_INTERVAL_SECS",
782
+ "DEFAULT_PROACTIVE_GC_MEMORY_GROWTH_MB",
760
783
  "DEFAULT_RATE_LIMIT_CLEANUP_INTERVAL_SECS",
761
784
  "DEFAULT_RATE_LIMIT_MAX_REQUESTS",
762
785
  "DEFAULT_RATE_LIMIT_WINDOW_SECS",
@@ -51,6 +51,8 @@ TRANSLATIONS = {
51
51
  "core.sdk.uninit.critical_error": "SDK uninitialization critical error: {error}",
52
52
  "core.sdk.uninit.client_close_failed": "Failed to close HTTP client connection pool: {error}",
53
53
  "core.sdk.gc.collected": "[GC] Python collected {collected} object(s), evicted {evicted} offline bot(s)",
54
+ "core.sdk.gc.skipped": "[GC] Round skipped (reason: {reason})",
55
+ "core.sdk.gc.config_changed": "[GC] Proactive GC config changed, restarting task",
54
56
  # ==================== SDK Environment Preparation ====================
55
57
  "core.sdk.prepare.start": "Starting initialization",
56
58
  "core.sdk.prepare.starting": "Preparing initialization environment...",
@@ -51,6 +51,8 @@ TRANSLATIONS = {
51
51
  "core.sdk.uninit.critical_error": "SDK 非初期化の重大なエラー: {error}",
52
52
  "core.sdk.uninit.client_close_failed": "HTTP クライアント接続プールのクローズに失敗しました: {error}",
53
53
  "core.sdk.gc.collected": "[GC] Python が {collected} 個のオブジェクトを回収し、{evicted} 個のオフライン Bot を削除しました",
54
+ "core.sdk.gc.skipped": "[GC] このラウンドはスキップされました(理由: {reason})",
55
+ "core.sdk.gc.config_changed": "[GC] アクティブ GC 設定が変更されたため、タスクを再起動します",
54
56
  # ==================== SDK 環境準備 ====================
55
57
  "core.sdk.prepare.start": "初期化を開始します",
56
58
  "core.sdk.prepare.starting": "初期化環境を準備しています...",
@@ -51,6 +51,8 @@ TRANSLATIONS = {
51
51
  "core.sdk.uninit.critical_error": "Критическая ошибка деинициализации SDK: {error}",
52
52
  "core.sdk.uninit.client_close_failed": "Не удалось закрыть пул соединений HTTP-клиента: {error}",
53
53
  "core.sdk.gc.collected": "[GC] Python собрал {collected} объект(ов), удалено {evicted} автономных бот(ов)",
54
+ "core.sdk.gc.skipped": "[GC] Цикл пропущен (причина: {reason})",
55
+ "core.sdk.gc.config_changed": "[GC] Конфигурация активного GC изменена, перезапуск задачи",
54
56
  # ==================== Подготовка окружения SDK ====================
55
57
  "core.sdk.prepare.start": "Начало инициализации",
56
58
  "core.sdk.prepare.starting": "Подготовка окружения инициализации...",
@@ -51,6 +51,8 @@ TRANSLATIONS = {
51
51
  "core.sdk.uninit.critical_error": "SDK反初始化严重错误: {error}",
52
52
  "core.sdk.uninit.client_close_failed": "关闭 HTTP 客户端连接池失败: {error}",
53
53
  "core.sdk.gc.collected": "[GC] Python 回收了 {collected} 个对象,清理了 {evicted} 个离线 Bot",
54
+ "core.sdk.gc.skipped": "[GC] 本轮跳过回收(原因: {reason})",
55
+ "core.sdk.gc.config_changed": "[GC] 主动 GC 配置已变更,重启回收任务",
54
56
  # ==================== SDK 环境准备 ====================
55
57
  "core.sdk.prepare.start": "开始初始化",
56
58
  "core.sdk.prepare.starting": "准备初始化环境...",
@@ -51,6 +51,8 @@ TRANSLATIONS = {
51
51
  "core.sdk.uninit.critical_error": "SDK反初始化嚴重錯誤: {error}",
52
52
  "core.sdk.uninit.client_close_failed": "關閉 HTTP 客戶端連線池失敗: {error}",
53
53
  "core.sdk.gc.collected": "[GC] Python 回收了 {collected} 個物件,清理了 {evicted} 個離線 Bot",
54
+ "core.sdk.gc.skipped": "[GC] 本輪略過回收(原因: {reason})",
55
+ "core.sdk.gc.config_changed": "[GC] 主動 GC 配置已變更,重新啟動回收任務",
54
56
  # ==================== SDK 環境準備 ====================
55
57
  "core.sdk.prepare.start": "開始初始化",
56
58
  "core.sdk.prepare.starting": "準備初始化環境...",
@@ -22,8 +22,11 @@ from ..Core.constants import (
22
22
  DEFAULT_MESSAGE_IGNORE_SELF,
23
23
  DEFAULT_OFFLINE_BOT_EXPIRY_SECS,
24
24
  DEFAULT_PROACTIVE_GC_FULL_EVERY,
25
+ DEFAULT_PROACTIVE_GC_GEN0_MIN,
25
26
  DEFAULT_PROACTIVE_GC_GENERATION,
27
+ DEFAULT_PROACTIVE_GC_IDLE_ONLY,
26
28
  DEFAULT_PROACTIVE_GC_INTERVAL_SECS,
29
+ DEFAULT_PROACTIVE_GC_MEMORY_GROWTH_MB,
27
30
  DEFAULT_SERVER_AUTO_START,
28
31
  DEFAULT_SERVER_HOST,
29
32
  DEFAULT_SERVER_PORT,
@@ -83,6 +86,9 @@ DEFAULT_ERISPULSE_CONFIG = {
83
86
  "proactive_gc_interval": DEFAULT_PROACTIVE_GC_INTERVAL_SECS,
84
87
  "proactive_gc_generation": DEFAULT_PROACTIVE_GC_GENERATION,
85
88
  "proactive_gc_full_every": DEFAULT_PROACTIVE_GC_FULL_EVERY,
89
+ "proactive_gc_memory_growth_mb": DEFAULT_PROACTIVE_GC_MEMORY_GROWTH_MB,
90
+ "proactive_gc_idle_only": DEFAULT_PROACTIVE_GC_IDLE_ONLY,
91
+ "proactive_gc_gen0_min": DEFAULT_PROACTIVE_GC_GEN0_MIN,
86
92
  "offline_bot_expiry": DEFAULT_OFFLINE_BOT_EXPIRY_SECS,
87
93
  },
88
94
  "i18n": {
@@ -12,6 +12,7 @@
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
+ from collections import OrderedDict
15
16
  from typing import Any
16
17
 
17
18
  __all__ = [
@@ -21,8 +22,12 @@ __all__ = [
21
22
  "snapshot",
22
23
  ]
23
24
 
24
- # label 上一次快照的 RSS(MB),用于计算增量
25
- _prev_rss: dict[str, float] = {}
25
+ # 快照标签的最大缓存数。防止使用动态 label 调用 snapshot() 时
26
+ # _prev_rss 无限增长,超出部分按最早写入顺序淘汰。
27
+ _MAX_SNAPSHOT_LABELS = 128
28
+
29
+ # 各 label 上一次快照的 RSS(MB),用于计算增量。按写入顺序淘汰。
30
+ _prev_rss: OrderedDict[str, float] = OrderedDict()
26
31
 
27
32
 
28
33
  def get_rss_mb() -> float | None:
@@ -81,10 +86,14 @@ def snapshot(label: str = "") -> dict[str, Any]:
81
86
 
82
87
  delta: float | None = None
83
88
  key = label or "__default__"
84
- if rss is not None and key in _prev_rss:
85
- delta = rss - _prev_rss[key]
86
89
  if rss is not None:
90
+ if key in _prev_rss:
91
+ delta = rss - _prev_rss[key]
92
+ # 记录并限制缓存上限,避免动态标签导致无界增长
87
93
  _prev_rss[key] = rss
94
+ _prev_rss.move_to_end(key)
95
+ while len(_prev_rss) > _MAX_SNAPSHOT_LABELS:
96
+ _prev_rss.popitem(last=False)
88
97
 
89
98
  return {
90
99
  "label": label,
@@ -24,8 +24,11 @@ from typing import TYPE_CHECKING, Any
24
24
 
25
25
  from .Core.constants import (
26
26
  DEFAULT_PROACTIVE_GC_FULL_EVERY,
27
+ DEFAULT_PROACTIVE_GC_GEN0_MIN,
27
28
  DEFAULT_PROACTIVE_GC_GENERATION,
29
+ DEFAULT_PROACTIVE_GC_IDLE_ONLY,
28
30
  DEFAULT_PROACTIVE_GC_INTERVAL_SECS,
31
+ DEFAULT_PROACTIVE_GC_MEMORY_GROWTH_MB,
29
32
  DEFAULT_UNINIT_TIMEOUT_SECS,
30
33
  HARD_RESTART_EXIT_CODE,
31
34
  LIFECYCLE_TIMER_CORE_INIT,
@@ -193,6 +196,7 @@ class SDK:
193
196
  self._initializer: SDK.Initializer | None = None
194
197
  self._initialized: bool = False
195
198
  self._gc_task: asyncio.Task | None = None # 主动 GC 后台任务
199
+ self._gc_config_snapshot: tuple | None = None # 主动 GC 配置快照(变更检测)
196
200
  self._shutdown_event: asyncio.Event | None = None # 优雅关闭事件
197
201
 
198
202
  @property
@@ -753,6 +757,15 @@ class SDK:
753
757
  adapter_manager.clear()
754
758
  module_manager.clear()
755
759
 
760
+ # 6.5 立即回收一次,尽早释放模块/适配器实例与循环引用,
761
+ # 降低 uninit 与 hard_restart 之间的内存驻留
762
+ try:
763
+ import gc
764
+
765
+ gc.collect()
766
+ except Exception:
767
+ pass
768
+
756
769
  # 获取清理耗时
757
770
  uninit_duration = self.lifecycle.stop_timer(LIFECYCLE_TIMER_CORE_UNINIT)
758
771
 
@@ -871,66 +884,97 @@ class SDK:
871
884
  定期执行 Python GC 和内部资源回收(离线 Bot 清理等),
872
885
  防止长期运行时的内存增长。
873
886
 
874
- GC 行为由三项框架配置控制(均支持热更新):
887
+ GC 行为由多项框架配置控制(均支持热更新,变更时即时重启任务):
875
888
 
876
889
  - ``proactive_gc_interval``: 回收间隔秒数(0 禁用)
877
- - ``proactive_gc_generation``: 回收分代(0/1/2,默认 2=最老代)
878
- - ``proactive_gc_full_every``: 每 N 轮额外做一次全量回收(0 禁用)
890
+ - ``proactive_gc_generation``: 常规轮次回收分代(0/1/2,钳制到 0..2
891
+ - ``proactive_gc_full_every``: 每 N 轮做一次全量回收(0 禁用)
892
+ - ``proactive_gc_memory_growth_mb``: 全量回收的内存增长门限(0 不设限)
893
+ - ``proactive_gc_idle_only``: 事件洪峰时跳过 Python GC(避免停顿竞争)
894
+ - ``proactive_gc_gen0_min``: gen0 垃圾量下限,低于则跳过回收(空转轮次零开销)
879
895
 
880
896
  初始化阶段已调用 ``gc.freeze()`` 将框架对象移入永久代,
881
897
  此处 ``gc.collect()`` 仅扫描运行期新建对象。
882
898
  """
883
- # 停止已有的 GC 任务
899
+ # 停止已有的 GC 任务(含反注册配置钩子)
884
900
  self._stop_proactive_gc()
885
901
 
886
- def _read_gc_config() -> tuple[int, int, int]:
887
- interval = DEFAULT_PROACTIVE_GC_INTERVAL_SECS
888
- generation = DEFAULT_PROACTIVE_GC_GENERATION
889
- full_every = DEFAULT_PROACTIVE_GC_FULL_EVERY
890
- try:
891
- from .runtime import get_framework_config
892
-
893
- fw = get_framework_config()
894
- interval = fw.get("proactive_gc_interval", interval)
895
- generation = fw.get("proactive_gc_generation", generation)
896
- full_every = fw.get("proactive_gc_full_every", full_every)
897
- except Exception:
898
- pass
899
- return interval, generation, full_every
902
+ # 注册配置变更钩子:proactive_gc_* 变化时即时重启任务。
903
+ # 修复旧实现"interval 切到 0 后无法再启用"的问题。
904
+ try:
905
+ self.lifecycle.register("config.set", self._on_gc_config_event)
906
+ self.lifecycle.register("config.updated", self._on_gc_config_event)
907
+ except Exception:
908
+ pass
900
909
 
901
- if _read_gc_config()[0] <= 0:
910
+ self._gc_config_snapshot = self._read_gc_config()
911
+ if self._gc_config_snapshot[0] <= 0:
902
912
  return # 配置禁用
903
913
 
904
914
  async def _gc_loop():
905
915
  import gc
906
916
 
907
917
  round_count = 0
918
+ memory_baseline: float | None = None
908
919
  while True:
909
920
  try:
910
- # 每轮重新读取配置,支持热更新
911
- interval, generation, full_every = _read_gc_config()
921
+ # 每轮重新读取配置,支持热更新;常规变更已由配置钩子即时重启任务
922
+ cfg = self._read_gc_config()
923
+ interval = cfg[0]
912
924
  if interval <= 0:
913
925
  # 配置运行时禁用了 GC,停止循环
914
926
  break
915
927
  await asyncio.sleep(interval)
916
928
  round_count += 1
917
- # 1. Python GC —— 按分代回收,可选周期性全量
918
- do_full = full_every > 0 and round_count % full_every == 0
919
- if do_full:
920
- collected = gc.collect()
921
- else:
922
- collected = gc.collect(generation)
923
- # 2. 内部资源回收
929
+
930
+ # 1. 内部资源回收(廉价,始终执行)
931
+ evicted = 0
924
932
  try:
925
- adapter_mgr = self.adapter
926
- evicted = adapter_mgr._evict_offline_bots()
927
- if collected > 0 or evicted > 0:
928
- self.logger.trace(
929
- i18n.t("core.sdk.gc.collected", collected=collected, evicted=evicted)
930
- )
933
+ evicted = self.adapter._evict_offline_bots()
931
934
  except Exception:
932
935
  pass
933
- # 3. 内存快照(TRACE),便于长期观察内存变化趋势
936
+
937
+ # 2. 空闲门控:事件洪峰时跳过 Python GC,避免停顿与消息处理竞争
938
+ if cfg[4] and self._has_handler_backlog():
939
+ self.logger.trace(
940
+ i18n.t("core.sdk.gc.skipped", reason="busy")
941
+ )
942
+ if evicted > 0:
943
+ self.logger.trace(
944
+ i18n.t(
945
+ "core.sdk.gc.collected", collected=0, evicted=evicted
946
+ )
947
+ )
948
+ continue
949
+
950
+ # 3. 垃圾量门控:gen0 无足够垃圾时跳过回收(空转轮次近乎零开销)
951
+ if gc.get_count()[0] < cfg[5]:
952
+ if evicted > 0:
953
+ self.logger.trace(
954
+ i18n.t(
955
+ "core.sdk.gc.collected", collected=0, evicted=evicted
956
+ )
957
+ )
958
+ continue
959
+
960
+ # 4. 周期性全量(受内存增长门限约束)或常规分代回收
961
+ is_full = cfg[2] > 0 and round_count % cfg[2] == 0
962
+ if is_full:
963
+ collected, memory_baseline = self._run_full_gc_collection(
964
+ gc, memory_baseline, cfg[3]
965
+ )
966
+ else:
967
+ collected = gc.collect(cfg[1])
968
+
969
+ if collected > 0 or evicted > 0:
970
+ self.logger.trace(
971
+ i18n.t(
972
+ "core.sdk.gc.collected",
973
+ collected=collected,
974
+ evicted=evicted,
975
+ )
976
+ )
977
+ # 5. 内存快照(TRACE),便于长期观察内存变化趋势
934
978
  try:
935
979
  from .runtime.memory import log_snapshot
936
980
 
@@ -951,12 +995,148 @@ class SDK:
951
995
  def _stop_proactive_gc(self) -> None:
952
996
  """
953
997
  {!--< internal-use >!--}
954
- 停止主动 GC 后台任务
998
+ 停止主动 GC 后台任务,并反注册配置变更钩子
955
999
  """
1000
+ try:
1001
+ self.lifecycle.unregister("config.set", self._on_gc_config_event)
1002
+ self.lifecycle.unregister("config.updated", self._on_gc_config_event)
1003
+ except Exception:
1004
+ pass
1005
+ self._gc_config_snapshot = None
956
1006
  if self._gc_task is not None and not self._gc_task.done():
957
1007
  self._gc_task.cancel()
958
1008
  self._gc_task = None
959
1009
 
1010
+ @staticmethod
1011
+ def _read_gc_config() -> tuple[float, int, int, int, bool, int]:
1012
+ """
1013
+ {!--< internal-use >!--}
1014
+ 读取并钳制主动 GC 相关框架配置
1015
+
1016
+ :return: ``(interval, generation, full_every, growth_mb, idle_only, gen0_min)``
1017
+ 元组,值均已钳制到合法范围;``interval`` 为秒(支持小数)
1018
+ """
1019
+ interval = DEFAULT_PROACTIVE_GC_INTERVAL_SECS
1020
+ generation = DEFAULT_PROACTIVE_GC_GENERATION
1021
+ full_every = DEFAULT_PROACTIVE_GC_FULL_EVERY
1022
+ growth_mb = DEFAULT_PROACTIVE_GC_MEMORY_GROWTH_MB
1023
+ idle_only = DEFAULT_PROACTIVE_GC_IDLE_ONLY
1024
+ gen0_min = DEFAULT_PROACTIVE_GC_GEN0_MIN
1025
+ try:
1026
+ from .runtime import get_framework_config
1027
+
1028
+ fw = get_framework_config()
1029
+ raw_interval = fw.get("proactive_gc_interval", interval)
1030
+ interval = float(raw_interval) if raw_interval is not None else interval
1031
+ generation = int(fw.get("proactive_gc_generation", generation))
1032
+ full_every = int(fw.get("proactive_gc_full_every", full_every))
1033
+ growth_mb = int(fw.get("proactive_gc_memory_growth_mb", growth_mb))
1034
+ idle_only = bool(fw.get("proactive_gc_idle_only", idle_only))
1035
+ gen0_min = int(fw.get("proactive_gc_gen0_min", gen0_min))
1036
+ except Exception:
1037
+ pass
1038
+ return (
1039
+ max(0.0, float(interval)),
1040
+ max(0, min(2, generation)),
1041
+ max(0, full_every),
1042
+ max(0, growth_mb),
1043
+ idle_only,
1044
+ max(0, gen0_min),
1045
+ )
1046
+
1047
+ def _on_gc_config_event(self, _data: dict) -> None:
1048
+ """
1049
+ {!--< internal-use >!--}
1050
+ ``config.set`` / ``config.updated`` 回调:proactive_gc_* 配置变化时重启 GC 任务
1051
+
1052
+ 相比旧实现"每轮重读",此钩子使配置变更(含 0→N 重新启用)即时生效。
1053
+ 从后台线程(如 config watcher)触发时,调度回主事件循环再重启。
1054
+ """
1055
+ try:
1056
+ current = self._read_gc_config()
1057
+ if (
1058
+ self._gc_config_snapshot is None
1059
+ or current == self._gc_config_snapshot
1060
+ ):
1061
+ return
1062
+ except Exception:
1063
+ return
1064
+
1065
+ def _restart() -> None:
1066
+ try:
1067
+ self._start_proactive_gc()
1068
+ except Exception:
1069
+ pass
1070
+
1071
+ try:
1072
+ # 当前线程有运行中的事件循环:直接重启
1073
+ asyncio.get_running_loop()
1074
+ _restart()
1075
+ except RuntimeError:
1076
+ # 非事件循环线程(如 config watcher):调度回主循环执行
1077
+ from .runtime.tasks import _get_main_loop
1078
+
1079
+ main_loop = _get_main_loop()
1080
+ if main_loop is not None and main_loop.is_running():
1081
+ main_loop.call_soon_threadsafe(_restart)
1082
+ else:
1083
+ # 无可用主循环:跳过本次重启,循环自身每轮重读配置兜底
1084
+ pass
1085
+
1086
+ @staticmethod
1087
+ def _has_handler_backlog() -> bool:
1088
+ """
1089
+ {!--< internal-use >!--}
1090
+ 事件处理器洪峰检测
1091
+
1092
+ :return: 存在未完成的 pending handler task 时返回 True
1093
+ """
1094
+ try:
1095
+ from .Core.adapter import adapter as _adapter
1096
+
1097
+ return len(getattr(_adapter, "_pending_handler_tasks", ())) > 0
1098
+ except Exception:
1099
+ return False
1100
+
1101
+ @staticmethod
1102
+ def _run_full_gc_collection(
1103
+ gc_module: Any, baseline: float | None, growth_mb: int
1104
+ ) -> tuple[int, float | None]:
1105
+ """
1106
+ {!--< internal-use >!--}
1107
+ 执行一次全量回收(受内存增长门限约束)
1108
+
1109
+ 优先使用 tracemalloc 追踪值,不可用则回退 RSS。当 ``growth_mb > 0``
1110
+ 且距上次全量回收基线增长不足时跳过回收,避免内存稳定时空转。
1111
+
1112
+ :param gc_module: ``gc`` 模块(便于测试注入)
1113
+ :param baseline: 上次全量回收后的内存基线(MB),None 表示首次
1114
+ :param growth_mb: 内存增长门限(MB),0 表示不设门限
1115
+ :return: ``(collected, 新基线)``
1116
+ """
1117
+ current: float | None = None
1118
+ try:
1119
+ from .runtime.memory import get_rss_mb, get_traced_mb
1120
+
1121
+ current = get_traced_mb()
1122
+ if current is None:
1123
+ current = get_rss_mb()
1124
+ except Exception:
1125
+ pass
1126
+
1127
+ if current is not None:
1128
+ if growth_mb > 0 and baseline is not None:
1129
+ # 内存稳定:增长未达门限,跳过本次全量回收
1130
+ if (current - baseline) < growth_mb:
1131
+ return 0, baseline
1132
+ baseline = current
1133
+
1134
+ try:
1135
+ collected = gc_module.collect()
1136
+ except Exception:
1137
+ collected = 0
1138
+ return collected, baseline
1139
+
960
1140
  def dump_state(self) -> dict:
961
1141
  """
962
1142
  导出框架当前运行状态的快照
File without changes