ErisPulse 2.7.0.dev5__tar.gz → 2.7.1.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 (111) hide show
  1. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/PKG-INFO +1 -1
  2. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/pyproject.toml +1 -1
  3. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/commands/create.py +46 -13
  4. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Bases/config_schema.py +52 -0
  5. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Event/wrapper.py +8 -7
  6. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/adapter.py +9 -23
  7. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/config.py +24 -14
  8. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/constants.py +13 -0
  9. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/module.py +9 -21
  10. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/runtime/__init__.py +6 -0
  11. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/runtime/frame_config.py +4 -0
  12. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/sdk.py +41 -11
  13. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/.gitignore +0 -0
  14. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/LICENSE +0 -0
  15. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/README.pypi.md +0 -0
  16. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/__init__.py +0 -0
  17. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/base.py +0 -0
  18. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/cli.py +0 -0
  19. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/commands/__init__.py +0 -0
  20. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/commands/doctor.py +0 -0
  21. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/commands/init.py +0 -0
  22. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/commands/install.py +0 -0
  23. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/commands/language.py +0 -0
  24. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/commands/list.py +0 -0
  25. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/commands/list_remote.py +0 -0
  26. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/commands/run.py +0 -0
  27. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/commands/self_update.py +0 -0
  28. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/commands/types.py +0 -0
  29. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/commands/uninstall.py +0 -0
  30. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/commands/upgrade.py +0 -0
  31. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/console.py +0 -0
  32. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/constants.py +0 -0
  33. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/hints.py +0 -0
  34. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/i18n/__init__.py +0 -0
  35. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/i18n/locales/__init__.py +0 -0
  36. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/i18n/locales/en.py +0 -0
  37. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/i18n/locales/ja.py +0 -0
  38. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/i18n/locales/ru.py +0 -0
  39. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/i18n/locales/zh_cn.py +0 -0
  40. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/i18n/locales/zh_tw.py +0 -0
  41. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/registry.py +0 -0
  42. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/utils/__init__.py +0 -0
  43. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/utils/display.py +0 -0
  44. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/utils/file_watcher.py +0 -0
  45. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/utils/package_manager.py +0 -0
  46. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/CLI/utils/scaffold_text.py +0 -0
  47. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Bases/__init__.py +0 -0
  48. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Bases/adapter.py +0 -0
  49. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Bases/client.py +0 -0
  50. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Bases/converter.py +0 -0
  51. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Bases/errors.py +0 -0
  52. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Bases/i18n_schema.py +0 -0
  53. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Bases/kv_builder.py +0 -0
  54. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Bases/manager.py +0 -0
  55. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Bases/module.py +0 -0
  56. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Bases/router.py +0 -0
  57. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Bases/send_builder.py +0 -0
  58. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Bases/send_rules.py +0 -0
  59. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Bases/storage.py +0 -0
  60. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Bases/websocket.py +0 -0
  61. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Event/__init__.py +0 -0
  62. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Event/base.py +0 -0
  63. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Event/command.py +0 -0
  64. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Event/message.py +0 -0
  65. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Event/message_builder.py +0 -0
  66. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Event/meta.py +0 -0
  67. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Event/notice.py +0 -0
  68. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Event/request.py +0 -0
  69. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/Event/session_type.py +0 -0
  70. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/__init__.py +0 -0
  71. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/assets/__init__.py +0 -0
  72. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/assets/error.css +0 -0
  73. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/assets/error.html +0 -0
  74. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/assets/root.css +0 -0
  75. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/assets/root.html +0 -0
  76. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/client.py +0 -0
  77. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/i18n/__init__.py +0 -0
  78. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/i18n/constants.py +0 -0
  79. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/i18n/locales/__init__.py +0 -0
  80. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/i18n/locales/en.py +0 -0
  81. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/i18n/locales/ja.py +0 -0
  82. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/i18n/locales/ru.py +0 -0
  83. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/i18n/locales/zh_cn.py +0 -0
  84. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/i18n/locales/zh_tw.py +0 -0
  85. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/lifecycle.py +0 -0
  86. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/logger.py +0 -0
  87. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/master.py +0 -0
  88. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/router.py +0 -0
  89. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/Core/storage.py +0 -0
  90. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/__init__.py +0 -0
  91. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/__main__.py +0 -0
  92. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/finders/__init__.py +0 -0
  93. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/finders/adapter.py +0 -0
  94. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/finders/bases/__init__.py +0 -0
  95. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/finders/bases/finder.py +0 -0
  96. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/finders/module.py +0 -0
  97. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/loaders/__init__.py +0 -0
  98. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/loaders/adapter.py +0 -0
  99. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/loaders/bases/__init__.py +0 -0
  100. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/loaders/bases/loader.py +0 -0
  101. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/loaders/module.py +0 -0
  102. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/loaders/strategy.py +0 -0
  103. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/loaders/strict.py +0 -0
  104. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/py.typed +0 -0
  105. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/runtime/config_schema.py +0 -0
  106. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/runtime/context.py +0 -0
  107. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/runtime/diagnostics.py +0 -0
  108. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/runtime/exceptions.py +0 -0
  109. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/runtime/hints.py +0 -0
  110. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/src/ErisPulse/runtime/memory.py +0 -0
  111. {erispulse-2.7.0.dev5 → erispulse-2.7.1.dev0}/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.0.dev5
3
+ Version: 2.7.1.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.0-dev.5"
7
+ version = "2.7.1-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"
@@ -462,33 +462,50 @@ class {converter_name}(BaseConverter):
462
462
  return None
463
463
  """
464
464
 
465
- _README_MODULE = """# {name}
465
+ _README_MODULE = """<div align="center">
466
466
 
467
- {description}
467
+ <img src="https://raw.githubusercontent.com/ErisPulse/ErisPulse/main/.github/assets/ErisPulseLogo.png" width="180" alt="{name}" />
468
+
469
+ # {name}
470
+
471
+ **{description}**
472
+
473
+ <p>
474
+ <a href="https://pypi.org/project/ErisPulse-{name}/"><img src="https://img.shields.io/pypi/v/ErisPulse-{name}?style=for-the-badge&logo=pypi&logoColor=white" alt="PyPI"></a>
475
+ <a href="https://pypi.org/project/ErisPulse-{name}/"><img src="https://img.shields.io/badge/Python-3.10+-FFD43B?style=for-the-badge&logo=python&logoColor=blue" alt="Python"></a>
476
+ <a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue?style=for-the-badge" alt="License"></a>
477
+ <a href="https://github.com/ErisPulse/ErisPulse"><img src="https://img.shields.io/badge/Powered_by-ErisPulse-FF6B9D?style=for-the-badge&logo=bookstack&logoColor=white" alt="ErisPulse"></a>
478
+ </p>
479
+
480
+ </div>
481
+
482
+ ---
468
483
 
469
484
  ## 安装
470
485
 
471
486
  ```bash
472
487
  epsdk install ErisPulse-{name}
473
488
  ```
489
+ """
474
490
 
475
- ## 使用
491
+ _README_ADAPTER = """<div align="center">
476
492
 
477
- 模块会自动加载,你也可以通过 `sdk.{name}` 访问模块实例。
493
+ <img src="https://raw.githubusercontent.com/ErisPulse/ErisPulse/main/.github/assets/ErisPulseLogo.png" width="180" alt="{name}" />
478
494
 
479
- ## 配置
495
+ # {name}
480
496
 
481
- 在 `config.toml` 中添加:
497
+ **{description}**
482
498
 
483
- ```toml
484
- [{name}]
485
- enabled = true
486
- ```
487
- """
499
+ <p>
500
+ <a href="https://pypi.org/project/ErisPulse-{name}/"><img src="https://img.shields.io/pypi/v/ErisPulse-{name}?style=for-the-badge&logo=pypi&logoColor=white" alt="PyPI"></a>
501
+ <a href="https://pypi.org/project/ErisPulse-{name}/"><img src="https://img.shields.io/badge/Python-3.10+-FFD43B?style=for-the-badge&logo=python&logoColor=blue" alt="Python"></a>
502
+ <a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue?style=for-the-badge" alt="License"></a>
503
+ <a href="https://github.com/ErisPulse/ErisPulse"><img src="https://img.shields.io/badge/Powered_by-ErisPulse-FF6B9D?style=for-the-badge&logo=bookstack&logoColor=white" alt="ErisPulse"></a>
504
+ </p>
488
505
 
489
- _README_ADAPTER = """# {name}
506
+ </div>
490
507
 
491
- {description}
508
+ ---
492
509
 
493
510
  ## 安装
494
511
 
@@ -561,6 +578,22 @@ def _validate_name(name: str) -> bool:
561
578
  return all(c.isalnum() or c == "_" for c in name)
562
579
 
563
580
 
581
+ def _copy_erispulse_logo(project_dir: Path) -> None:
582
+ """
583
+ 将 ErisPulseLogo.png 拷贝到项目的 .github/assets/ 目录
584
+
585
+ :param project_dir: [Path] 项目根目录
586
+ """
587
+ import shutil
588
+
589
+ logo_src = Path(__file__).parent.parent / "assets" / "ErisPulseLogo.png"
590
+ if not logo_src.exists():
591
+ return
592
+ dest = project_dir / ".github" / "assets" / "ErisPulseLogo.png"
593
+ dest.parent.mkdir(parents=True, exist_ok=True)
594
+ shutil.copy2(logo_src, dest)
595
+
596
+
564
597
  def _scaffold_text(name: str) -> dict:
565
598
  """
566
599
  构建当前语言的脚手架文案映射,并预填充 {name} 占位符
@@ -424,6 +424,58 @@ def dict_to_dataclass(config_class: type, data: dict):
424
424
  return config_class(**kwargs)
425
425
 
426
426
 
427
+ def _notify_instance_config_update(
428
+ instance: Any,
429
+ old_dict: dict | None,
430
+ new_dict: dict | None,
431
+ *,
432
+ i18n_key: str,
433
+ log_params: dict,
434
+ ) -> None:
435
+ """
436
+ 调用实例的 ``on_config_update`` 回调,传入类型安全的配置对象
437
+
438
+ 若实例声明了 ``ConfigClass``,则将字典通过 :func:`dict_to_dataclass`
439
+ 转换为 dataclass 实例;否则原样传入字典。回调中抛出的异常会被捕获
440
+ 并按指定的 i18n 键记录日志,不会向上传播。
441
+
442
+ 供 ``ModuleManager`` 与 ``AdapterManager`` 的配置热更新路由共用,
443
+ 避免在两处重复实现字典→dataclass 转换 + 异常兜底逻辑。
444
+
445
+ {!--< internal-use >!--}
446
+ {!--< /internal-use >!--}
447
+
448
+ :param instance: 模块/适配器实例(需实现 ``on_config_update``)
449
+ :param old_dict: 变更前的配置字典(可能为 None)
450
+ :param new_dict: 变更后的配置字典(可能为 None)
451
+ :param i18n_key: 回调异常日志的 i18n 键(如 ``core.module.config_update_failed``)
452
+ :param log_params: 异常日志的额外格式化参数(如 ``{"name": "MyModule"}``)
453
+ """
454
+ config_class = getattr(instance, "ConfigClass", None)
455
+ try:
456
+ if config_class is not None:
457
+ old_config = (
458
+ dict_to_dataclass(config_class, old_dict) if old_dict else None
459
+ )
460
+ new_config = (
461
+ dict_to_dataclass(config_class, new_dict) if new_dict else None
462
+ )
463
+ else:
464
+ old_config = old_dict
465
+ new_config = new_dict
466
+ instance.on_config_update(old_config, new_config)
467
+ except Exception as e:
468
+ try:
469
+ from ErisPulse.Core.i18n import i18n
470
+ from ErisPulse.Core.logger import logger
471
+
472
+ params = dict(log_params)
473
+ params["error"] = e
474
+ logger.error(i18n.t(i18n_key, **params))
475
+ except Exception:
476
+ pass
477
+
478
+
427
479
  def validate_config(instance) -> list[str]:
428
480
  """
429
481
  校验 dataclass 实例
@@ -1576,8 +1576,8 @@ class Event(dict):
1576
1576
  future.set_result(raw)
1577
1577
  except asyncio.InvalidStateError:
1578
1578
  pass
1579
- except Exception:
1580
- pass
1579
+ except Exception as _e:
1580
+ logger.trace(f"[Event] onebot wait handler 异常: {_e}")
1581
1581
 
1582
1582
  handler_wrapper = {"func": _temp_handler, "platform": None}
1583
1583
  adapter._onebot_handlers[event_type].append(handler_wrapper)
@@ -1919,7 +1919,8 @@ class Conversation:
1919
1919
  try:
1920
1920
  if not cond(self.context):
1921
1921
  continue
1922
- except Exception:
1922
+ except Exception as _e:
1923
+ logger.trace(f"[Conversation] 字段 condition 执行异常,跳过: {_e}")
1923
1924
  continue
1924
1925
  filtered_fields.append(f)
1925
1926
 
@@ -2115,8 +2116,8 @@ class Conversation:
2115
2116
  if event:
2116
2117
  self._event = event
2117
2118
  return True
2118
- except Exception:
2119
- pass
2119
+ except Exception as _e:
2120
+ logger.trace(f"[Conversation] resume 失败: {_e}")
2120
2121
  return False
2121
2122
 
2122
2123
  async def clear_saved(self):
@@ -2133,8 +2134,8 @@ class Conversation:
2133
2134
  platform = self._event.get_platform()
2134
2135
  key = f"{CONVERSATION_KEY_PREFIX}:{platform}:{user_id}"
2135
2136
  storage.delete(key)
2136
- except Exception:
2137
- pass
2137
+ except Exception as _e:
2138
+ logger.trace(f"[Conversation] clear_saved 失败: {_e}")
2138
2139
 
2139
2140
 
2140
2141
  __all__ = [
@@ -235,29 +235,15 @@ class AdapterManager(ManagerBase):
235
235
  {!--< internal-use >!--}
236
236
  调用适配器的 on_config_update 回调,传入类型安全的配置对象
237
237
  """
238
- config_class = getattr(instance, "ConfigClass", None)
239
- try:
240
- if config_class is not None:
241
- from ..runtime.config_schema import dict_to_dataclass
242
-
243
- old_config = (
244
- dict_to_dataclass(config_class, old_dict) if old_dict else None
245
- )
246
- new_config = (
247
- dict_to_dataclass(config_class, new_dict) if new_dict else None
248
- )
249
- else:
250
- old_config = old_dict
251
- new_config = new_dict
252
- instance.on_config_update(old_config, new_config)
253
- except Exception as e:
254
- logger.error(
255
- i18n.t(
256
- "core.adapter.config_update_failed",
257
- platform=platform or instance.__class__.__name__,
258
- error=e,
259
- )
260
- )
238
+ from ..Core.Bases.config_schema import _notify_instance_config_update
239
+
240
+ _notify_instance_config_update(
241
+ instance,
242
+ old_dict,
243
+ new_dict,
244
+ i18n_key="core.adapter.config_update_failed",
245
+ log_params={"platform": platform or instance.__class__.__name__},
246
+ )
261
247
 
262
248
  # ==================== 适配器注册与管理 ====================
263
249
 
@@ -50,6 +50,7 @@ class ConfigManager:
50
50
  self._lock = threading.RLock() # 线程安全锁
51
51
  self._file_lock = threading.RLock() # 文件操作锁
52
52
  self._atexit_registered = False # atexit 钩子注册标记
53
+ self._last_self_write_mtime: float = 0.0 # 框架自身最后一次刷盘的 mtime
53
54
  self._migrate_config() # 迁移旧配置文件
54
55
  self._load_config() # 初始化时加载配置
55
56
  self._watch_config_file() # 记录配置文件 mtime 以便后续监听
@@ -77,19 +78,21 @@ class ConfigManager:
77
78
  if self._watcher_stop.is_set():
78
79
  break
79
80
  try:
80
- if self._check_file_change():
81
- # 文件被外部修改 → 取消待写入定时器并丢弃脏键,
82
- # 避免 _flush_config 回写旧值覆盖用户编辑。
83
- if self._write_timer:
84
- self._write_timer.cancel()
85
- self._write_timer = None
86
- self._dirty_keys.clear()
87
- with self._lock:
88
- old_cache = self._cache.copy() if self._cache else {}
89
- # 仅在成功加载时广播变更,半成品/语法错误的 TOML
90
- # 不再以空配置形式触发 config.updated
91
- if self._load_config():
92
- self._emit_config_updated(old_cache)
81
+ with self._lock:
82
+ # _check_file_change 已能区分"框架自身刷盘"与"外部修改":
83
+ # 自身刷盘的 mtime 与 _last_self_write_mtime 一致 → 返回 False
84
+ if not self._check_file_change():
85
+ continue
86
+ # 真正的外部修改:重载文件到缓存。
87
+ # 不清空 _dirty_keys、不取消 _write_timer —— 待写键会在
88
+ # 下次 _flush_config 时与外部内容合并(脏键优先),
89
+ # 避免丢失本进程尚未落盘的写入。
90
+ old_cache = self._cache.copy() if self._cache else {}
91
+ loaded = self._load_config()
92
+ # 仅在成功加载时广播变更,半成品/语法错误的 TOML
93
+ # 不再以空配置形式触发 config.updated
94
+ if loaded:
95
+ self._emit_config_updated(old_cache)
93
96
  except Exception as e:
94
97
  # 监听异常不再静默吞掉,便于排查 watcher 故障
95
98
  self._log_config_error(
@@ -365,6 +368,7 @@ class ConfigManager:
365
368
  # 从而重复触发 config.updated(与 config.set 路由重复调用 on_config_update)
366
369
  try:
367
370
  self._config_mtime = Path(self.CONFIG_FILE).stat().st_mtime
371
+ self._last_self_write_mtime = self._config_mtime
368
372
  except OSError:
369
373
  pass
370
374
 
@@ -504,8 +508,10 @@ class ConfigManager:
504
508
  检测配置文件是否被外部程序或用户手动编辑
505
509
 
506
510
  对比记录的 mtime 与当前文件 mtime,若不一致说明文件已被外部修改。
511
+ 若变化后的 mtime 与框架自身最后一次刷盘的 mtime(``_last_self_write_mtime``)
512
+ 一致,则判定为框架自身的写入而非外部修改,返回 False。
507
513
 
508
- :return: bool 文件是否已变化
514
+ :return: bool 文件是否被外部修改
509
515
 
510
516
  {!--< internal-use >!--}
511
517
  {!--< /internal-use >!--}
@@ -517,6 +523,10 @@ class ConfigManager:
517
523
  current_mtime = config_path.stat().st_mtime
518
524
  if current_mtime != self._config_mtime:
519
525
  self._config_mtime = current_mtime
526
+ # 若 mtime 与框架自身最后一次刷盘一致,说明是 _flush_config 的
527
+ # 写入,不算外部修改(避免 watcher 误触发重载)
528
+ if current_mtime == self._last_self_write_mtime:
529
+ return False
520
530
  return True
521
531
  except OSError:
522
532
  pass
@@ -633,6 +633,17 @@ DEFAULT_OFFLINE_BOT_EXPIRY_SECS: Final[int] = 3600
633
633
  # 修改影响: 设大减少 CPU 中断但内存峰值更高,设小更频繁回收但增加开销。0 表示禁用。
634
634
  DEFAULT_PROACTIVE_GC_INTERVAL_SECS: Final[int] = 300
635
635
 
636
+ # 主动 GC 分代(generation)。
637
+ # 运行时行为。gc.collect(generation) 只回收指定代及更年轻的对象:0=最年轻代(最轻量),
638
+ # 1=中年代,2=最老代(覆盖最全)。默认 2 兼顾覆盖面与频率可控。
639
+ # 修改影响: 设低减少单次扫描开销但回收不彻底,设高(2)覆盖更全但单次更重。
640
+ DEFAULT_PROACTIVE_GC_GENERATION: Final[int] = 2
641
+
642
+ # 主动 GC 全量收集周期(每 N 轮做一次全量 gc.collect())。
643
+ # 运行时行为。0 表示禁用(仅按 generation 回收),N>0 表示每 N 轮额外触发一次全量回收。
644
+ # 修改影响: 设低(如 5)确保周期性深度回收,设高或 0 减少长尾暂停。
645
+ DEFAULT_PROACTIVE_GC_FULL_EVERY: Final[int] = 0
646
+
636
647
  # shutdown 时等待在途事件处理器完成的最长时间(秒)。
637
648
  # 运行时行为。适配器关闭时取消所有 pending handler tasks 后等待它们退出的耐心时间。
638
649
  # 修改影响: 设大确保处理器完整结束,设小加速关闭流程。
@@ -743,6 +754,8 @@ __all__ = [
743
754
  "DEFAULT_MODULE_ENABLED",
744
755
  "DEFAULT_MODULE_PRIORITY",
745
756
  "DEFAULT_OFFLINE_BOT_EXPIRY_SECS",
757
+ "DEFAULT_PROACTIVE_GC_FULL_EVERY",
758
+ "DEFAULT_PROACTIVE_GC_GENERATION",
746
759
  "DEFAULT_PROACTIVE_GC_INTERVAL_SECS",
747
760
  "DEFAULT_RATE_LIMIT_CLEANUP_INTERVAL_SECS",
748
761
  "DEFAULT_RATE_LIMIT_MAX_REQUESTS",
@@ -184,27 +184,15 @@ class ModuleManager(ManagerBase):
184
184
  :param old_dict: 变更前的配置字典(可能为 None)
185
185
  :param new_dict: 变更后的配置字典(可能为 None)
186
186
  """
187
- config_class = getattr(instance, "ConfigClass", None)
188
- try:
189
- if config_class is not None:
190
- from ..runtime.config_schema import dict_to_dataclass
191
-
192
- old_config = (
193
- dict_to_dataclass(config_class, old_dict) if old_dict else None
194
- )
195
- new_config = (
196
- dict_to_dataclass(config_class, new_dict) if new_dict else None
197
- )
198
- else:
199
- old_config = old_dict
200
- new_config = new_dict
201
- instance.on_config_update(old_config, new_config)
202
- except Exception as e:
203
- logger.error(
204
- i18n.t(
205
- "core.module.config_update_failed", name=module_name, error=e
206
- )
207
- )
187
+ from ..Core.Bases.config_schema import _notify_instance_config_update
188
+
189
+ _notify_instance_config_update(
190
+ instance,
191
+ old_dict,
192
+ new_dict,
193
+ i18n_key="core.module.config_update_failed",
194
+ log_params={"name": module_name},
195
+ )
208
196
 
209
197
  # ==================== 模块注册与管理 ====================
210
198
 
@@ -51,6 +51,8 @@ __all__ = [
51
51
  # 通用配置 Schema(适配器/模块/外部项目均可使用)
52
52
  "BaseConfig",
53
53
  "BotAccountConfig",
54
+ "I18nConfig",
55
+ "SECRET_REDACTED",
54
56
  # 异常处理
55
57
  "ExceptionHandler",
56
58
  "async_exception_handler",
@@ -78,6 +80,7 @@ __all__ = [
78
80
  # 异常诊断
79
81
  "log_diagnostic",
80
82
  "log_snapshot",
83
+ "redact_secret",
81
84
  "register_config_i18n",
82
85
  "resolve_config_schema",
83
86
  "setup_exception_handling",
@@ -99,10 +102,13 @@ _LAZY_FROM_CONFIG_SCHEMA = {
99
102
  "AdapterConfig",
100
103
  "BaseConfig",
101
104
  "BotAccountConfig",
105
+ "I18nConfig",
106
+ "SECRET_REDACTED",
102
107
  "dataclass_to_defaults_dict",
103
108
  "dataclass_to_toml_with_comments",
104
109
  "dict_to_dataclass",
105
110
  "get_config_schema",
111
+ "redact_secret",
106
112
  "register_config_i18n",
107
113
  "resolve_config_schema",
108
114
  "validate_config",
@@ -21,6 +21,8 @@ from ..Core.constants import (
21
21
  DEFAULT_LOG_MEMORY_LIMIT,
22
22
  DEFAULT_MESSAGE_IGNORE_SELF,
23
23
  DEFAULT_OFFLINE_BOT_EXPIRY_SECS,
24
+ DEFAULT_PROACTIVE_GC_FULL_EVERY,
25
+ DEFAULT_PROACTIVE_GC_GENERATION,
24
26
  DEFAULT_PROACTIVE_GC_INTERVAL_SECS,
25
27
  DEFAULT_SERVER_AUTO_START,
26
28
  DEFAULT_SERVER_HOST,
@@ -79,6 +81,8 @@ DEFAULT_ERISPULSE_CONFIG = {
79
81
  # 性能优化与主动 GC 配置
80
82
  "handler_max_concurrency": DEFAULT_HANDLER_MAX_CONCURRENCY,
81
83
  "proactive_gc_interval": DEFAULT_PROACTIVE_GC_INTERVAL_SECS,
84
+ "proactive_gc_generation": DEFAULT_PROACTIVE_GC_GENERATION,
85
+ "proactive_gc_full_every": DEFAULT_PROACTIVE_GC_FULL_EVERY,
82
86
  "offline_bot_expiry": DEFAULT_OFFLINE_BOT_EXPIRY_SECS,
83
87
  },
84
88
  "i18n": {
@@ -23,6 +23,8 @@ from collections.abc import Callable
23
23
  from typing import TYPE_CHECKING, Any
24
24
 
25
25
  from .Core.constants import (
26
+ DEFAULT_PROACTIVE_GC_FULL_EVERY,
27
+ DEFAULT_PROACTIVE_GC_GENERATION,
26
28
  DEFAULT_PROACTIVE_GC_INTERVAL_SECS,
27
29
  DEFAULT_UNINIT_TIMEOUT_SECS,
28
30
  HARD_RESTART_EXIT_CODE,
@@ -569,6 +571,16 @@ class SDK:
569
571
  },
570
572
  )
571
573
 
574
+ # 冻结框架对象:将当前所有已跟踪对象移入永久代,
575
+ # 后续 gc.collect() 不再扫描框架单例/缓存/模块/适配器实例,
576
+ # 仅回收运行期新建的临时对象,降低 GC 暂停与误回收风险。
577
+ import gc as _gc
578
+
579
+ try:
580
+ _gc.freeze()
581
+ except Exception:
582
+ pass
583
+
572
584
  # 启动主动 GC 后台任务
573
585
  self._sdk._start_proactive_gc()
574
586
 
@@ -834,39 +846,57 @@ class SDK:
834
846
  启动主动 GC 后台任务
835
847
 
836
848
  定期执行 Python GC 和内部资源回收(离线 Bot 清理等),
837
- 防止长期运行时的内存增长。间隔由框架配置 proactive_gc_interval 控制。
849
+ 防止长期运行时的内存增长。
850
+
851
+ GC 行为由三项框架配置控制(均支持热更新):
852
+
853
+ - ``proactive_gc_interval``: 回收间隔秒数(0 禁用)
854
+ - ``proactive_gc_generation``: 回收分代(0/1/2,默认 2=最老代)
855
+ - ``proactive_gc_full_every``: 每 N 轮额外做一次全量回收(0 禁用)
856
+
857
+ 初始化阶段已调用 ``gc.freeze()`` 将框架对象移入永久代,
858
+ 此处 ``gc.collect()`` 仅扫描运行期新建对象。
838
859
  """
839
860
  # 停止已有的 GC 任务
840
861
  self._stop_proactive_gc()
841
862
 
842
- def _read_gc_interval() -> int:
863
+ def _read_gc_config() -> tuple[int, int, int]:
843
864
  interval = DEFAULT_PROACTIVE_GC_INTERVAL_SECS
865
+ generation = DEFAULT_PROACTIVE_GC_GENERATION
866
+ full_every = DEFAULT_PROACTIVE_GC_FULL_EVERY
844
867
  try:
845
868
  from .runtime import get_framework_config
846
869
 
847
- interval = get_framework_config().get(
848
- "proactive_gc_interval", interval
849
- )
870
+ fw = get_framework_config()
871
+ interval = fw.get("proactive_gc_interval", interval)
872
+ generation = fw.get("proactive_gc_generation", generation)
873
+ full_every = fw.get("proactive_gc_full_every", full_every)
850
874
  except Exception:
851
875
  pass
852
- return interval
876
+ return interval, generation, full_every
853
877
 
854
- if _read_gc_interval() <= 0:
878
+ if _read_gc_config()[0] <= 0:
855
879
  return # 配置禁用
856
880
 
857
881
  async def _gc_loop():
858
882
  import gc
859
883
 
884
+ round_count = 0
860
885
  while True:
861
886
  try:
862
- # 每轮重新读取间隔,支持 proactive_gc_interval 热更新
863
- interval = _read_gc_interval()
887
+ # 每轮重新读取配置,支持热更新
888
+ interval, generation, full_every = _read_gc_config()
864
889
  if interval <= 0:
865
890
  # 配置运行时禁用了 GC,停止循环
866
891
  break
867
892
  await asyncio.sleep(interval)
868
- # 1. Python GC
869
- collected = gc.collect()
893
+ round_count += 1
894
+ # 1. Python GC —— 按分代回收,可选周期性全量
895
+ do_full = full_every > 0 and round_count % full_every == 0
896
+ if do_full:
897
+ collected = gc.collect()
898
+ else:
899
+ collected = gc.collect(generation)
870
900
  # 2. 内部资源回收
871
901
  try:
872
902
  adapter_mgr = self.adapter
File without changes