ErisPulse 2.10.0.dev0__tar.gz → 2.10.1__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 (143) hide show
  1. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/PKG-INFO +2 -2
  2. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/pyproject.toml +6 -2
  3. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/commands/create.py +3 -18
  4. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/constants.py +1 -1
  5. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Bases/converter.py +11 -2
  6. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Event/base.py +28 -4
  7. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Event/command.py +104 -50
  8. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Event/interaction.py +48 -16
  9. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Event/message.py +12 -4
  10. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Event/message_builder.py +7 -1
  11. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Event/overrides.py +7 -3
  12. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Event/wrapper.py +116 -33
  13. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/config.py +27 -9
  14. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/constants.py +8 -0
  15. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/i18n/locales/en.py +6 -0
  16. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/i18n/locales/ja.py +6 -0
  17. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/i18n/locales/ru.py +6 -0
  18. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/i18n/locales/zh_cn.py +6 -0
  19. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/i18n/locales/zh_tw.py +6 -0
  20. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/router.py +41 -4
  21. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/runtime/sdk_initializer.py +17 -0
  22. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/.gitignore +0 -0
  23. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/LICENSE +0 -0
  24. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/README.pypi.md +0 -0
  25. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/__init__.py +0 -0
  26. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/base.py +0 -0
  27. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/cli.py +0 -0
  28. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/commands/__init__.py +0 -0
  29. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/commands/config.py +0 -0
  30. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/commands/doctor.py +0 -0
  31. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/commands/init.py +0 -0
  32. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/commands/install.py +0 -0
  33. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/commands/language.py +0 -0
  34. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/commands/list.py +0 -0
  35. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/commands/list_remote.py +0 -0
  36. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/commands/run.py +0 -0
  37. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/commands/self_update.py +0 -0
  38. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/commands/types.py +0 -0
  39. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/commands/uninstall.py +0 -0
  40. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/commands/upgrade.py +0 -0
  41. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/commands/version.py +0 -0
  42. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/console.py +0 -0
  43. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/hints.py +0 -0
  44. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/i18n/__init__.py +0 -0
  45. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/i18n/locales/__init__.py +0 -0
  46. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/i18n/locales/en.py +0 -0
  47. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/i18n/locales/ja.py +0 -0
  48. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/i18n/locales/ru.py +0 -0
  49. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/i18n/locales/zh_cn.py +0 -0
  50. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/i18n/locales/zh_tw.py +0 -0
  51. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/registry.py +0 -0
  52. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/utils/__init__.py +0 -0
  53. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/utils/config_wizard.py +0 -0
  54. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/utils/display.py +0 -0
  55. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/utils/interactive.py +0 -0
  56. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/utils/package_manager.py +0 -0
  57. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/CLI/utils/scaffold_text.py +0 -0
  58. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Bases/__init__.py +0 -0
  59. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Bases/adapter.py +0 -0
  60. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Bases/api_dsl.py +0 -0
  61. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Bases/client.py +0 -0
  62. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Bases/config_schema.py +0 -0
  63. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Bases/errors.py +0 -0
  64. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Bases/i18n_schema.py +0 -0
  65. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Bases/kv_builder.py +0 -0
  66. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Bases/manager.py +0 -0
  67. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Bases/model.py +0 -0
  68. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Bases/module.py +0 -0
  69. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Bases/router.py +0 -0
  70. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Bases/send_builder.py +0 -0
  71. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Bases/send_rules.py +0 -0
  72. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Bases/sql_base.py +0 -0
  73. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Bases/storage.py +0 -0
  74. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Bases/websocket.py +0 -0
  75. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Event/__init__.py +0 -0
  76. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Event/command_args.py +0 -0
  77. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Event/conversation.py +0 -0
  78. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Event/governance.py +0 -0
  79. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Event/meta.py +0 -0
  80. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Event/notice.py +0 -0
  81. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Event/request.py +0 -0
  82. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Event/session_type.py +0 -0
  83. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Event/throttle.py +0 -0
  84. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/Event/trace.py +0 -0
  85. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/__init__.py +0 -0
  86. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/adapter.py +0 -0
  87. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/assets/__init__.py +0 -0
  88. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/assets/error.css +0 -0
  89. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/assets/error.html +0 -0
  90. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/assets/root.css +0 -0
  91. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/assets/root.html +0 -0
  92. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/client.py +0 -0
  93. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/connections.py +0 -0
  94. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/di.py +0 -0
  95. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/i18n/__init__.py +0 -0
  96. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/i18n/constants.py +0 -0
  97. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/i18n/locales/__init__.py +0 -0
  98. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/lifecycle.py +0 -0
  99. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/logger.py +0 -0
  100. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/master.py +0 -0
  101. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/module.py +0 -0
  102. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/ownership.py +0 -0
  103. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/scope.py +0 -0
  104. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/shadow.py +0 -0
  105. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/storage/__init__.py +0 -0
  106. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/storage/mysql.py +0 -0
  107. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/storage/postgres.py +0 -0
  108. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/storage/sqlite.py +0 -0
  109. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/text_match.py +0 -0
  110. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/Core/transcript.py +0 -0
  111. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/__init__.py +0 -0
  112. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/__main__.py +0 -0
  113. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/finders/__init__.py +0 -0
  114. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/finders/adapter.py +0 -0
  115. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/finders/bases/__init__.py +0 -0
  116. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/finders/bases/finder.py +0 -0
  117. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/finders/module.py +0 -0
  118. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/loaders/__init__.py +0 -0
  119. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/loaders/adapter.py +0 -0
  120. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/loaders/bases/__init__.py +0 -0
  121. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/loaders/bases/loader.py +0 -0
  122. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/loaders/module.py +0 -0
  123. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/loaders/plugin_folder.py +0 -0
  124. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/loaders/strategy.py +0 -0
  125. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/loaders/strict.py +0 -0
  126. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/py.typed +0 -0
  127. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/runtime/__init__.py +0 -0
  128. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/runtime/config_schema.py +0 -0
  129. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/runtime/context.py +0 -0
  130. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/runtime/diagnostics.py +0 -0
  131. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/runtime/example_config.py +0 -0
  132. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/runtime/exceptions.py +0 -0
  133. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/runtime/file_watcher.py +0 -0
  134. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/runtime/frame_config.py +0 -0
  135. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/runtime/hints.py +0 -0
  136. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/runtime/memory.py +0 -0
  137. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/runtime/owner_cleanup.py +0 -0
  138. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/runtime/plugin_reload.py +0 -0
  139. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/runtime/proactive_gc.py +0 -0
  140. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/runtime/tasks.py +0 -0
  141. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/runtime/troubleshoot.py +0 -0
  142. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/runtime/version.py +0 -0
  143. {erispulse-2.10.0.dev0 → erispulse-2.10.1}/src/ErisPulse/sdk.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ErisPulse
3
- Version: 2.10.0-dev.0
3
+ Version: 2.10.1
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
@@ -34,7 +34,7 @@ Requires-Dist: aiosqlite>=0.20.0
34
34
  Requires-Dist: fastapi>=0.116.1
35
35
  Requires-Dist: rich>=13.0.0
36
36
  Requires-Dist: tomlkit>=0.15.1
37
- Requires-Dist: uvicorn>=0.35.0
37
+ Requires-Dist: uvicorn<1.0,>=0.35.0
38
38
  Requires-Dist: websockets>=12.0
39
39
  Provides-Extra: mysql
40
40
  Requires-Dist: aiomysql>=0.2.0; extra == 'mysql'
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ErisPulse"
7
- version = "2.10.0-dev.0"
7
+ version = "2.10.1"
8
8
  description = "Event-driven modular async bot framework — write once, deploy to any platform | 事件驱动的模块化异步机器人框架,一次编写,多平台部署"
9
9
  readme = "README.pypi.md"
10
10
  requires-python = ">=3.10"
@@ -67,7 +67,11 @@ dependencies = [
67
67
  "websockets>=12.0",
68
68
  "aiohttp>=3.9.0",
69
69
  "fastapi>=0.116.1",
70
- "uvicorn>=0.35.0",
70
+
71
+ # uvicorn的内嵌启动依赖私有接口 Server._serve()(0.34.0 引入,用于跳过 serve() 的信号捕获),
72
+ # 上限锁定 0.x 线;启动期按能力探测,接口缺失时仅告警并回退公开 serve()
73
+ "uvicorn>=0.35.0,<1.0",
74
+
71
75
  # storage 相关(默认 SQLite 后端异步驱动,开箱即用)
72
76
  "aiosqlite>=0.20.0",
73
77
  ]
@@ -42,10 +42,10 @@ SOFTWARE.
42
42
  """
43
43
 
44
44
  # 生成模块/适配器模板的最低 SDK 地板(写入模板 min_sdk_version 字段)。
45
- # 模板示例使用 args=/options=(2.9 特性);pip 默认不解析预发布版本,
46
- # 该地板不得高于已发布的正式版本,否则生成的组件会因依赖装不上或
45
+ # 模板示例使用根导入契约(2.10 特性)与 args=/options=(2.9 特性);pip 默认不解析
46
+ # 预发布版本,该地板不得高于已发布的正式版本,否则生成的组件会因依赖装不上或
47
47
  # 加载器按 PEP 440 拒载(与 CLI/constants.py 的 INIT_ERISPULSE_DEPENDENCY 同步升版)。
48
- _TEMPLATE_MIN_SDK_VERSION = "2.9.0"
48
+ _TEMPLATE_MIN_SDK_VERSION = "2.10.0"
49
49
 
50
50
  _MODULE_PYPROJECT = """[project]
51
51
  name = "ErisPulse-{name}"
@@ -107,12 +107,6 @@ class Main(BaseModule):
107
107
  :ivar advanced_threshold: 高级阈值(docstring :ivar: 声明即为字段描述,无需 metadata)
108
108
  \"\"\"
109
109
 
110
- enabled: bool = field(
111
- default=True,
112
- metadata={{
113
- \"description\": {{\"i18n\": \"module.{name}.enabled\", \"default\": \"Enable module\"}},
114
- }},
115
- )
116
110
  # example 字段:默认不写入 config.toml(仅记录在 config.full.example,用户按需启用)
117
111
  advanced_threshold: int = field(
118
112
  default=100,
@@ -132,15 +126,6 @@ class Main(BaseModule):
132
126
  ru=\"Модуль {name}\",
133
127
  zh_TW=\"{name} 模組\",
134
128
  )
135
- enabled: I18nKey = I18nKey(
136
- key=\"module.{name}.enabled\",
137
- default=\"Enable module\",
138
- zh_CN=\"是否启用模块\",
139
- en=\"Enable module\",
140
- ja=\"モジュールを有効にする\",
141
- ru=\"Включить модуль\",
142
- zh_TW=\"啟用模組\",
143
- )
144
129
  hello_help: I18nKey = I18nKey(
145
130
  key=\"module.{name}.command.hello.help\",
146
131
  default=\"Send a greeting message\",
@@ -60,7 +60,7 @@ RUN_PERSISTENT_CRASH_HINT_THRESHOLD: int = 3
60
60
  # append_pyproject_dependencies 四处。
61
61
  # 修改影响: 真实 pip 依赖——pip 默认不解析预发布版本,该下限不得高于已发布的
62
62
  # 正式版本(与 create.py 的 _TEMPLATE_MIN_SDK_VERSION 同步升版)。
63
- INIT_ERISPULSE_DEPENDENCY: str = "erispulse>=2.9.0"
63
+ INIT_ERISPULSE_DEPENDENCY: str = "erispulse>=2.10.0"
64
64
 
65
65
  __all__ = [
66
66
  "ADAPTER_ENTRY_POINT_GROUP",
@@ -55,8 +55,17 @@ class BaseConverter:
55
55
 
56
56
  @staticmethod
57
57
  def at(user_id: str) -> dict:
58
- """构造 @ 消息段"""
59
- return {"type": "at", "data": {"user_id": user_id}}
58
+ """
59
+ 构造 @ 消息段
60
+
61
+ 方法名保留 ``at`` 别名,产出为 OB12 规范的 ``mention`` 段——
62
+ 此前产出 ``{"type": "at"}``(OB11 遗留形态),框架全部 mention
63
+ 判定(must_at_bot / has_mention / on_at_message)均不识别该形态。
64
+ """
65
+ return {"type": "mention", "data": {"user_id": user_id}}
66
+
67
+ # mention 为规范名别名(2.10.1 起推荐显式使用)
68
+ mention = at
60
69
 
61
70
  @staticmethod
62
71
  def image(file: str) -> dict:
@@ -386,9 +386,12 @@ class BaseEventHandler:
386
386
  },
387
387
  )
388
388
 
389
- # 忽略自己发送的消息
389
+ # 忽略自己发送的消息(self 标识取 account_id or user_id,与全框架口径一致:
390
+ # 仅填 self.account_id 的适配器此前会漏判导致机器人消息自激分发)
390
391
  if self.event_type == EVENT_TYPE_MESSAGE:
391
- if event.get("self", {}).get("user_id") == event.get("user_id"):
392
+ _self_info = event.get("self", {})
393
+ _self_id = _self_info.get("account_id") or _self_info.get("user_id")
394
+ if _self_id is not None and str(_self_id) == str(event.get("user_id")):
392
395
  event_config = get_event_config()
393
396
  ignore_self = event_config.get("message", {}).get("ignore_self", True)
394
397
  if ignore_self:
@@ -419,11 +422,32 @@ class BaseEventHandler:
419
422
  for _priority, group_iter in groupby(self.handlers, key=lambda h: h["priority"]):
420
423
  group = list(group_iter)
421
424
 
422
- # 过滤出满足条件的处理器(条件函数 + 作用域模块维度 + 事件作用域文本过滤)
425
+ # 过滤出满足条件的处理器(条件函数 + 作用域模块维度 + 事件作用域文本过滤)。
426
+ # 条件函数逐个异常隔离:一个坏模块的条件崩溃只影响自身命中与否,
427
+ # 不得拖死同优先级组的其它处理器(含命令分发器)
428
+ def _condition_ok(h: dict) -> bool:
429
+ condition = h.get("condition")
430
+ if not condition:
431
+ return True
432
+ try:
433
+ return bool(condition(event))
434
+ except Exception as _cond_err:
435
+ _h_qualname = getattr(
436
+ h["func"], "__qualname__", getattr(h["func"], "__name__", str(h["func"]))
437
+ )
438
+ logger.error(
439
+ i18n.t(
440
+ "core.event.condition_error",
441
+ handler=_h_qualname,
442
+ error=_cond_err,
443
+ )
444
+ )
445
+ return False
446
+
423
447
  active = [
424
448
  h
425
449
  for h in group
426
- if (not h.get("condition") or h["condition"](event))
450
+ if _condition_ok(h)
427
451
  and self._is_scope_allowed(h, scope_platform, scope_bot, scope_session)
428
452
  and self._is_scope_handler_ok(h, event)
429
453
  ]
@@ -391,6 +391,10 @@ class CommandHandler:
391
391
  # 使用函数名作为命令名
392
392
  cmd_names = [func.__name__]
393
393
 
394
+ # 注册期校验(fail-fast):空列表 / 非 str 名此前会 IndexError 或静默产出垃圾名
395
+ if not cmd_names or any(not isinstance(n, str) or not n for n in cmd_names):
396
+ raise ValueError(i18n.t("core.command.invalid_name", func=func.__qualname__))
397
+
394
398
  main_name = cmd_names[0]
395
399
 
396
400
  # 声明式参数/选项(args= / options=)注册期解析与校验(fail-fast):
@@ -861,6 +865,16 @@ class CommandHandler:
861
865
  # (命中 / 取消路径条目已被移除,此处仅兜底超时路径的残留)
862
866
  interaction.cancel(wait_key)
863
867
 
868
+ # 文本处理三态结果(_process_text_for_command / _try_execute_command 的返回契约):
869
+ # unmatched = 文本未解析出任何注册命令(允许 alt_message 兜底重试);
870
+ # consumed = 命令已执行 / 治理拦截 / 参数错误等已消费路径;
871
+ # rejected = 命中注册命令但被 scope/ACL/master/permission 拒绝。
872
+ # 命中注册命令后(无论拒绝与否)不再用 alt_message 重跑整条链——
873
+ # 否则拒绝回复会重复发送、用户自定义权限函数副作用会双触发。
874
+ _STATE_UNMATCHED = "unmatched"
875
+ _STATE_CONSUMED = "consumed"
876
+ _STATE_REJECTED = "rejected"
877
+
864
878
  async def _handle_message(self, event: dict[str, Any]):
865
879
  """
866
880
  处理消息事件中的命令
@@ -914,7 +928,7 @@ class CommandHandler:
914
928
  )
915
929
  return
916
930
 
917
- async def _process_text_for_command(event: "Event", text: str) -> bool:
931
+ async def _process_text_for_command(event: "Event", text: str) -> str:
918
932
  """
919
933
  处理文本内容,尝试匹配并执行命令
920
934
 
@@ -923,18 +937,25 @@ class CommandHandler:
923
937
 
924
938
  :param event: 消息事件数据
925
939
  :param text: 要处理的文本内容
926
- :return: 是否成功执行命令
940
+ :return: 处理状态三态(unmatched / consumed / rejected,
941
+ 见 :attr:`_STATE_UNMATCHED` 系列常量)
942
+ {!--< /internal-use >!--}
927
943
  """
928
944
  if not text:
929
- return False
945
+ return self._STATE_UNMATCHED
946
+
947
+ # 前导空白容忍:"@bot /kick x" 场景下首段文本常以空格开头
948
+ # (mention 段后的空格随平台输入分段进入 text),不剥离则前缀永不命中
949
+ text = text.lstrip()
930
950
 
931
951
  # 处理大小写敏感性
932
952
  check_text = text if self.case_sensitive else text.lower()
933
953
  prefixes = self._prefixes if self.case_sensitive else [p.lower() for p in self._prefixes]
934
954
 
935
- # 检查前缀,找出匹配的前缀(支持多个前缀)
955
+ # 检查前缀,找出匹配的前缀(支持多个前缀,最长优先——
956
+ # 避免 "/a" 抢占 "/ab" 导致后者整组命令不可达)
936
957
  matched_prefix = None
937
- for prefix in prefixes:
958
+ for prefix in sorted(prefixes, key=len, reverse=True):
938
959
  has_prefix = check_text.startswith(prefix)
939
960
  has_space_prefix = self.allow_space_prefix and check_text.startswith(prefix + " ")
940
961
  if has_prefix or has_space_prefix:
@@ -949,21 +970,32 @@ class CommandHandler:
949
970
  user_id=event.get("user_id", ""),
950
971
  )
951
972
  )
952
- return False
973
+ return self._STATE_UNMATCHED
953
974
 
954
975
  # 检查是否必须@机器人
955
976
  if self.must_at_bot:
956
977
  detail_type = infer_receive_type(event)
957
978
  # 一对一场景(private或user)不需要检查@
958
979
  if detail_type not in (DETAIL_TYPE_PRIVATE, DETAIL_TYPE_USER):
959
- message_segments = event.get("message", [])
960
- self_id = event.get("self", {}).get("user_id")
980
+ message_segments = event.get("message") or []
981
+ self_id = event.get_self_account_id() or event.get("self", {}).get("user_id")
961
982
 
962
983
  has_mention = False
963
- for segment in message_segments:
964
- if segment.get("type") == "mention" and segment.get("data", {}).get("user_id") == self_id:
965
- has_mention = True
966
- break
984
+ if isinstance(message_segments, list):
985
+ for segment in message_segments:
986
+ if not isinstance(segment, dict):
987
+ continue
988
+ data = segment.get("data")
989
+ segment_user_id = data.get("user_id") if isinstance(data, dict) else None
990
+ # user_id 两侧 str() 归一:适配器透传平台原生 int 时不失配
991
+ if (
992
+ segment.get("type") == "mention"
993
+ and segment_user_id is not None
994
+ and self_id is not None
995
+ and str(segment_user_id) == str(self_id)
996
+ ):
997
+ has_mention = True
998
+ break
967
999
 
968
1000
  if not has_mention:
969
1001
  logger.trace(
@@ -973,37 +1005,52 @@ class CommandHandler:
973
1005
  user_id=event.get("user_id", ""),
974
1006
  )
975
1007
  )
976
- return False
1008
+ # 文本是命令形态但未@机器人:判定为拒绝(alt_message
1009
+ # 走同样的段判定,重试不可能改变结果)
1010
+ return self._STATE_REJECTED
977
1011
 
978
1012
  # 尝试执行命令
979
1013
  return await self._try_execute_command(event, text, matched_prefix)
980
1014
 
981
- # 从 message 列表和 alt_message 中提取文本内容
982
- message_segments = event.get("message", [])
983
- message_text = ""
984
- for segment in message_segments:
985
- if segment.get("type") == "text":
986
- message_text = segment.get("data", {}).get("text", "")
987
- break
1015
+ # 从 message 段列表提取命令文本:按段顺序收集全部 text 段并以空格拼接——
1016
+ # 平台常把一条消息切成多个 text 段(如 mention 前后各一段),只取首段会
1017
+ # 静默丢失尾部参数。畸形输入全程容错(None / 字符串 / 非 dict 段 / data 非 dict)
1018
+ message_segments = event.get("message") or []
1019
+ raw_text_parts: list[str] = []
1020
+ if isinstance(message_segments, str):
1021
+ # OB11 遗留形态:适配器直接塞 CQ 码 / 纯文本字符串,按单段文本处理
1022
+ if message_segments:
1023
+ raw_text_parts.append(message_segments)
1024
+ elif isinstance(message_segments, list):
1025
+ for segment in message_segments:
1026
+ if not isinstance(segment, dict):
1027
+ continue
1028
+ if segment.get("type") != "text":
1029
+ continue
1030
+ segment_data = segment.get("data")
1031
+ segment_text = segment_data.get("text", "") if isinstance(segment_data, dict) else ""
1032
+ if segment_text:
1033
+ raw_text_parts.append(segment_text)
1034
+ message_text = " ".join(raw_text_parts)
988
1035
 
989
1036
  alt_message = event.get("alt_message", "")
990
1037
 
991
- # 尝试使用 message 列表的内容
1038
+ # 三态流转:仅当文本未命中任何注册命令时才用 alt_message 兜底——
1039
+ # 命中即止(无论执行 / 治理拦截 / 权限拒绝),杜绝拒绝链重复执行
1040
+ command_state = self._STATE_UNMATCHED
992
1041
  if message_text:
993
- command_matched = await _process_text_for_command(event, message_text)
994
- if command_matched:
995
- return
1042
+ command_state = await _process_text_for_command(event, message_text)
996
1043
 
997
- # 尝试使用 alt_message
998
- if alt_message and alt_message != message_text:
999
- command_matched = await _process_text_for_command(event, alt_message)
1000
- if command_matched:
1001
- return
1044
+ if command_state == self._STATE_UNMATCHED and alt_message and alt_message != message_text:
1045
+ command_state = await _process_text_for_command(event, alt_message)
1046
+
1047
+ if command_state != self._STATE_UNMATCHED:
1048
+ return
1002
1049
 
1003
- # 决策链:带前缀的文本未命中任何注册命令(或非命令文本)——放行给消息处理器
1050
+ # 决策链:带前缀的文本未命中任何注册命令(或非命令文本)——放行给消息处理器。
1051
+ # 交互等待已在方法开头 resolve 过(单次判定),此处不再重复调用:
1052
+ # 命令处理器内新注册的 wait_reply 不应被本条触发消息消费
1004
1053
  trace_step("dispatch", "passed", "core.trace.passed_to_message")
1005
- # 如果都没有匹配,检查是否是等待回复的消息
1006
- await self._check_pending_reply(event)
1007
1054
  return
1008
1055
 
1009
1056
  def _is_command_text(self, text: str) -> bool:
@@ -1024,11 +1071,13 @@ class CommandHandler:
1024
1071
  if not text:
1025
1072
  return False
1026
1073
 
1074
+ # 前导空白容忍与最长前缀优先:口径与 _process_text_for_command 保持一致
1075
+ text = text.lstrip()
1027
1076
  check_text = text if self.case_sensitive else text.lower()
1028
1077
  prefixes = self._prefixes if self.case_sensitive else [p.lower() for p in self._prefixes]
1029
1078
 
1030
1079
  matched_prefix = None
1031
- for prefix in prefixes:
1080
+ for prefix in sorted(prefixes, key=len, reverse=True):
1032
1081
  has_prefix = check_text.startswith(prefix)
1033
1082
  has_space_prefix = self.allow_space_prefix and check_text.startswith(prefix + " ")
1034
1083
  if has_prefix or has_space_prefix:
@@ -1046,7 +1095,7 @@ class CommandHandler:
1046
1095
  cmd_name, _actual, _matched = self._resolve_command_tokens(parts)
1047
1096
  return cmd_name is not None
1048
1097
 
1049
- async def _try_execute_command(self, event: "Event", original_text: str, prefix: str) -> bool:
1098
+ async def _try_execute_command(self, event: "Event", original_text: str, prefix: str) -> str:
1050
1099
  """
1051
1100
  尝试执行命令
1052
1101
 
@@ -1056,14 +1105,15 @@ class CommandHandler:
1056
1105
  :param event: 消息事件数据
1057
1106
  :param original_text: 原始文本内容(命令名匹配时按配置做大小写归一,参数保留原文)
1058
1107
  :param prefix: 已匹配的命令前缀(可能已转换为小写)
1059
- :return: 是否成功执行命令
1108
+ :return: 处理状态三态(unmatched / consumed / rejected,
1109
+ 见 :attr:`_STATE_UNMATCHED` 系列常量)
1060
1110
  """
1061
1111
  # 解析命令和参数:参数与 raw 载荷保留用户原始大小写(取自 original_text,
1062
1112
  # 前缀为配置字面量、在大小写归一前后长度一致),仅命令名匹配候选做归一
1063
1113
  command_text = original_text[len(prefix) :].strip()
1064
1114
  raw_parts = command_text.split()
1065
1115
  if not raw_parts:
1066
- return False
1116
+ return self._STATE_UNMATCHED
1067
1117
 
1068
1118
  # 处理大小写敏感性:候选名归一化用于匹配,参数列表保留原始形式
1069
1119
  parts = raw_parts if self.case_sensitive else [part.lower() for part in raw_parts]
@@ -1087,7 +1137,7 @@ class CommandHandler:
1087
1137
  text=f"{prefix}{raw_parts[0]}",
1088
1138
  suggestion=suggestion or "",
1089
1139
  )
1090
- return False
1140
+ return self._STATE_UNMATCHED
1091
1141
  args = raw_parts[matched:]
1092
1142
 
1093
1143
  # 命中即认领:文本已解析为一条注册命令,无论后续作用域 / ACL /
@@ -1127,7 +1177,8 @@ class CommandHandler:
1127
1177
  cmd_info = self.commands[actual_cmd_name]
1128
1178
  handler = cmd_info["func"]
1129
1179
 
1130
- # 作用域检查:模块未对该 Bot / 会话 / 平台启用时静默忽略(不回复、不认领)
1180
+ # 作用域检查:模块未对该 Bot / 会话 / 平台启用时静默不回复
1181
+ # (事件已在命令命中时统一认领,对低优先级处理器不可见)
1131
1182
  cmd_owner = cmd_info.get("owner")
1132
1183
  if cmd_owner:
1133
1184
  from ..scope import scope
@@ -1146,7 +1197,7 @@ class CommandHandler:
1146
1197
  command=actual_cmd_name,
1147
1198
  module=cmd_owner,
1148
1199
  )
1149
- return False
1200
+ return self._STATE_REJECTED
1150
1201
 
1151
1202
  # 命令用户 ACL(event.overrides.acl):命令名支持 glob
1152
1203
  # deny 命中 / allow 白名单未命中 / 严格模式无 ACL → 拒绝;
@@ -1172,7 +1223,7 @@ class CommandHandler:
1172
1223
  )
1173
1224
  )
1174
1225
  await self._send_permission_denied(event)
1175
- return False
1226
+ return self._STATE_REJECTED
1176
1227
 
1177
1228
  # 命令实现参数覆写(event.overrides.command):覆盖 master / hidden / aliases / prefix 等
1178
1229
  # 覆写键 master 由 overrides.command.apply 统一映射到存储键 must_master(用户优先)
@@ -1201,7 +1252,7 @@ class CommandHandler:
1201
1252
  )
1202
1253
  )
1203
1254
  await self._send_permission_denied(event)
1204
- return False
1255
+ return self._STATE_REJECTED
1205
1256
 
1206
1257
  # 检查权限:子命令未声明权限时继承最近声明了权限的祖先命令
1207
1258
  # (如 /admin 声明权限后,/admin add 等子命令自动受同一权限保护)
@@ -1233,7 +1284,7 @@ class CommandHandler:
1233
1284
  command=actual_cmd_name,
1234
1285
  )
1235
1286
  await self._send_permission_denied(event)
1236
- return False
1287
+ return self._STATE_REJECTED
1237
1288
  except Exception as e:
1238
1289
  logger.error(i18n.t("core.command.permission_check_error", error=e))
1239
1290
  trace_step(
@@ -1244,7 +1295,7 @@ class CommandHandler:
1244
1295
  error=str(e),
1245
1296
  )
1246
1297
  await self._send_permission_denied(event)
1247
- return False
1298
+ return self._STATE_REJECTED
1248
1299
 
1249
1300
  # 添加命令相关信息到事件(合并覆写后的有效参数)
1250
1301
  # owner:注册该命令的模块(懒激活占位命令即目标模块),
@@ -1261,6 +1312,9 @@ class CommandHandler:
1261
1312
  "owner": cmd_info.get("owner"),
1262
1313
  }
1263
1314
 
1315
+ if "command" in event:
1316
+ # 平台 payload 透传了同名键:框架写入会覆盖原始数据,告警留痕
1317
+ logger.warning(i18n.t("core.command.key_collision", platform=event.get("platform", UNKNOWN_PLATFORM)))
1264
1318
  event["command"] = command_info
1265
1319
 
1266
1320
  # 认领已在命令命中时完成(见上方 mark_processed),
@@ -1325,7 +1379,7 @@ class CommandHandler:
1325
1379
  "error": error_text,
1326
1380
  },
1327
1381
  )
1328
- return True
1382
+ return self._STATE_CONSUMED
1329
1383
 
1330
1384
  # 治理判定(cooldown / rate_limit / usage,EPRFC-2026-001 方向七):
1331
1385
  # 位于全部权限检查与参数解析之后——无权限用户不触发计时,
@@ -1335,15 +1389,15 @@ class CommandHandler:
1335
1389
  if await self._gate.check_cooldown(
1336
1390
  cmd_info["main_name"], actual_cmd_name, _effective, event, self._send_args_error
1337
1391
  ):
1338
- return True
1392
+ return self._STATE_CONSUMED
1339
1393
  if await self._gate.check_rate_limit(
1340
1394
  cmd_info["main_name"], actual_cmd_name, _effective, event, self._send_args_error
1341
1395
  ):
1342
- return True
1396
+ return self._STATE_CONSUMED
1343
1397
  if await self._gate.check_usage(
1344
1398
  cmd_info["main_name"], actual_cmd_name, _effective, event, self._send_args_error
1345
1399
  ):
1346
- return True
1400
+ return self._STATE_CONSUMED
1347
1401
 
1348
1402
  # 废弃声明(deprecated=):调用时自动回复废弃文案;默认继续执行,
1349
1403
  # deprecated_reject=True 时拒绝执行(命令已认领,不漏给消息处理器)
@@ -1379,7 +1433,7 @@ class CommandHandler:
1379
1433
  "error": "deprecated",
1380
1434
  },
1381
1435
  )
1382
- return True
1436
+ return self._STATE_CONSUMED
1383
1437
  trace_step(
1384
1438
  "deprecated",
1385
1439
  "notice",
@@ -1455,7 +1509,7 @@ class CommandHandler:
1455
1509
  },
1456
1510
  )
1457
1511
 
1458
- return True
1512
+ return self._STATE_CONSUMED
1459
1513
 
1460
1514
  logger.trace(
1461
1515
  i18n.t(
@@ -1465,7 +1519,7 @@ class CommandHandler:
1465
1519
  user_id=event.get("user_id", ""),
1466
1520
  )
1467
1521
  )
1468
- return False
1522
+ return self._STATE_UNMATCHED
1469
1523
 
1470
1524
  async def _check_pending_reply(self, event: "Event"):
1471
1525
  """
@@ -83,6 +83,7 @@ class _Entry:
83
83
  "future",
84
84
  "key",
85
85
  "kind",
86
+ "matcher",
86
87
  "owner",
87
88
  "pattern",
88
89
  "platform",
@@ -103,6 +104,8 @@ class _Entry:
103
104
  self.validator: Any = None
104
105
  self.pattern: str | None = None
105
106
  self.regex: str | None = None
107
+ # 自定义匹配器(select 多路等待聚合用):提供时完全取代 pattern/regex/validator 链
108
+ self.matcher: Any = None
106
109
  # 命令穿透三态(2.8.3):None=跟随全局配置 / True=强制豁免 / False=强制吞
107
110
  self.cmdpass: bool | None = None
108
111
  # lease 类别字段
@@ -290,7 +293,10 @@ class InteractionManager:
290
293
  _send_type, target_id = get_send_type_and_target_id(event, platform)
291
294
  self_info = event.get("self") or {}
292
295
  bot_id = self_info.get("account_id", "") or self_info.get("user_id", "")
293
- return f"{platform}:{bot_id}:{target_id}"
296
+ # 并入 detail_type:同平台同 bot 下 user_id 与 group_id 数值碰撞时
297
+ # (如 user 123 与 group 123),私聊与群聊的会话级等待不再互相命中
298
+ detail_type = event.get("detail_type") or ""
299
+ return f"{platform}:{bot_id}:{detail_type}:{target_id}"
294
300
 
295
301
  # ==================== 索引维护 ====================
296
302
 
@@ -342,6 +348,7 @@ class InteractionManager:
342
348
  owner: str | None = None,
343
349
  session_scope: bool = False,
344
350
  cmdpass: bool | None = None,
351
+ matcher: Any = None,
345
352
  ) -> _Entry:
346
353
  """
347
354
  {!--< internal-use >!--}
@@ -361,6 +368,8 @@ class InteractionManager:
361
368
  :param session_scope: 会话级等待(同会话任何人的回复均可命中,键不含 user 维度)
362
369
  :param cmdpass: 是否跳过命令匹配的三态(None=跟随全局配置,默认不跳过;
363
370
  True=跳过命令匹配,等待期间消息一律作为回复消费)
371
+ :param matcher: 自定义命中判定函数 ``callable(Event) -> bool``,提供时完全取代
372
+ pattern/regex/validator 链(select 多路等待的聚合入口;异常按不命中处理)
364
373
  :return: 注册的条目(含推导的会话键)
365
374
  """
366
375
  if session_scope:
@@ -383,6 +392,7 @@ class InteractionManager:
383
392
  entry.pattern = pattern
384
393
  entry.regex = regex
385
394
  entry.cmdpass = cmdpass
395
+ entry.matcher = matcher
386
396
  entry.timestamp = time.monotonic()
387
397
 
388
398
  if session_scope:
@@ -430,9 +440,13 @@ class InteractionManager:
430
440
  .get("cmdpass", DEFAULT_WAIT_REPLY_CMDPASS)
431
441
  )
432
442
  if not cmdpass and event.get("type", "") == "message":
443
+ # 文本口径与命令分发一致(alt_message 优先 + 段回退):
444
+ # 仅填 message 段的事件此前判空直接放行,命令穿透永不生效。
445
+ # 经 extract_text 提取:resolve 的入参可能是裸 dict(非 Event 包装)
446
+ from ..text_match import extract_text
433
447
  from .command import command as _command_handler
434
448
 
435
- text = event.get("alt_message", "") or ""
449
+ text = extract_text(event)
436
450
  if text and _command_handler._is_command_text(text):
437
451
  logger.trace(
438
452
  i18n.t(
@@ -444,10 +458,16 @@ class InteractionManager:
444
458
  )
445
459
  return False
446
460
 
447
- # pattern(glob)/ regex(正则)过滤:不匹配则继续等待(不消费 future)
448
- if entry.pattern or entry.regex:
449
- text_cond = compile_text_matcher(entry.pattern, entry.regex)
450
- if text_cond is not None and not text_cond(event):
461
+ # 命中判定:matcher(自定义聚合判定,select 多路等待用)优先,
462
+ # 否则走 pattern(glob)/ regex(正则)过滤 + validator 校验链——
463
+ # 不匹配则继续等待(不消费 future)
464
+ if entry.matcher is not None:
465
+ try:
466
+ matched = bool(entry.matcher(event))
467
+ except Exception as e:
468
+ logger.error(i18n.t("core.interaction.matcher_error", error=e))
469
+ matched = False
470
+ if not matched:
451
471
  logger.trace(
452
472
  i18n.t(
453
473
  "core.interaction.reply_not_matched",
@@ -457,23 +477,35 @@ class InteractionManager:
457
477
  )
458
478
  )
459
479
  return False
460
-
461
- # validator 校验:失败则继续等待
462
- validator = entry.validator
463
- if validator is not None:
464
- try:
465
- if not validator(event):
480
+ else:
481
+ if entry.pattern or entry.regex:
482
+ text_cond = compile_text_matcher(entry.pattern, entry.regex)
483
+ if text_cond is not None and not text_cond(event):
466
484
  logger.trace(
467
485
  i18n.t(
468
- "core.interaction.reply_validation_failed",
486
+ "core.interaction.reply_not_matched",
469
487
  wait_key=key,
470
488
  user_id=event.get("user_id", ""),
489
+ platform=event.get("platform", UNKNOWN_PLATFORM),
471
490
  )
472
491
  )
473
492
  return False
474
- except Exception as e:
475
- logger.error(i18n.t("core.interaction.validator_error", error=e))
476
- return False
493
+
494
+ validator = entry.validator
495
+ if validator is not None:
496
+ try:
497
+ if not validator(event):
498
+ logger.trace(
499
+ i18n.t(
500
+ "core.interaction.reply_validation_failed",
501
+ wait_key=key,
502
+ user_id=event.get("user_id", ""),
503
+ )
504
+ )
505
+ return False
506
+ except Exception as e:
507
+ logger.error(i18n.t("core.interaction.validator_error", error=e))
508
+ return False
477
509
 
478
510
  # 权限复查:会话建立后被拉黑 / owner 模块被解绑 → 终止等待
479
511
  revoked_reason = self._check_revoked(entry, event)