ErisPulse 2.8.6__tar.gz → 2.9.0.dev2__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.8.6 → erispulse-2.9.0.dev2}/.gitignore +7 -2
  2. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/PKG-INFO +5 -3
  3. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/pyproject.toml +22 -4
  4. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/cli.py +12 -0
  5. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/commands/create.py +2 -0
  6. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/commands/doctor.py +47 -1
  7. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/commands/init.py +11 -7
  8. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/commands/install.py +10 -6
  9. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/commands/list.py +40 -6
  10. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/commands/run.py +29 -4
  11. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/commands/self_update.py +5 -4
  12. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/commands/upgrade.py +9 -3
  13. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/constants.py +16 -0
  14. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/i18n/locales/en.py +13 -0
  15. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/i18n/locales/ja.py +13 -0
  16. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/i18n/locales/ru.py +13 -0
  17. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/i18n/locales/zh_cn.py +13 -0
  18. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/i18n/locales/zh_tw.py +13 -0
  19. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/utils/config_wizard.py +7 -6
  20. erispulse-2.9.0.dev2/src/ErisPulse/CLI/utils/interactive.py +127 -0
  21. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/utils/package_manager.py +9 -6
  22. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Bases/__init__.py +8 -0
  23. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Bases/adapter.py +62 -656
  24. erispulse-2.9.0.dev2/src/ErisPulse/Core/Bases/api_dsl.py +698 -0
  25. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Bases/config_schema.py +215 -17
  26. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Bases/kv_builder.py +4 -2
  27. erispulse-2.9.0.dev2/src/ErisPulse/Core/Bases/model.py +1167 -0
  28. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Bases/module.py +8 -0
  29. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Bases/send_rules.py +8 -2
  30. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Bases/sql_base.py +175 -13
  31. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Event/__init__.py +14 -0
  32. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Event/base.py +76 -7
  33. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Event/command.py +609 -22
  34. erispulse-2.9.0.dev2/src/ErisPulse/Core/Event/command_args.py +539 -0
  35. erispulse-2.9.0.dev2/src/ErisPulse/Core/Event/conversation.py +773 -0
  36. erispulse-2.9.0.dev2/src/ErisPulse/Core/Event/governance.py +474 -0
  37. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Event/interaction.py +3 -2
  38. erispulse-2.9.0.dev2/src/ErisPulse/Core/Event/message.py +299 -0
  39. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Event/overrides.py +10 -8
  40. erispulse-2.9.0.dev2/src/ErisPulse/Core/Event/throttle.py +180 -0
  41. erispulse-2.9.0.dev2/src/ErisPulse/Core/Event/trace.py +134 -0
  42. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Event/wrapper.py +54 -748
  43. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/__init__.py +20 -1
  44. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/adapter.py +273 -94
  45. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/client.py +24 -19
  46. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/config.py +263 -25
  47. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/constants.py +78 -7
  48. erispulse-2.9.0.dev2/src/ErisPulse/Core/di.py +226 -0
  49. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/i18n/locales/en.py +89 -4
  50. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/i18n/locales/ja.py +89 -4
  51. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/i18n/locales/ru.py +89 -4
  52. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/i18n/locales/zh_cn.py +89 -4
  53. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/i18n/locales/zh_tw.py +89 -4
  54. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/lifecycle.py +137 -70
  55. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/logger.py +4 -4
  56. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/master.py +1 -1
  57. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/module.py +273 -190
  58. erispulse-2.9.0.dev2/src/ErisPulse/Core/ownership.py +517 -0
  59. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/router.py +391 -71
  60. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/scope.py +326 -190
  61. erispulse-2.9.0.dev2/src/ErisPulse/Core/shadow.py +497 -0
  62. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/storage/mysql.py +23 -0
  63. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/storage/postgres.py +19 -0
  64. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/storage/sqlite.py +11 -2
  65. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/text_match.py +1 -1
  66. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/transcript.py +55 -4
  67. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/loaders/module.py +386 -5
  68. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/runtime/__init__.py +9 -0
  69. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/runtime/diagnostics.py +126 -0
  70. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/runtime/exceptions.py +9 -0
  71. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/runtime/file_watcher.py +29 -2
  72. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/runtime/plugin_reload.py +3 -2
  73. erispulse-2.9.0.dev2/src/ErisPulse/runtime/proactive_gc.py +292 -0
  74. erispulse-2.9.0.dev2/src/ErisPulse/runtime/sdk_initializer.py +708 -0
  75. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/runtime/tasks.py +8 -2
  76. erispulse-2.9.0.dev2/src/ErisPulse/runtime/troubleshoot.py +233 -0
  77. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/sdk.py +62 -923
  78. erispulse-2.8.6/src/ErisPulse/Core/Event/message.py +0 -197
  79. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/LICENSE +0 -0
  80. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/README.pypi.md +0 -0
  81. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/__init__.py +0 -0
  82. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/base.py +0 -0
  83. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/commands/__init__.py +0 -0
  84. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/commands/config.py +0 -0
  85. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/commands/language.py +0 -0
  86. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/commands/list_remote.py +0 -0
  87. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/commands/types.py +0 -0
  88. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/commands/uninstall.py +0 -0
  89. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/commands/version.py +0 -0
  90. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/console.py +0 -0
  91. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/hints.py +0 -0
  92. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/i18n/__init__.py +0 -0
  93. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/i18n/locales/__init__.py +0 -0
  94. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/registry.py +0 -0
  95. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/utils/__init__.py +0 -0
  96. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/utils/display.py +0 -0
  97. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/CLI/utils/scaffold_text.py +0 -0
  98. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Bases/client.py +0 -0
  99. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Bases/converter.py +0 -0
  100. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Bases/errors.py +0 -0
  101. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Bases/i18n_schema.py +0 -0
  102. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Bases/manager.py +0 -0
  103. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Bases/router.py +0 -0
  104. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Bases/send_builder.py +0 -0
  105. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Bases/storage.py +0 -0
  106. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Bases/websocket.py +0 -0
  107. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Event/message_builder.py +0 -0
  108. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Event/meta.py +0 -0
  109. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Event/notice.py +0 -0
  110. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Event/request.py +0 -0
  111. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/Event/session_type.py +0 -0
  112. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/assets/__init__.py +0 -0
  113. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/assets/error.css +0 -0
  114. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/assets/error.html +0 -0
  115. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/assets/root.css +0 -0
  116. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/assets/root.html +0 -0
  117. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/i18n/__init__.py +0 -0
  118. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/i18n/constants.py +0 -0
  119. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/i18n/locales/__init__.py +0 -0
  120. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/Core/storage/__init__.py +0 -0
  121. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/__init__.py +0 -0
  122. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/__main__.py +0 -0
  123. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/finders/__init__.py +0 -0
  124. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/finders/adapter.py +0 -0
  125. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/finders/bases/__init__.py +0 -0
  126. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/finders/bases/finder.py +0 -0
  127. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/finders/module.py +0 -0
  128. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/loaders/__init__.py +0 -0
  129. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/loaders/adapter.py +0 -0
  130. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/loaders/bases/__init__.py +0 -0
  131. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/loaders/bases/loader.py +0 -0
  132. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/loaders/plugin_folder.py +0 -0
  133. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/loaders/strategy.py +0 -0
  134. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/loaders/strict.py +0 -0
  135. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/py.typed +0 -0
  136. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/runtime/config_schema.py +0 -0
  137. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/runtime/context.py +0 -0
  138. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/runtime/example_config.py +0 -0
  139. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/runtime/frame_config.py +0 -0
  140. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/runtime/hints.py +0 -0
  141. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/runtime/memory.py +0 -0
  142. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/runtime/owner_cleanup.py +0 -0
  143. {erispulse-2.8.6 → erispulse-2.9.0.dev2}/src/ErisPulse/runtime/version.py +0 -0
@@ -5,7 +5,6 @@ plugins/
5
5
  venvs
6
6
  config.full.example
7
7
  config.toml
8
- uv.lock
9
8
  snapshots/
10
9
  logs/
11
10
  config.db*
@@ -123,7 +122,7 @@ ipython_config.py
123
122
  # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
124
123
  # This is especially recommended for binary packages to ensure reproducibility, and is more
125
124
  # commonly ignored for libraries.
126
- #uv.lock
125
+ uv.lock
127
126
 
128
127
  # poetry
129
128
  # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
@@ -239,3 +238,9 @@ pyvenv.cfg
239
238
 
240
239
  # 但保留源中文文档
241
240
  !docs/zh-CN/
241
+
242
+ .zcode/
243
+
244
+ .github/assets/ErisPulseFilm1.mp4
245
+ .github/assets/score1.wav
246
+ .github/assets/score2.wav
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ErisPulse
3
- Version: 2.8.6
3
+ Version: 2.9.0-dev.2
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
@@ -24,6 +24,7 @@ Classifier: Programming Language :: Python :: 3.10
24
24
  Classifier: Programming Language :: Python :: 3.11
25
25
  Classifier: Programming Language :: Python :: 3.12
26
26
  Classifier: Programming Language :: Python :: 3.13
27
+ Classifier: Programming Language :: Python :: 3.14
27
28
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
28
29
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
29
30
  Classifier: Typing :: Typed
@@ -33,18 +34,19 @@ Requires-Dist: aiosqlite>=0.20.0
33
34
  Requires-Dist: fastapi>=0.116.1
34
35
  Requires-Dist: rich>=13.0.0
35
36
  Requires-Dist: tomlkit>=0.15.1
36
- Requires-Dist: uvicorn>=0.30.0
37
+ Requires-Dist: uvicorn>=0.35.0
37
38
  Requires-Dist: websockets>=12.0
38
39
  Provides-Extra: mysql
39
40
  Requires-Dist: aiomysql>=0.2.0; extra == 'mysql'
41
+ Requires-Dist: cryptography>=41.0.0; extra == 'mysql'
40
42
  Provides-Extra: postgres
41
43
  Requires-Dist: asyncpg>=0.29.0; extra == 'postgres'
42
44
  Provides-Extra: test
43
45
  Requires-Dist: httpx>=0.24.0; extra == 'test'
44
46
  Requires-Dist: pytest-asyncio>=0.21.0; extra == 'test'
45
- Requires-Dist: pytest-benchmark>=4.0.0; extra == 'test'
46
47
  Requires-Dist: pytest-cov>=4.0.0; extra == 'test'
47
48
  Requires-Dist: pytest-mock>=3.10.0; extra == 'test'
49
+ Requires-Dist: pytest-timeout>=2.1.0; extra == 'test'
48
50
  Requires-Dist: pytest-xdist>=3.0.0; extra == 'test'
49
51
  Requires-Dist: pytest>=7.0.0; extra == 'test'
50
52
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ErisPulse"
7
- version = "2.8.6"
7
+ version = "2.9.0-dev.2"
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"
@@ -51,6 +51,7 @@ classifiers = [
51
51
  "Programming Language :: Python :: 3.11",
52
52
  "Programming Language :: Python :: 3.12",
53
53
  "Programming Language :: Python :: 3.13",
54
+ "Programming Language :: Python :: 3.14",
54
55
  "Programming Language :: Python :: 3 :: Only",
55
56
  "Operating System :: OS Independent",
56
57
  "Topic :: Software Development :: Libraries :: Python Modules",
@@ -66,7 +67,7 @@ dependencies = [
66
67
  "websockets>=12.0",
67
68
  "aiohttp>=3.9.0",
68
69
  "fastapi>=0.116.1",
69
- "uvicorn>=0.30.0",
70
+ "uvicorn>=0.35.0",
70
71
  # storage 相关(默认 SQLite 后端异步驱动,开箱即用)
71
72
  "aiosqlite>=0.20.0",
72
73
  ]
@@ -83,6 +84,8 @@ Changelog = "https://github.com/ErisPulse/ErisPulse/blob/main/CHANGELOG.md"
83
84
  # 存储后端可选驱动:pip install ErisPulse[mysql] / ErisPulse[postgres]
84
85
  mysql = [
85
86
  "aiomysql>=0.2.0",
87
+ # MySQL 8 默认 caching_sha2_password 认证要求(缺失时连接池初始化失败)
88
+ "cryptography>=41.0.0",
86
89
  ]
87
90
  postgres = [
88
91
  "asyncpg>=0.29.0",
@@ -92,12 +95,14 @@ test = [
92
95
  "pytest-cov>=4.0.0",
93
96
  "pytest-asyncio>=0.21.0",
94
97
  "pytest-xdist>=3.0.0",
98
+ "pytest-timeout>=2.1.0",
95
99
  "pytest-mock>=3.10.0",
96
- "pytest-benchmark>=4.0.0",
97
100
  "httpx>=0.24.0",
98
101
  ]
99
102
 
100
103
  [dependency-groups]
104
+ # 测试依赖统一维护于 [project.optional-dependencies].test(公开 extras,
105
+ # 生态可 pip install ErisPulse[test]);此处的 test 组已删除以免双处漂移。
101
106
  dev = [
102
107
  "pyclean>=3.6.0",
103
108
  "ruff>=0.1.7",
@@ -105,6 +110,9 @@ dev = [
105
110
  "typing_extensions>=4.7.0; python_version < '3.11'",
106
111
  ]
107
112
 
113
+ [tool.uv]
114
+ default-groups = ["dev"]
115
+
108
116
  [project.scripts]
109
117
  ep-init = "ErisPulse.__init__:init_sync"
110
118
  epsdk = "ErisPulse.__main__:main"
@@ -130,7 +138,7 @@ include = [
130
138
  target-version = "py310"
131
139
  line-length = 120
132
140
 
133
- include = ["src/**/*.py", "tests/**/*.py"]
141
+ include = ["src/**/*.py", "tests/**/*.py", "scripts/**/*.py", "examples/**/*.py"]
134
142
 
135
143
  extend-exclude = [
136
144
  "__pycache__",
@@ -168,6 +176,7 @@ ignore = [
168
176
  "RUF003", # ambiguous-unicode-character-comment
169
177
 
170
178
  # 框架/异步代码风格性规则(强行修会破坏 API 或语义)
179
+ "B008", # function-call-in-default-argument —— 依赖注入(Depends)声明的标准形态,DI 框架惯例豁免
171
180
  "PLC0415", # import-outside-top-level —— 异步框架中 lazy import 是常用且必要的模式
172
181
  "PLR0911", # too-many-return-statements
173
182
  "PLR0912", # too-many-branches
@@ -208,6 +217,13 @@ ignore = [
208
217
 
209
218
  [tool.ruff.lint.per-file-ignores]
210
219
  "__init__.py" = ["F401", "PLC0415", "I001"] # 导出式 __init__ 经常按需导入
220
+ # 旧版生成/翻译脚本:存量风格债渐进收编(新脚本 check_i18n_locales/check_docs_links/
221
+ # release_check/_common 已全净)。TODO: 逐步修复后移除本段。
222
+ "scripts/tools/translate-docs.py" = ["PERF401", "PTH", "UP035", "B", "RET505", "FURB", "RUF013", "UP008", "A001", "SIM"]
223
+ "scripts/tools/check-translation.py" = ["PERF401", "PTH", "UP035", "B", "RET505", "FURB", "RUF013", "SIM"]
224
+ "scripts/tools/generate-api-docs.py" = ["PERF401", "PTH", "UP035", "B", "RET505", "FURB", "RUF013", "A001", "SIM", "W29", "E501"]
225
+ "scripts/tools/generate-docs-index.py" = ["PERF401", "PTH", "UP035", "B", "RET505", "FURB", "RUF013", "RUF015", "SIM"]
226
+ "scripts/tools/generate-type-stubs.py" = ["PERF401", "PTH", "UP035", "B", "RET505", "FURB", "RUF013", "ANN", "A001", "PLW0603", "SIM", "W29"]
211
227
  "tests/**/*.py" = [
212
228
  # 断言/夹具相关(测试代码天然模式)
213
229
  "S101", "PLR2004", "SLF001", "ARG", "ANN", "PTH",
@@ -282,6 +298,8 @@ python_classes = "Test*"
282
298
  python_functions = "test_*"
283
299
  minversion = "7.0"
284
300
  addopts = [
301
+ "--timeout=120",
302
+ "--timeout-method=thread",
285
303
  "-v",
286
304
  "--strict-markers",
287
305
  "--strict-config",
@@ -305,6 +305,11 @@ class CLI:
305
305
  args, unknown = self.parser.parse_known_args()
306
306
  args._unknown_args = unknown
307
307
 
308
+ # --yes/-y:assume-yes 模式,由统一交互封装(utils/interactive)消费
309
+ from .utils.interactive import set_assume_yes
310
+
311
+ set_assume_yes(bool(getattr(args, "yes", False)))
312
+
308
313
  # --no-color:禁用 Rich 控制台着色(CI / 日志采集场景)
309
314
  if getattr(args, "no_color", False):
310
315
  console.no_color = True
@@ -375,6 +380,13 @@ class CLI:
375
380
  console.print(f"\n[warning]{i18n.t('cli.run.user_interrupted')}[/]")
376
381
  sys.exit(1)
377
382
  except Exception as e:
383
+ from .utils.interactive import NonInteractiveError
384
+
385
+ if isinstance(e, NonInteractiveError):
386
+ # 非交互环境遇到无法自动应答的提示:明确报错而非 EOF 崩溃
387
+ console.print(f"[error]{e}[/]")
388
+ console.print(f"[hint]{i18n.t('cli.interactive.non_tty_hint')}[/]")
389
+ sys.exit(1)
378
390
  console.print(f"[error]{i18n.t('cli.run.exec_error', error=e)}[/]")
379
391
  # 场景化友好提示:根据异常类型给出下一步建议
380
392
  from .hints import suggest_for_exception
@@ -219,6 +219,8 @@ class Main(BaseModule):
219
219
  # 跨命令的用户黑白名单用覆写系统 ACL
220
220
  # (ErisPulse.event.overrides.acl 或 overrides.acl.set(name, allow/deny),命令名支持 glob);
221
221
  # 模块级可用性 / 事件准入 / 出站限制由作用域 scope 管理(用户可控)
222
+ # 命令支持声明式参数与选项(args= / options=):框架自动解析并按名注入处理器参数,
223
+ # 用户输入错误自动回复本地化提示与用法(类型:str/int/float/bool/literal/duration/rest)
222
224
  @command(\"hello\", help=i18n.t(\"module.{name}.command.hello.help\"))
223
225
  async def hello_command(event: Event):
224
226
  await event.reply(i18n.t(\"module.{name}.command.hello.reply\"))
@@ -95,7 +95,7 @@ class DoctorCommand(Command):
95
95
  pip_note = (
96
96
  i18n.t("cli.doctor.venv_pip_ok")
97
97
  if venv_pip.exists()
98
- else "uv(pip 不可用,uv 后端替代)"
98
+ else i18n.t("cli.doctor.venv_pip_uv")
99
99
  )
100
100
  rows.append(
101
101
  (
@@ -133,6 +133,52 @@ class DoctorCommand(Command):
133
133
  )
134
134
  failed = True
135
135
 
136
+ # 4.5 组件配置健康(配置文件存在 ≠ 必填项齐全:复用配置向导的
137
+ # schema 状态检测,"装好了跑不起来"多因适配器必填项缺失)
138
+ try:
139
+ from ..utils.config_wizard import (
140
+ STATUS_INCOMPLETE,
141
+ get_target_status,
142
+ load_config_targets,
143
+ )
144
+
145
+ incomplete: list[str] = []
146
+ for target in load_config_targets():
147
+ status, _errors = get_target_status(target)
148
+ # 仅"待完善"(开了但必填项缺失)判 FAIL;完全未配置属
149
+ # 用户尚未开始配置,不算故障
150
+ if status == STATUS_INCOMPLETE:
151
+ incomplete.append(target.name)
152
+ if incomplete:
153
+ failed = True
154
+ rows.append(
155
+ (
156
+ self._status(False),
157
+ i18n.t("cli.doctor.component_config"),
158
+ i18n.t(
159
+ "cli.doctor.component_config_incomplete",
160
+ names=", ".join(incomplete),
161
+ ),
162
+ )
163
+ )
164
+ else:
165
+ rows.append(
166
+ (
167
+ self._status(True),
168
+ i18n.t("cli.doctor.component_config"),
169
+ i18n.t("cli.doctor.component_config_ok"),
170
+ )
171
+ )
172
+ except Exception:
173
+ # 发现失败(如依赖缺失)不阻塞其余诊断项
174
+ rows.append(
175
+ (
176
+ self._status(True),
177
+ i18n.t("cli.doctor.component_config"),
178
+ i18n.t("cli.doctor.component_config_skipped"),
179
+ )
180
+ )
181
+
136
182
  # 5. PyPI 连通性
137
183
  try:
138
184
  remote = asyncio.run(pm.get_remote_packages())
@@ -11,7 +11,6 @@ import sys
11
11
  from argparse import ArgumentParser
12
12
  from pathlib import Path
13
13
 
14
- from rich.prompt import Confirm, IntPrompt
15
14
  from rich.text import Text
16
15
 
17
16
  from ..base import Command
@@ -19,6 +18,7 @@ from ..console import console
19
18
  from ..i18n import i18n
20
19
  from ..utils import PackageManager
21
20
  from ..utils.display import _input, prompt_validated, section_header
21
+ from ..utils.interactive import confirm, select
22
22
  from ..utils.package_manager import (
23
23
  append_pyproject_dependencies,
24
24
  create_project_venv,
@@ -432,7 +432,11 @@ class InitCommand(Command):
432
432
  f" [bold]2.[/] {i18n.t('cli.init.location_option_new')} [dim]— {i18n.t('cli.init.location_desc_new')}[/]"
433
433
  )
434
434
  console.print()
435
- location_choice = IntPrompt.ask(i18n.t("cli.create.select_prompt"), default=2, choices=["1", "2"])
435
+ location_choice = int(
436
+ select(
437
+ i18n.t("cli.create.select_prompt"), choices=["1", "2"], default="2"
438
+ )
439
+ )
436
440
  console.print()
437
441
  in_current_dir = location_choice == 1
438
442
 
@@ -458,7 +462,7 @@ class InitCommand(Command):
458
462
  project_name = p.name
459
463
  project_path = (Path(target_dir) if target_dir else Path()) / project_name
460
464
  if project_path.exists() and not force:
461
- if not Confirm.ask(
465
+ if not confirm(
462
466
  f" [cyan]{i18n.t('cli.init.dir_overwrite_prompt', name=project_name)}[/]",
463
467
  default=False,
464
468
  ):
@@ -467,10 +471,10 @@ class InitCommand(Command):
467
471
 
468
472
  # 虚拟环境与 git 仓库问询
469
473
  if not getattr(self, "no_venv", False):
470
- create_venv = Confirm.ask(
474
+ create_venv = confirm(
471
475
  f" [cyan]{i18n.t('cli.init.venv_prompt')}[/]", default=True
472
476
  )
473
- git_init = Confirm.ask(
477
+ git_init = confirm(
474
478
  f" [cyan]{i18n.t('cli.init.git_prompt')}[/]", default=False
475
479
  )
476
480
 
@@ -521,7 +525,7 @@ class InitCommand(Command):
521
525
  console.print(f"[warning] {i18n.t('cli.init.invalid_port', port=new_port)}[/]")
522
526
  new_port = _input(">")
523
527
 
524
- if Confirm.ask(
528
+ if confirm(
525
529
  f"\n [cyan]{i18n.t('cli.init.configure_adapters_prompt')}[/]",
526
530
  default=True,
527
531
  ):
@@ -587,7 +591,7 @@ class InitCommand(Command):
587
591
 
588
592
  console.print(f"[dim] {i18n.t('cli.init.adapters_enabled', count=len(enabled))}[/]")
589
593
 
590
- if enabled and Confirm.ask(f" [cyan]{i18n.t('cli.init.install_selected_prompt')}[/]", default=True):
594
+ if enabled and confirm(f" [cyan]{i18n.t('cli.init.install_selected_prompt')}[/]", default=True):
591
595
  self._install_adapters(enabled, adapters)
592
596
 
593
597
  def _install_adapters(self, adapter_names, adapters_info):
@@ -8,7 +8,7 @@ import asyncio
8
8
  import sys
9
9
  from argparse import ArgumentParser
10
10
 
11
- from rich.prompt import Confirm, Prompt
11
+ from rich.prompt import Prompt
12
12
  from rich.text import Text
13
13
 
14
14
  from ..base import Command
@@ -16,6 +16,7 @@ from ..console import console
16
16
  from ..i18n import i18n
17
17
  from ..utils import PackageManager, config_wizard
18
18
  from ..utils.display import interactive_select_table
19
+ from ..utils.interactive import confirm
19
20
  from ..utils.package_manager import (
20
21
  resolve_target_python,
21
22
  warn_if_uv_isolated,
@@ -251,6 +252,7 @@ class InstallCommand(Command):
251
252
  if success:
252
253
  # 非交互路径不进入向导,仅提示后续配置入口
253
254
  console.print(f"[dim] {i18n.t('cli.install.configure_hint')}[/]")
255
+ console.print(f"[hint] {i18n.t('cli.install.done_hint')}[/]")
254
256
 
255
257
  if not success:
256
258
  sys.exit(1)
@@ -294,7 +296,7 @@ class InstallCommand(Command):
294
296
  elif choice == "4":
295
297
  self._install_custom(upgrade, pre)
296
298
 
297
- if not Confirm.ask(f"\n [cyan]{i18n.t('cli.install.continue_install')}[/]", default=False):
299
+ if not confirm(f"\n [cyan]{i18n.t('cli.install.continue_install')}[/]", default=False):
298
300
  break
299
301
 
300
302
  def _install_adapters(self, remote_packages: dict, upgrade: bool, pre: bool):
@@ -341,7 +343,7 @@ class InstallCommand(Command):
341
343
 
342
344
  selected_names = [name for name, _ in selected]
343
345
  console.print(f"\n [dim]{i18n.t('cli.install.selected', selected=', '.join(selected_names))}[/]")
344
- if Confirm.ask(
346
+ if confirm(
345
347
  f" [cyan]{i18n.t('cli.install.confirm_adapters', count=len(selected_names))}[/]",
346
348
  default=True,
347
349
  ):
@@ -390,7 +392,7 @@ class InstallCommand(Command):
390
392
 
391
393
  selected_names = [name for name, _ in selected]
392
394
  console.print(f"\n [dim]{i18n.t('cli.install.selected', selected=', '.join(selected_names))}[/]")
393
- if Confirm.ask(
395
+ if confirm(
394
396
  f" [cyan]{i18n.t('cli.install.confirm_modules', count=len(selected_names))}[/]",
395
397
  default=True,
396
398
  ):
@@ -495,7 +497,7 @@ class InstallCommand(Command):
495
497
  except ValueError:
496
498
  continue
497
499
 
498
- if selected and Confirm.ask(
500
+ if selected and confirm(
499
501
  f" [cyan]{i18n.t('cli.install.confirm_search_install', count=len(selected))}[/]",
500
502
  default=True,
501
503
  ):
@@ -514,7 +516,7 @@ class InstallCommand(Command):
514
516
  if package_name.lower() == "q":
515
517
  return
516
518
  if package_name:
517
- if Confirm.ask(
519
+ if confirm(
518
520
  f" [cyan]{i18n.t('cli.install.confirm_custom_install', package_name=package_name)}[/]",
519
521
  default=True,
520
522
  ):
@@ -534,6 +536,7 @@ class InstallCommand(Command):
534
536
  """
535
537
  if not config_wizard.is_interactive():
536
538
  console.print(f"[dim] {i18n.t('cli.install.configure_hint')}[/]")
539
+ console.print(f"[hint] {i18n.t('cli.install.done_hint')}[/]")
537
540
  return
538
541
 
539
542
  resolved = []
@@ -545,3 +548,4 @@ class InstallCommand(Command):
545
548
  config_wizard.post_install_configure(resolved)
546
549
  except Exception as e:
547
550
  console.print(f"[warning] {i18n.t('cli.config.post_install_failed', error=e)}[/]")
551
+ console.print(f"[hint] {i18n.t('cli.install.done_hint')}[/]")
@@ -56,6 +56,7 @@ class ListCommand(Command):
56
56
 
57
57
  # 仅在需要时一次性拉取远程索引,避免逐包重复 asyncio.run / 网络请求
58
58
  remote_packages = None
59
+ remote_unavailable = False
59
60
  if outdated_only:
60
61
  try:
61
62
  remote_packages = asyncio.run(
@@ -63,15 +64,29 @@ class ListCommand(Command):
63
64
  )
64
65
  except Exception:
65
66
  remote_packages = None
67
+ # 索引拉取失败(异常或双源均空):区分"全是最新"与"无法检查"
68
+ remote_unavailable = remote_packages is None or not (
69
+ remote_packages.get("modules") or remote_packages.get("adapters")
70
+ )
66
71
 
67
72
  if pkg_type == "all":
68
- self._print_installed_packages("modules", outdated_only, remote_packages)
69
- self._print_installed_packages("adapters", outdated_only, remote_packages)
73
+ self._print_installed_packages(
74
+ "modules", outdated_only, remote_packages, remote_unavailable
75
+ )
76
+ self._print_installed_packages(
77
+ "adapters", outdated_only, remote_packages, remote_unavailable
78
+ )
70
79
  else:
71
- self._print_installed_packages(pkg_type, outdated_only, remote_packages)
80
+ self._print_installed_packages(
81
+ pkg_type, outdated_only, remote_packages, remote_unavailable
82
+ )
72
83
 
73
84
  def _print_installed_packages(
74
- self, pkg_type: str, outdated_only: bool = False, remote_packages: dict | None = None
85
+ self,
86
+ pkg_type: str,
87
+ outdated_only: bool = False,
88
+ remote_packages: dict | None = None,
89
+ remote_unavailable: bool = False,
75
90
  ):
76
91
  """
77
92
  以表格形式打印已安装的模块或适配器
@@ -79,6 +94,7 @@ class ListCommand(Command):
79
94
  :param pkg_type: [str] 组件类型 (modules 或 adapters)
80
95
  :param outdated_only: [bool] 是否仅显示可升级的包 (默认: False)
81
96
  :param remote_packages: [Optional[dict]] 预取的远程索引,避免逐包重复拉取 (默认: None)
97
+ :param remote_unavailable: [bool] 远程索引是否不可达(空结果区分文案用,默认: False)
82
98
  """
83
99
  installed = self.package_manager.get_installed_packages()
84
100
 
@@ -121,6 +137,10 @@ class ListCommand(Command):
121
137
  )
122
138
  # 展示模块注册的脚本入口
123
139
  self._print_package_scripts(installed["modules"])
140
+ elif outdated_only and remote_unavailable:
141
+ console.print(f"[dim] {i18n.t('cli.list.outdated_unavailable')}[/]")
142
+ elif outdated_only:
143
+ console.print(f"[dim] {i18n.t('cli.list.no_outdated')}[/]")
124
144
  else:
125
145
  console.print(f"[dim] {i18n.t('cli.list.no_modules')}[/]")
126
146
  console.print(f"[dim] {i18n.t('cli.list.empty_hint')}[/]")
@@ -134,6 +154,7 @@ class ListCommand(Command):
134
154
  )
135
155
  table.add_column(i18n.t("cli.list.header_package"), min_width=20)
136
156
  table.add_column(i18n.t("cli.list.header_version"), width=10)
157
+ table.add_column(i18n.t("cli.list.header_status"), width=8)
137
158
  table.add_column(i18n.t("cli.list.header_desc"))
138
159
 
139
160
  count = 0
@@ -142,10 +163,16 @@ class ListCommand(Command):
142
163
  info["package"], info["version"], remote_packages
143
164
  ):
144
165
  continue
166
+ status = (
167
+ f"[green]{i18n.t('cli.list.status_enabled')}[/]"
168
+ if info.get("enabled", True)
169
+ else f"[yellow]{i18n.t('cli.list.status_disabled')}[/]"
170
+ )
145
171
  table.add_row(
146
172
  name,
147
173
  info["package"],
148
174
  info["version"],
175
+ status,
149
176
  info["summary"],
150
177
  )
151
178
  count += 1
@@ -155,6 +182,10 @@ class ListCommand(Command):
155
182
  console.print(
156
183
  f"[dim] {i18n.t('cli.list.count_adapters', count=count)}[/]"
157
184
  )
185
+ elif outdated_only and remote_unavailable:
186
+ console.print(f"[dim] {i18n.t('cli.list.outdated_unavailable')}[/]")
187
+ elif outdated_only:
188
+ console.print(f"[dim] {i18n.t('cli.list.no_outdated')}[/]")
158
189
  else:
159
190
  console.print(f"[dim] {i18n.t('cli.list.no_adapters')}[/]")
160
191
  console.print(f"[dim] {i18n.t('cli.list.empty_hint')}[/]")
@@ -182,12 +213,15 @@ class ListCommand(Command):
182
213
  """
183
214
  if remote_packages is None:
184
215
  remote_packages = asyncio.run(self.package_manager.get_remote_packages())
216
+ # 仅"远端更新"才算可升级:本地 dev/git 安装版本比远端新时不再误报
217
+ from ...runtime.version import compare_versions
218
+
185
219
  for module_info in remote_packages["modules"].values():
186
220
  if module_info["package"] == package_name:
187
- return module_info["version"] != current_version
221
+ return compare_versions(module_info["version"], current_version) > 0
188
222
  for adapter_info in remote_packages["adapters"].values():
189
223
  if adapter_info["package"] == package_name:
190
- return adapter_info["version"] != current_version
224
+ return compare_versions(adapter_info["version"], current_version) > 0
191
225
  return False
192
226
 
193
227
  def _print_package_scripts(self, packages: dict) -> None:
@@ -19,7 +19,14 @@ from rich.panel import Panel
19
19
  from ...runtime.file_watcher import FileSystemEventHandler, PollingObserver
20
20
  from ..base import Command
21
21
  from ..console import console
22
- from ..constants import ENV_SUPERVISED, HARD_RESTART_EXIT_CODE
22
+ from ..constants import (
23
+ ENV_SUPERVISED,
24
+ HARD_RESTART_EXIT_CODE,
25
+ RUN_CRASH_BACKOFF_MAX_SECS,
26
+ RUN_CRASH_BACKOFF_PER_CRASH_SECS,
27
+ RUN_PERSISTENT_CRASH_HINT_THRESHOLD,
28
+ RUN_RESTART_PAUSE_SECS,
29
+ )
23
30
  from ..i18n import i18n
24
31
  from ..utils.package_manager import (
25
32
  resolve_target_python,
@@ -152,7 +159,6 @@ class RunCommand(Command):
152
159
  self._run_internal(reload_mode)
153
160
 
154
161
  _RESTART_EXIT_CODE = HARD_RESTART_EXIT_CODE
155
- _MAX_CRASH_BACKOFF = 60.0
156
162
 
157
163
  def _run_internal(self, reload_mode: bool):
158
164
  """
@@ -200,6 +206,11 @@ class RunCommand(Command):
200
206
  ]
201
207
 
202
208
  crash_count = 0
209
+ # 连续同退出码崩溃计数:达到阈值输出一次排查指引(确定性故障
210
+ # 退避重试无意义,与其反复失败不如引导留现场)
211
+ consecutive_same_code = 0
212
+ last_crash_code: int | None = None
213
+ crash_hint_shown = False
203
214
  process = None
204
215
  try:
205
216
  while True:
@@ -213,7 +224,7 @@ class RunCommand(Command):
213
224
  if process.returncode == self._RESTART_EXIT_CODE:
214
225
  console.print(f"[info]{i18n.t('cli.run.restart_request')}[/]")
215
226
  crash_count = 0
216
- time.sleep(0.5)
227
+ time.sleep(RUN_RESTART_PAUSE_SECS)
217
228
  continue
218
229
 
219
230
  if process.returncode == 0:
@@ -224,7 +235,21 @@ class RunCommand(Command):
224
235
  # 非硬重启/非正常退出码:模块/适配器内部错误导致子进程异常终止
225
236
  # 不退出主进程,等待后自动重试
226
237
  crash_count += 1
227
- backoff = min(self._MAX_CRASH_BACKOFF, 3.0 * crash_count)
238
+ if process.returncode == last_crash_code:
239
+ consecutive_same_code += 1
240
+ else:
241
+ consecutive_same_code = 1
242
+ last_crash_code = process.returncode
243
+ if (
244
+ not crash_hint_shown
245
+ and RUN_PERSISTENT_CRASH_HINT_THRESHOLD > 0
246
+ and consecutive_same_code >= RUN_PERSISTENT_CRASH_HINT_THRESHOLD
247
+ ):
248
+ crash_hint_shown = True
249
+ console.print(
250
+ f"[hint]{i18n.t('cli.run.persistent_crash_hint', count=consecutive_same_code, code=process.returncode)}[/]"
251
+ )
252
+ backoff = min(RUN_CRASH_BACKOFF_MAX_SECS, RUN_CRASH_BACKOFF_PER_CRASH_SECS * crash_count)
228
253
  console.print(
229
254
  f"[warning]{i18n.t('cli.run.process_crashed', code=process.returncode)}[/]"
230
255
  )
@@ -9,7 +9,7 @@ import sys
9
9
  from argparse import ArgumentParser
10
10
 
11
11
  from rich.box import SIMPLE
12
- from rich.prompt import Confirm, Prompt
12
+ from rich.prompt import Prompt
13
13
  from rich.table import Table
14
14
  from rich.text import Text
15
15
 
@@ -18,6 +18,7 @@ from ..console import console
18
18
  from ..i18n import i18n
19
19
  from ..utils import PackageManager
20
20
  from ..utils.display import _input, section_header
21
+ from ..utils.interactive import confirm
21
22
 
22
23
 
23
24
  class SelfUpdateCommand(Command):
@@ -82,7 +83,7 @@ class SelfUpdateCommand(Command):
82
83
  )
83
84
  sys.exit(0)
84
85
  elif not args.force:
85
- if not Confirm.ask(
86
+ if not confirm(
86
87
  i18n.t(
87
88
  "cli.self_update.confirm_update",
88
89
  current=current_version,
@@ -114,7 +115,7 @@ class SelfUpdateCommand(Command):
114
115
  console.print(
115
116
  f"[warning]{i18n.t('cli.self_update.version_not_found', version=specified_version)}[/]"
116
117
  )
117
- if not Confirm.ask(
118
+ if not confirm(
118
119
  i18n.t("cli.self_update.confirm_continue"), default=False
119
120
  ):
120
121
  return None
@@ -197,7 +198,7 @@ class SelfUpdateCommand(Command):
197
198
  console.print(
198
199
  f"[warning]{i18n.t('cli.self_update.version_not_found', version=target_version)}[/]"
199
200
  )
200
- if not Confirm.ask(
201
+ if not confirm(
201
202
  i18n.t("cli.self_update.confirm_continue"), default=False
202
203
  ):
203
204
  return None
@@ -7,11 +7,11 @@ Upgrade 命令实现
7
7
  import sys
8
8
  from argparse import ArgumentParser
9
9
 
10
- from rich.prompt import Confirm
11
-
12
10
  from ..base import Command
11
+ from ..console import console
13
12
  from ..i18n import i18n
14
13
  from ..utils import PackageManager
14
+ from ..utils.interactive import confirm
15
15
  from ..utils.package_manager import resolve_target_python, warn_if_uv_isolated
16
16
 
17
17
 
@@ -50,15 +50,21 @@ class UpgradeCommand(Command):
50
50
  def execute(self, args):
51
51
  warn_if_uv_isolated()
52
52
  self.package_manager.no_uv = getattr(args, "no_uv", False)
53
+ upgraded = False
53
54
  if args.package:
54
55
  # 升级指定包
55
56
  success = self.package_manager.upgrade_package(args.package, pre=args.pre)
56
57
  if not success:
57
58
  sys.exit(1)
59
+ upgraded = True
58
60
  # 升级所有包
59
- elif args.force or Confirm.ask(
61
+ elif args.force or confirm(
60
62
  i18n.t("cli.upgrade.confirm_all"), default=False
61
63
  ):
62
64
  success = self.package_manager.upgrade_all()
63
65
  if not success:
64
66
  sys.exit(1)
67
+ upgraded = True
68
+ if upgraded:
69
+ # 生效方式提示:升级只更新磁盘代码,运行中的进程需重启或热重载
70
+ console.print(f"[hint]{i18n.t('cli.upgrade.done_hint')}[/]")