ErisPulse 2.7.1.dev1__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.
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/PKG-INFO +1 -1
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/pyproject.toml +1 -1
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/run.py +12 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/i18n/locales/en.py +1 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/i18n/locales/ja.py +1 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/i18n/locales/ru.py +1 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/i18n/locales/zh_cn.py +1 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/i18n/locales/zh_tw.py +1 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/base.py +4 -4
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/command.py +16 -8
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/wrapper.py +68 -5
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/constants.py +26 -10
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/locales/en.py +2 -2
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/locales/ja.py +2 -2
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/locales/ru.py +2 -2
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/locales/zh_cn.py +2 -2
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/locales/zh_tw.py +2 -2
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/router.py +22 -57
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/frame_config.py +6 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/memory.py +13 -4
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/sdk.py +225 -47
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/.gitignore +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/LICENSE +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/README.pypi.md +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/__init__.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/base.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/cli.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/__init__.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/create.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/doctor.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/init.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/install.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/language.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/list.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/list_remote.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/self_update.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/types.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/uninstall.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/upgrade.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/console.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/constants.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/hints.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/i18n/__init__.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/i18n/locales/__init__.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/registry.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/utils/__init__.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/utils/display.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/utils/file_watcher.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/utils/package_manager.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/utils/scaffold_text.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/__init__.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/adapter.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/client.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/config_schema.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/converter.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/errors.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/i18n_schema.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/kv_builder.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/manager.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/module.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/router.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/send_builder.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/send_rules.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/storage.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/websocket.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/__init__.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/message.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/message_builder.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/meta.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/notice.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/request.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/session_type.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/__init__.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/adapter.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/assets/__init__.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/assets/error.css +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/assets/error.html +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/assets/root.css +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/assets/root.html +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/client.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/config.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/__init__.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/constants.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/locales/__init__.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/lifecycle.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/logger.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/master.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/module.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/storage.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/__init__.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/__main__.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/finders/__init__.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/finders/adapter.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/finders/bases/__init__.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/finders/bases/finder.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/finders/module.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/loaders/__init__.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/loaders/adapter.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/loaders/bases/__init__.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/loaders/bases/loader.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/loaders/module.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/loaders/strategy.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/loaders/strict.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/py.typed +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/__init__.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/config_schema.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/context.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/diagnostics.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/exceptions.py +0 -0
- {erispulse-2.7.1.dev1 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/hints.py +0 -0
- {erispulse-2.7.1.dev1 → 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.
|
|
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.
|
|
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"
|
|
@@ -183,6 +183,7 @@ class RunCommand(Command):
|
|
|
183
183
|
]
|
|
184
184
|
|
|
185
185
|
crash_count = 0
|
|
186
|
+
process = None
|
|
186
187
|
try:
|
|
187
188
|
while True:
|
|
188
189
|
process = subprocess.Popen(cmd)
|
|
@@ -208,6 +209,17 @@ class RunCommand(Command):
|
|
|
208
209
|
# 继续循环,重新启动子进程
|
|
209
210
|
except KeyboardInterrupt:
|
|
210
211
|
pass
|
|
212
|
+
finally:
|
|
213
|
+
# 运行器退出(Ctrl+C 等)前必须终止子进程,否则孤儿进程会继续持有
|
|
214
|
+
# 端口等资源,导致下次启动时端口被占用(如监听 8000 的残留进程)。
|
|
215
|
+
if process is not None and process.poll() is None:
|
|
216
|
+
console.print(f"[info]{i18n.t('cli.run.terminating_child')}[/]")
|
|
217
|
+
process.terminate()
|
|
218
|
+
try:
|
|
219
|
+
process.wait(timeout=5)
|
|
220
|
+
except subprocess.TimeoutExpired:
|
|
221
|
+
process.kill()
|
|
222
|
+
process.wait()
|
|
211
223
|
|
|
212
224
|
def _run_script(self, script_path: str, reload_mode: bool):
|
|
213
225
|
"""
|
|
@@ -277,6 +277,7 @@ TRANSLATIONS = {
|
|
|
277
277
|
"cli.run.process_exited": "Process exited normally, waiting for file changes to restart",
|
|
278
278
|
"cli.run.process_crashed": "Process crashed (exit code {code}), fix and save to restart",
|
|
279
279
|
"cli.run.subprocess_crashed_retry": "Subprocess crashed, will auto-retry in {seconds}s...",
|
|
280
|
+
"cli.run.terminating_child": "Terminating the bot subprocess to release ports and resources...",
|
|
280
281
|
# ==================== uninstall command ====================
|
|
281
282
|
"cli.uninstall.description": "Uninstall module/adapter packages",
|
|
282
283
|
"cli.uninstall.package_help": "Package name(s) to uninstall (multiple allowed)",
|
|
@@ -277,6 +277,7 @@ TRANSLATIONS = {
|
|
|
277
277
|
"cli.run.process_exited": "プロセスが正常に終了しました。ファイルの変更を待機して再起動",
|
|
278
278
|
"cli.run.process_crashed": "プロセスが異常終了しました(終了コード {code})。修正して保存すると自動再起動",
|
|
279
279
|
"cli.run.subprocess_crashed_retry": "サブプロセスが異常終了しました。{seconds} 秒後に自動再試行します...",
|
|
280
|
+
"cli.run.terminating_child": "ポートなどのリソースを解放するため、ボットの子プロセスを終了しています...",
|
|
280
281
|
# ==================== uninstall コマンド ====================
|
|
281
282
|
"cli.uninstall.description": "モジュール/アダプターパッケージをアンインストール",
|
|
282
283
|
"cli.uninstall.package_help": "アンインストールするパッケージ名(複数指定可)",
|
|
@@ -277,6 +277,7 @@ TRANSLATIONS = {
|
|
|
277
277
|
"cli.run.process_exited": "Процесс завершен нормально, ожидание изменений файлов для перезапуска",
|
|
278
278
|
"cli.run.process_crashed": "Процесс аварийно завершен (код {code}), исправьте и сохраните для перезапуска",
|
|
279
279
|
"cli.run.subprocess_crashed_retry": "Подпроцесс аварийно завершен, автоматическая перезагрузка через {seconds}с...",
|
|
280
|
+
"cli.run.terminating_child": "Завершение дочернего процесса бота для освобождения портов и ресурсов...",
|
|
280
281
|
# ==================== команда uninstall ====================
|
|
281
282
|
"cli.uninstall.description": "Удалить пакеты модулей/адаптеров",
|
|
282
283
|
"cli.uninstall.package_help": "Имя(ена) пакета(ов) для удаления (можно указать несколько)",
|
|
@@ -277,6 +277,7 @@ TRANSLATIONS = {
|
|
|
277
277
|
"cli.run.process_exited": "进程已正常退出,等待文件变更后重启",
|
|
278
278
|
"cli.run.process_crashed": "进程异常退出(退出码 {code}),修复后保存文件将自动重启",
|
|
279
279
|
"cli.run.subprocess_crashed_retry": "子进程异常终止,将在 {seconds} 秒后自动重试...",
|
|
280
|
+
"cli.run.terminating_child": "正在终止机器人子进程,释放端口等资源...",
|
|
280
281
|
# ==================== uninstall 命令 ====================
|
|
281
282
|
"cli.uninstall.description": "卸载模块/适配器包",
|
|
282
283
|
"cli.uninstall.package_help": "要卸载的包名(可指定多个)",
|
|
@@ -277,6 +277,7 @@ TRANSLATIONS = {
|
|
|
277
277
|
"cli.run.process_exited": "程式已正常退出,等待檔案變更後重啟",
|
|
278
278
|
"cli.run.process_crashed": "程式異常退出(退出碼 {code}),修復後儲存檔案將自動重啟",
|
|
279
279
|
"cli.run.subprocess_crashed_retry": "子程序異常終止,將在 {seconds} 秒後自動重試...",
|
|
280
|
+
"cli.run.terminating_child": "正在終止機器人子程序,釋放連接埠等資源...",
|
|
280
281
|
# ==================== uninstall 命令 ====================
|
|
281
282
|
"cli.uninstall.description": "解除安裝模組/適配器包",
|
|
282
283
|
"cli.uninstall.package_help": "要解除安裝的包名(可指定多個)",
|
|
@@ -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.
|
|
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
|
-
|
|
329
|
-
|
|
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.
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
1768
|
+
return self.get("_propagation_stopped", False)
|
|
1706
1769
|
|
|
1707
1770
|
# ==================== 魔术方法 ====================
|
|
1708
1771
|
|
|
@@ -53,12 +53,6 @@ DEFAULT_SERVER_AUTO_START: Final[bool] = True
|
|
|
53
53
|
# 修改影响: Ctrl+C 后等待 uvicorn 关闭的耐心时间。超时后强制终止。
|
|
54
54
|
SERVER_SHUTDOWN_TIMEOUT_SECS: Final[float] = 5.0
|
|
55
55
|
|
|
56
|
-
# 路由服务器启动时端口被占用后的自动顺延尝试次数(含起始端口)。
|
|
57
|
-
# 运行时行为。启动时若端口被占用,自动依次尝试后续端口,
|
|
58
|
-
# 直至找到可用端口或达到该上限。实际监听端口通过 router.base_url 暴露。
|
|
59
|
-
# 修改影响: 设大允许更多顺延,设小更快放弃;设为 1 表示不顺延,占用即报错。
|
|
60
|
-
DEFAULT_SERVER_PORT_RETRY_LIMIT: Final[int] = 50
|
|
61
|
-
|
|
62
56
|
# ==============================================================================
|
|
63
57
|
# 配置键模板
|
|
64
58
|
#
|
|
@@ -641,14 +635,34 @@ DEFAULT_PROACTIVE_GC_INTERVAL_SECS: Final[int] = 300
|
|
|
641
635
|
|
|
642
636
|
# 主动 GC 分代(generation)。
|
|
643
637
|
# 运行时行为。gc.collect(generation) 只回收指定代及更年轻的对象:0=最年轻代(最轻量),
|
|
644
|
-
# 1=中年代,2
|
|
638
|
+
# 1=中年代,2=最老代(覆盖最全,等价于全量 gc.collect())。默认 0 使常规轮次保持轻量;
|
|
639
|
+
# 深度回收由 proactive_gc_full_every 周期性触发。
|
|
645
640
|
# 修改影响: 设低减少单次扫描开销但回收不彻底,设高(2)覆盖更全但单次更重。
|
|
646
|
-
DEFAULT_PROACTIVE_GC_GENERATION: Final[int] =
|
|
641
|
+
DEFAULT_PROACTIVE_GC_GENERATION: Final[int] = 0
|
|
647
642
|
|
|
648
643
|
# 主动 GC 全量收集周期(每 N 轮做一次全量 gc.collect())。
|
|
649
644
|
# 运行时行为。0 表示禁用(仅按 generation 回收),N>0 表示每 N 轮额外触发一次全量回收。
|
|
645
|
+
# 全量回收受 proactive_gc_memory_growth_mb 内存增长门限约束,无增长时跳过。
|
|
650
646
|
# 修改影响: 设低(如 5)确保周期性深度回收,设高或 0 减少长尾暂停。
|
|
651
|
-
DEFAULT_PROACTIVE_GC_FULL_EVERY: Final[int] =
|
|
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
|
|
652
666
|
|
|
653
667
|
# shutdown 时等待在途事件处理器完成的最长时间(秒)。
|
|
654
668
|
# 运行时行为。适配器关闭时取消所有 pending handler tasks 后等待它们退出的耐心时间。
|
|
@@ -761,8 +775,11 @@ __all__ = [
|
|
|
761
775
|
"DEFAULT_MODULE_PRIORITY",
|
|
762
776
|
"DEFAULT_OFFLINE_BOT_EXPIRY_SECS",
|
|
763
777
|
"DEFAULT_PROACTIVE_GC_FULL_EVERY",
|
|
778
|
+
"DEFAULT_PROACTIVE_GC_GEN0_MIN",
|
|
764
779
|
"DEFAULT_PROACTIVE_GC_GENERATION",
|
|
780
|
+
"DEFAULT_PROACTIVE_GC_IDLE_ONLY",
|
|
765
781
|
"DEFAULT_PROACTIVE_GC_INTERVAL_SECS",
|
|
782
|
+
"DEFAULT_PROACTIVE_GC_MEMORY_GROWTH_MB",
|
|
766
783
|
"DEFAULT_RATE_LIMIT_CLEANUP_INTERVAL_SECS",
|
|
767
784
|
"DEFAULT_RATE_LIMIT_MAX_REQUESTS",
|
|
768
785
|
"DEFAULT_RATE_LIMIT_WINDOW_SECS",
|
|
@@ -772,7 +789,6 @@ __all__ = [
|
|
|
772
789
|
"DEFAULT_SERVER_AUTO_START",
|
|
773
790
|
"DEFAULT_SERVER_HOST",
|
|
774
791
|
"DEFAULT_SERVER_PORT",
|
|
775
|
-
"DEFAULT_SERVER_PORT_RETRY_LIMIT",
|
|
776
792
|
"DEFAULT_STRICT_MODE",
|
|
777
793
|
"DEFAULT_UNINIT_TIMEOUT_SECS",
|
|
778
794
|
"DEFAULT_USE_GLOBAL_DB",
|
|
@@ -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...",
|
|
@@ -257,8 +259,6 @@ TRANSLATIONS = {
|
|
|
257
259
|
"core.router.start_failed_msg": "Router server startup failed",
|
|
258
260
|
"core.router.start_failed": "Failed to start server: {error}",
|
|
259
261
|
"core.router.port_in_use": "Port {host}:{port} is already in use; the router server could not start. Check whether another ErisPulse instance or another program is using this port (run `netstat -ano` to find the process holding it)",
|
|
260
|
-
"core.router.port_advance": "Port {host}:{requested} is already in use; automatically advanced to {host}:{port}",
|
|
261
|
-
"core.router.port_range_in_use": "Port {host}:{port} and the next {count} ports are all in use; the router server could not start. Check whether multiple ErisPulse instances or other programs are occupying these ports",
|
|
262
262
|
"core.router.stopping": "Stopping router server...",
|
|
263
263
|
"core.router.stopped_normal": "Router server stopped normally",
|
|
264
264
|
"core.router.stopped_cancelled": "Router server was cancelled",
|
|
@@ -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": "初期化環境を準備しています...",
|
|
@@ -257,8 +259,6 @@ TRANSLATIONS = {
|
|
|
257
259
|
"core.router.start_failed_msg": "ルーターサーバーの起動に失敗しました",
|
|
258
260
|
"core.router.start_failed": "サーバーの起動に失敗しました: {error}",
|
|
259
261
|
"core.router.port_in_use": "ポート {host}:{port} は既に使用されています。ルーターサーバーを起動できません。別の ErisPulse インスタンスや他のプログラムがこのポートを使用していないか確認してください(`netstat -ano` で使用中のプロセスを確認できます)",
|
|
260
|
-
"core.router.port_advance": "ポート {host}:{requested} は既に使用されています。{host}:{port} に自動で切り替えました",
|
|
261
|
-
"core.router.port_range_in_use": "ポート {host}:{port} とその後の {count} 個のポートがすべて使用中です。ルーターサーバーを起動できません。複数の ErisPulse インスタンスや他のプログラムがこれらのポートを占有していないか確認してください",
|
|
262
262
|
"core.router.stopping": "ルーターサーバーを停止しています...",
|
|
263
263
|
"core.router.stopped_normal": "ルーターサーバーは正常に停止しました",
|
|
264
264
|
"core.router.stopped_cancelled": "ルーターサーバーはキャンセルされました",
|
|
@@ -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": "Подготовка окружения инициализации...",
|
|
@@ -257,8 +259,6 @@ TRANSLATIONS = {
|
|
|
257
259
|
"core.router.start_failed_msg": "Ошибка запуска сервера маршрутизации",
|
|
258
260
|
"core.router.start_failed": "Ошибка запуска сервера: {error}",
|
|
259
261
|
"core.router.port_in_use": "Порт {host}:{port} уже занят, сервер маршрутизации не может запуститься. Проверьте, не использует ли этот порт другой экземпляр ErisPulse или другая программа (выполните `netstat -ano`, чтобы найти процесс, удерживающий порт)",
|
|
260
|
-
"core.router.port_advance": "Порт {host}:{requested} уже занят, автоматически переключено на {host}:{port}",
|
|
261
|
-
"core.router.port_range_in_use": "Порт {host}:{port} и последующие {count} портов заняты, сервер маршрутизации не может запуститься. Проверьте, не занимают ли эти порты несколько экземпляров ErisPulse или других программ",
|
|
262
262
|
"core.router.stopping": "Остановка сервера маршрутизации...",
|
|
263
263
|
"core.router.stopped_normal": "Сервер маршрутизации остановлен нормально",
|
|
264
264
|
"core.router.stopped_cancelled": "Работа сервера маршрутизации была отменена",
|
|
@@ -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": "准备初始化环境...",
|
|
@@ -257,8 +259,6 @@ TRANSLATIONS = {
|
|
|
257
259
|
"core.router.start_failed_msg": "路由服务器启动失败",
|
|
258
260
|
"core.router.start_failed": "启动服务器失败: {error}",
|
|
259
261
|
"core.router.port_in_use": "端口 {host}:{port} 已被占用,路由服务器无法启动。请检查是否有另一个 ErisPulse 实例或其他程序正在使用该端口(可执行 `netstat -ano` 查看占用进程)",
|
|
260
|
-
"core.router.port_advance": "端口 {host}:{requested} 已被占用,已自动顺延至 {host}:{port}",
|
|
261
|
-
"core.router.port_range_in_use": "端口 {host}:{port} 及其后 {count} 个端口均已被占用,无法启动路由服务器。请检查是否有多个 ErisPulse 实例或其他程序占用了这些端口",
|
|
262
262
|
"core.router.stopping": "正在停止路由服务器...",
|
|
263
263
|
"core.router.stopped_normal": "路由服务器已正常停止",
|
|
264
264
|
"core.router.stopped_cancelled": "路由服务器已被取消",
|
|
@@ -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": "準備初始化環境...",
|
|
@@ -257,8 +259,6 @@ TRANSLATIONS = {
|
|
|
257
259
|
"core.router.start_failed_msg": "路由伺服器啟動失敗",
|
|
258
260
|
"core.router.start_failed": "啟動伺服器失敗: {error}",
|
|
259
261
|
"core.router.port_in_use": "連接埠 {host}:{port} 已被占用,路由伺服器無法啟動。請檢查是否有另一個 ErisPulse 實例或其他程式正在使用該連接埠(可執行 `netstat -ano` 查看占用程序)",
|
|
260
|
-
"core.router.port_advance": "連接埠 {host}:{requested} 已被占用,已自動順延至 {host}:{port}",
|
|
261
|
-
"core.router.port_range_in_use": "連接埠 {host}:{port} 及其後 {count} 個連接埠均已被占用,無法啟動路由伺服器。請檢查是否有多個 ErisPulse 實例或其他程式占用了這些連接埠",
|
|
262
262
|
"core.router.stopping": "正在停止路由伺服器...",
|
|
263
263
|
"core.router.stopped_normal": "路由伺服器已正常停止",
|
|
264
264
|
"core.router.stopped_cancelled": "路由伺服器已被取消",
|
|
@@ -53,7 +53,6 @@ from .constants import (
|
|
|
53
53
|
DEFAULT_SECURITY_HEADERS,
|
|
54
54
|
DEFAULT_SERVER_HOST,
|
|
55
55
|
DEFAULT_SERVER_PORT,
|
|
56
|
-
DEFAULT_SERVER_PORT_RETRY_LIMIT,
|
|
57
56
|
DEFAULT_WS_AUTO_ACCEPT,
|
|
58
57
|
FALLBACK_IPV4,
|
|
59
58
|
FALLBACK_IPV6_HOST,
|
|
@@ -2601,26 +2600,23 @@ class RouterManager:
|
|
|
2601
2600
|
self._get_local_ips()
|
|
2602
2601
|
self._apply_config()
|
|
2603
2602
|
|
|
2604
|
-
#
|
|
2605
|
-
#
|
|
2606
|
-
#
|
|
2607
|
-
#
|
|
2608
|
-
|
|
2609
|
-
bind_port = self._find_available_port(
|
|
2610
|
-
host, port, DEFAULT_SERVER_PORT_RETRY_LIMIT
|
|
2611
|
-
)
|
|
2603
|
+
# 同步探测端口是否可绑定。uvicorn 在端口被占用时会执行 sys.exit(3),
|
|
2604
|
+
# 其 SystemExit 会被事件循环重新抛出并取消所有任务,导致进程异常退出并
|
|
2605
|
+
# 刷屏大量 "Task was destroyed but it is pending"。此处先同步 bind 探测,
|
|
2606
|
+
# 端口被占用时在启动前抛出清晰、可操作的错误,由上层按致命错误处理。
|
|
2607
|
+
self._check_port_available(host, port)
|
|
2612
2608
|
|
|
2613
2609
|
config = uvicorn.Config(
|
|
2614
2610
|
self.app,
|
|
2615
2611
|
host=host,
|
|
2616
|
-
port=
|
|
2612
|
+
port=port,
|
|
2617
2613
|
log_level="warning",
|
|
2618
2614
|
ssl_certfile=ssl_certfile,
|
|
2619
2615
|
ssl_keyfile=ssl_keyfile,
|
|
2620
2616
|
)
|
|
2621
2617
|
self._uvicorn_server = uvicorn.Server(config)
|
|
2622
2618
|
|
|
2623
|
-
self.base_url = f"http{'s' if ssl_certfile else ''}://{host}:{
|
|
2619
|
+
self.base_url = f"http{'s' if ssl_certfile else ''}://{host}:{port}"
|
|
2624
2620
|
display_url = self._format_display_url(self.base_url)
|
|
2625
2621
|
http_count = sum(len(paths) for paths in self._http_routes.values())
|
|
2626
2622
|
ws_count = sum(len(paths) for paths in self._websocket_routes.values())
|
|
@@ -2662,7 +2658,7 @@ class RouterManager:
|
|
|
2662
2658
|
data={
|
|
2663
2659
|
"base_url": self.base_url,
|
|
2664
2660
|
"host": host,
|
|
2665
|
-
"port":
|
|
2661
|
+
"port": port,
|
|
2666
2662
|
},
|
|
2667
2663
|
)
|
|
2668
2664
|
except Exception as e:
|
|
@@ -2684,18 +2680,21 @@ class RouterManager:
|
|
|
2684
2680
|
logger.error(i18n.t("core.router.start_failed", error=e))
|
|
2685
2681
|
raise e
|
|
2686
2682
|
|
|
2687
|
-
def
|
|
2683
|
+
def _check_port_available(self, host: str, port: int) -> None:
|
|
2688
2684
|
"""
|
|
2689
|
-
|
|
2685
|
+
同步探测端口是否可绑定,避免 uvicorn 异步启动失败后引发级联错误
|
|
2690
2686
|
|
|
2691
2687
|
uvicorn 在端口被占用时会执行 ``sys.exit(STARTUP_FAILURE)``,其 SystemExit
|
|
2692
2688
|
会被事件循环重新抛出并取消所有任务,导致进程异常退出并刷屏大量
|
|
2693
|
-
"Task was destroyed but it is pending"。此处先同步 bind
|
|
2694
|
-
|
|
2689
|
+
"Task was destroyed but it is pending"。此处先同步 bind 探测端口,若被占用
|
|
2690
|
+
则在启动前抛出清晰的 i18n 提示,由上层按致命错误处理(不自动顺延端口,
|
|
2691
|
+
避免生产环境暴露的端口变化导致外部访问失败)。
|
|
2695
2692
|
|
|
2696
2693
|
:param host: str 监听地址
|
|
2697
2694
|
:param port: int 监听端口
|
|
2698
|
-
|
|
2695
|
+
|
|
2696
|
+
:raises RuntimeError: 当端口被占用时抛出,携带友好的错误提示
|
|
2697
|
+
:raises OSError: 其他绑定错误(如无权限绑定特权端口)
|
|
2699
2698
|
"""
|
|
2700
2699
|
try:
|
|
2701
2700
|
family = socket.AF_INET6 if ":" in host else socket.AF_INET
|
|
@@ -2704,46 +2703,12 @@ class RouterManager:
|
|
|
2704
2703
|
probe.bind((host, port))
|
|
2705
2704
|
finally:
|
|
2706
2705
|
probe.close()
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
) -> int:
|
|
2714
|
-
"""
|
|
2715
|
-
在 [port, port + retry_limit) 范围内寻找第一个可绑定的端口
|
|
2716
|
-
|
|
2717
|
-
端口被占用时自动顺延,实际监听端口通过
|
|
2718
|
-
router.base_url 暴露给下游。若范围内全部被占用,抛出清晰的错误提示。
|
|
2719
|
-
|
|
2720
|
-
:param host: str 监听地址
|
|
2721
|
-
:param port: int 期望端口(顺延起点)
|
|
2722
|
-
:param retry_limit: int 尝试的端口个数(含起点),最小为 1
|
|
2723
|
-
:return: int 实际可绑定的端口
|
|
2724
|
-
|
|
2725
|
-
:raises RuntimeError: 当范围内端口全部被占用时抛出
|
|
2726
|
-
"""
|
|
2727
|
-
for candidate in range(port, port + max(1, retry_limit)):
|
|
2728
|
-
if self._is_port_bindable(host, candidate):
|
|
2729
|
-
if candidate != port:
|
|
2730
|
-
logger.warning(
|
|
2731
|
-
i18n.t(
|
|
2732
|
-
"core.router.port_advance",
|
|
2733
|
-
host=host,
|
|
2734
|
-
requested=port,
|
|
2735
|
-
port=candidate,
|
|
2736
|
-
)
|
|
2737
|
-
)
|
|
2738
|
-
return candidate
|
|
2739
|
-
raise RuntimeError(
|
|
2740
|
-
i18n.t(
|
|
2741
|
-
"core.router.port_range_in_use",
|
|
2742
|
-
host=host,
|
|
2743
|
-
port=port,
|
|
2744
|
-
count=retry_limit,
|
|
2745
|
-
)
|
|
2746
|
-
)
|
|
2706
|
+
except OSError as e:
|
|
2707
|
+
if e.errno == errno.EADDRINUSE:
|
|
2708
|
+
raise RuntimeError(
|
|
2709
|
+
i18n.t("core.router.port_in_use", host=host, port=port)
|
|
2710
|
+
) from e
|
|
2711
|
+
raise
|
|
2747
2712
|
|
|
2748
2713
|
def _start_rate_limit_cleanup(self) -> None:
|
|
2749
2714
|
"""
|
|
@@ -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": {
|