MaiBot 1.3.8__py3-none-any.whl

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 (716) hide show
  1. maibot-1.3.8.dist-info/METADATA +245 -0
  2. maibot-1.3.8.dist-info/RECORD +716 -0
  3. maibot-1.3.8.dist-info/WHEEL +5 -0
  4. maibot-1.3.8.dist-info/entry_points.txt +2 -0
  5. maibot-1.3.8.dist-info/licenses/LICENSE +674 -0
  6. maibot-1.3.8.dist-info/top_level.txt +1 -0
  7. src/A_memorix/LICENSE +661 -0
  8. src/A_memorix/LICENSE-MAIBOT-GPL.md +22 -0
  9. src/A_memorix/__init__.py +5 -0
  10. src/A_memorix/config_schema.json +1640 -0
  11. src/A_memorix/core/__init__.py +83 -0
  12. src/A_memorix/core/embedding/__init__.py +17 -0
  13. src/A_memorix/core/embedding/api_adapter.py +650 -0
  14. src/A_memorix/core/embedding/manager.py +506 -0
  15. src/A_memorix/core/embedding/presets.py +68 -0
  16. src/A_memorix/core/image/__init__.py +6 -0
  17. src/A_memorix/core/image/asset_store.py +141 -0
  18. src/A_memorix/core/image/component_paths.py +36 -0
  19. src/A_memorix/core/image/embedder.py +129 -0
  20. src/A_memorix/core/image/fingerprints.py +36 -0
  21. src/A_memorix/core/image/runtime.py +900 -0
  22. src/A_memorix/core/retrieval/__init__.py +66 -0
  23. src/A_memorix/core/retrieval/dual_path.py +3272 -0
  24. src/A_memorix/core/retrieval/graph_relation_recall.py +285 -0
  25. src/A_memorix/core/retrieval/pagerank.py +471 -0
  26. src/A_memorix/core/retrieval/posterior_graph.py +787 -0
  27. src/A_memorix/core/retrieval/score_calibration.py +180 -0
  28. src/A_memorix/core/retrieval/sparse_bm25.py +717 -0
  29. src/A_memorix/core/retrieval/threshold.py +404 -0
  30. src/A_memorix/core/runtime/__init__.py +26 -0
  31. src/A_memorix/core/runtime/admin_contracts.py +293 -0
  32. src/A_memorix/core/runtime/kernel_compat.py +269 -0
  33. src/A_memorix/core/runtime/lifecycle_orchestrator.py +368 -0
  34. src/A_memorix/core/runtime/models.py +27 -0
  35. src/A_memorix/core/runtime/runtime_facade.py +141 -0
  36. src/A_memorix/core/runtime/runtime_writer_lock.py +105 -0
  37. src/A_memorix/core/runtime/sdk_memory_kernel.py +1907 -0
  38. src/A_memorix/core/runtime/search_runtime_initializer.py +290 -0
  39. src/A_memorix/core/runtime/services/__init__.py +63 -0
  40. src/A_memorix/core/runtime/services/background_task_service.py +477 -0
  41. src/A_memorix/core/runtime/services/base.py +28 -0
  42. src/A_memorix/core/runtime/services/bundle_admin_service.py +2200 -0
  43. src/A_memorix/core/runtime/services/chat_filter_service.py +88 -0
  44. src/A_memorix/core/runtime/services/correction_admin_service.py +1274 -0
  45. src/A_memorix/core/runtime/services/delete_admin_service.py +2018 -0
  46. src/A_memorix/core/runtime/services/dual_vector_migration_service.py +109 -0
  47. src/A_memorix/core/runtime/services/dual_vector_state_service.py +503 -0
  48. src/A_memorix/core/runtime/services/embedding_state_service.py +515 -0
  49. src/A_memorix/core/runtime/services/episode_admin_service.py +104 -0
  50. src/A_memorix/core/runtime/services/fact_admin_service.py +199 -0
  51. src/A_memorix/core/runtime/services/feedback_correction_service.py +1523 -0
  52. src/A_memorix/core/runtime/services/graph_admin_service.py +1521 -0
  53. src/A_memorix/core/runtime/services/image_service.py +158 -0
  54. src/A_memorix/core/runtime/services/import_tuning_admin_service.py +158 -0
  55. src/A_memorix/core/runtime/services/ingest_service.py +787 -0
  56. src/A_memorix/core/runtime/services/memory_maintenance_service.py +404 -0
  57. src/A_memorix/core/runtime/services/memory_search_service.py +373 -0
  58. src/A_memorix/core/runtime/services/profile_admin_service.py +646 -0
  59. src/A_memorix/core/runtime/services/request_dedup_service.py +34 -0
  60. src/A_memorix/core/runtime/services/runtime_config_service.py +94 -0
  61. src/A_memorix/core/runtime/services/runtime_dependency_service.py +105 -0
  62. src/A_memorix/core/runtime/services/runtime_lifecycle_service.py +409 -0
  63. src/A_memorix/core/runtime/services/search_hit_processing_service.py +891 -0
  64. src/A_memorix/core/runtime/services/source_admin_service.py +52 -0
  65. src/A_memorix/core/runtime/services/stats_service.py +35 -0
  66. src/A_memorix/core/runtime/services/summary_service.py +50 -0
  67. src/A_memorix/core/runtime/services/v5_admin_service.py +326 -0
  68. src/A_memorix/core/runtime/services/vector_delete_service.py +30 -0
  69. src/A_memorix/core/runtime/services/vector_rebuild_checkpoint.py +65 -0
  70. src/A_memorix/core/runtime/services/vector_recovery_service.py +460 -0
  71. src/A_memorix/core/runtime/services/vector_runtime_service.py +1417 -0
  72. src/A_memorix/core/runtime/services/vector_space_service.py +251 -0
  73. src/A_memorix/core/storage/__init__.py +62 -0
  74. src/A_memorix/core/storage/format_migration.py +676 -0
  75. src/A_memorix/core/storage/graph_store.py +1733 -0
  76. src/A_memorix/core/storage/knowledge_types.py +183 -0
  77. src/A_memorix/core/storage/metadata_cleanup.py +913 -0
  78. src/A_memorix/core/storage/metadata_episode.py +1659 -0
  79. src/A_memorix/core/storage/metadata_fact.py +1410 -0
  80. src/A_memorix/core/storage/metadata_feedback.py +745 -0
  81. src/A_memorix/core/storage/metadata_fts.py +937 -0
  82. src/A_memorix/core/storage/metadata_image.py +1076 -0
  83. src/A_memorix/core/storage/metadata_profile.py +782 -0
  84. src/A_memorix/core/storage/metadata_schema.py +2503 -0
  85. src/A_memorix/core/storage/metadata_store.py +4565 -0
  86. src/A_memorix/core/storage/sqlite_connection.py +176 -0
  87. src/A_memorix/core/storage/tokenizer_runtime.py +9 -0
  88. src/A_memorix/core/storage/transaction.py +42 -0
  89. src/A_memorix/core/storage/type_detection.py +137 -0
  90. src/A_memorix/core/storage/value_coercion.py +10 -0
  91. src/A_memorix/core/storage/vector_store.py +2851 -0
  92. src/A_memorix/core/strategies/__init__.py +0 -0
  93. src/A_memorix/core/strategies/base.py +92 -0
  94. src/A_memorix/core/strategies/chat_log.py +80 -0
  95. src/A_memorix/core/strategies/factual.py +104 -0
  96. src/A_memorix/core/strategies/narrative.py +148 -0
  97. src/A_memorix/core/strategies/quote.py +47 -0
  98. src/A_memorix/core/utils/__init__.py +33 -0
  99. src/A_memorix/core/utils/aggregate_query_service.py +359 -0
  100. src/A_memorix/core/utils/episode_retrieval_service.py +222 -0
  101. src/A_memorix/core/utils/episode_segmentation_service.py +322 -0
  102. src/A_memorix/core/utils/episode_service.py +646 -0
  103. src/A_memorix/core/utils/feedback_policy.py +149 -0
  104. src/A_memorix/core/utils/hash.py +128 -0
  105. src/A_memorix/core/utils/import_payloads.py +222 -0
  106. src/A_memorix/core/utils/io.py +85 -0
  107. src/A_memorix/core/utils/matcher.py +175 -0
  108. src/A_memorix/core/utils/memory_lifecycle_policy.py +336 -0
  109. src/A_memorix/core/utils/metadata.py +11 -0
  110. src/A_memorix/core/utils/model_routing.py +239 -0
  111. src/A_memorix/core/utils/monitor.py +188 -0
  112. src/A_memorix/core/utils/path_fallback_service.py +182 -0
  113. src/A_memorix/core/utils/person_profile_service.py +1277 -0
  114. src/A_memorix/core/utils/plugin_id_policy.py +26 -0
  115. src/A_memorix/core/utils/profile_evidence.py +25 -0
  116. src/A_memorix/core/utils/profile_policy.py +45 -0
  117. src/A_memorix/core/utils/profile_text.py +204 -0
  118. src/A_memorix/core/utils/quantization.py +343 -0
  119. src/A_memorix/core/utils/relation_query.py +121 -0
  120. src/A_memorix/core/utils/relation_write_service.py +504 -0
  121. src/A_memorix/core/utils/retrieval_tuning_manager.py +2369 -0
  122. src/A_memorix/core/utils/runtime_payloads.py +132 -0
  123. src/A_memorix/core/utils/runtime_self_check.py +303 -0
  124. src/A_memorix/core/utils/search_execution_service.py +445 -0
  125. src/A_memorix/core/utils/search_postprocess.py +91 -0
  126. src/A_memorix/core/utils/summary_importer.py +991 -0
  127. src/A_memorix/core/utils/time_parser.py +156 -0
  128. src/A_memorix/core/utils/web_import_manager.py +4511 -0
  129. src/A_memorix/host_service.py +1018 -0
  130. src/A_memorix/paths.py +58 -0
  131. src/A_memorix/plugin.py +321 -0
  132. src/A_memorix/runtime_registry.py +30 -0
  133. src/A_memorix/scripts/_bootstrap.py +22 -0
  134. src/A_memorix/scripts/audit_vector_consistency.py +205 -0
  135. src/A_memorix/scripts/backfill_relation_vectors.py +265 -0
  136. src/A_memorix/scripts/backfill_temporal_metadata.py +67 -0
  137. src/A_memorix/scripts/convert_lpmm.py +537 -0
  138. src/A_memorix/scripts/import_lpmm_json.py +189 -0
  139. src/A_memorix/scripts/migrate_chat_history.py +101 -0
  140. src/A_memorix/scripts/migrate_maibot_memory.py +1955 -0
  141. src/A_memorix/scripts/migrate_person_memory_points.py +113 -0
  142. src/A_memorix/scripts/process_knowledge.py +831 -0
  143. src/A_memorix/scripts/rebuild_episodes.py +207 -0
  144. src/A_memorix/scripts/release_vnext_migrate.py +875 -0
  145. src/A_memorix/scripts/runtime_self_check.py +144 -0
  146. src/__init__.py +14 -0
  147. src/_resources/EULA.md +135 -0
  148. src/_resources/PRIVACY.md +60 -0
  149. src/_resources/changelogs/changelog.md +918 -0
  150. src/_resources/locales/en-US/config.json +39 -0
  151. src/_resources/locales/en-US/core.json +8 -0
  152. src/_resources/locales/en-US/prompts.json +8 -0
  153. src/_resources/locales/en-US/startup.json +98 -0
  154. src/_resources/locales/ja-JP/config.json +39 -0
  155. src/_resources/locales/ja-JP/core.json +8 -0
  156. src/_resources/locales/ja-JP/prompts.json +8 -0
  157. src/_resources/locales/ja-JP/startup.json +98 -0
  158. src/_resources/locales/ko/config.json +39 -0
  159. src/_resources/locales/ko/core.json +8 -0
  160. src/_resources/locales/ko/prompts.json +8 -0
  161. src/_resources/locales/ko/startup.json +98 -0
  162. src/_resources/locales/zh-CN/config.json +39 -0
  163. src/_resources/locales/zh-CN/core.json +8 -0
  164. src/_resources/locales/zh-CN/prompts.json +8 -0
  165. src/_resources/locales/zh-CN/startup.json +98 -0
  166. src/_resources/prompts/en-US/.meta.toml +164 -0
  167. src/_resources/prompts/en-US/a_memorix_chat_summary.prompt +45 -0
  168. src/_resources/prompts/en-US/emoji_content_analysis.prompt +3 -0
  169. src/_resources/prompts/en-US/emoji_content_filtration.prompt +6 -0
  170. src/_resources/prompts/en-US/emoji_replace.prompt +12 -0
  171. src/_resources/prompts/en-US/emoji_selection.prompt +6 -0
  172. src/_resources/prompts/en-US/expression_evaluation.prompt +14 -0
  173. src/_resources/prompts/en-US/heuristic_memory_impression.prompt +15 -0
  174. src/_resources/prompts/en-US/image_description.prompt +1 -0
  175. src/_resources/prompts/en-US/jargon_compare_inference.prompt +14 -0
  176. src/_resources/prompts/en-US/jargon_inference_content_only.prompt +11 -0
  177. src/_resources/prompts/en-US/jargon_inference_with_context.prompt +19 -0
  178. src/_resources/prompts/en-US/learn_jargon.prompt +31 -0
  179. src/_resources/prompts/en-US/learn_style.prompt +28 -0
  180. src/_resources/prompts/en-US/maisaka_chat.prompt +28 -0
  181. src/_resources/prompts/en-US/maisaka_chat_focus.prompt +39 -0
  182. src/_resources/prompts/en-US/maisaka_chat_optimized.prompt +19 -0
  183. src/_resources/prompts/en-US/maisaka_replyer.prompt +10 -0
  184. src/_resources/prompts/en-US/memory_fuzzy_modify_plan.prompt +45 -0
  185. src/_resources/prompts/en-US/mid_term_memory_summary.prompt +10 -0
  186. src/_resources/prompts/en-US/reply_attachments.prompt +3 -0
  187. src/_resources/prompts/en-US/retro_private_replyer.prompt +13 -0
  188. src/_resources/prompts/en-US/retro_private_replyer_self.prompt +13 -0
  189. src/_resources/prompts/en-US/retro_replyer.prompt +17 -0
  190. src/_resources/prompts/en-US/retro_replyer_light.prompt +17 -0
  191. src/_resources/prompts/en-US/webui_ai_search.prompt +24 -0
  192. src/_resources/prompts/en-US/webui_ai_search_correction.prompt +1 -0
  193. src/_resources/prompts/en-US/webui_ai_search_evidence.prompt +3 -0
  194. src/_resources/prompts/en-US/webui_ai_search_final.prompt +1 -0
  195. src/_resources/prompts/ja-JP/.meta.toml +164 -0
  196. src/_resources/prompts/ja-JP/a_memorix_chat_summary.prompt +45 -0
  197. src/_resources/prompts/ja-JP/emoji_content_analysis.prompt +3 -0
  198. src/_resources/prompts/ja-JP/emoji_content_filtration.prompt +6 -0
  199. src/_resources/prompts/ja-JP/emoji_replace.prompt +12 -0
  200. src/_resources/prompts/ja-JP/emoji_selection.prompt +6 -0
  201. src/_resources/prompts/ja-JP/expression_evaluation.prompt +14 -0
  202. src/_resources/prompts/ja-JP/heuristic_memory_impression.prompt +15 -0
  203. src/_resources/prompts/ja-JP/image_description.prompt +1 -0
  204. src/_resources/prompts/ja-JP/jargon_compare_inference.prompt +14 -0
  205. src/_resources/prompts/ja-JP/jargon_inference_content_only.prompt +11 -0
  206. src/_resources/prompts/ja-JP/jargon_inference_with_context.prompt +19 -0
  207. src/_resources/prompts/ja-JP/learn_jargon.prompt +31 -0
  208. src/_resources/prompts/ja-JP/learn_style.prompt +28 -0
  209. src/_resources/prompts/ja-JP/maisaka_chat.prompt +28 -0
  210. src/_resources/prompts/ja-JP/maisaka_chat_focus.prompt +39 -0
  211. src/_resources/prompts/ja-JP/maisaka_chat_optimized.prompt +19 -0
  212. src/_resources/prompts/ja-JP/maisaka_replyer.prompt +10 -0
  213. src/_resources/prompts/ja-JP/memory_fuzzy_modify_plan.prompt +45 -0
  214. src/_resources/prompts/ja-JP/mid_term_memory_summary.prompt +10 -0
  215. src/_resources/prompts/ja-JP/reply_attachments.prompt +3 -0
  216. src/_resources/prompts/ja-JP/retro_private_replyer.prompt +13 -0
  217. src/_resources/prompts/ja-JP/retro_private_replyer_self.prompt +13 -0
  218. src/_resources/prompts/ja-JP/retro_replyer.prompt +17 -0
  219. src/_resources/prompts/ja-JP/retro_replyer_light.prompt +17 -0
  220. src/_resources/prompts/ja-JP/webui_ai_search.prompt +24 -0
  221. src/_resources/prompts/ja-JP/webui_ai_search_correction.prompt +1 -0
  222. src/_resources/prompts/ja-JP/webui_ai_search_evidence.prompt +3 -0
  223. src/_resources/prompts/ja-JP/webui_ai_search_final.prompt +1 -0
  224. src/_resources/prompts/ko/.meta.toml +4 -0
  225. src/_resources/prompts/ko/reply_attachments.prompt +3 -0
  226. src/_resources/prompts/zh-CN/.meta.toml +159 -0
  227. src/_resources/prompts/zh-CN/a_memorix_chat_summary.prompt +59 -0
  228. src/_resources/prompts/zh-CN/emoji_content_analysis.prompt +3 -0
  229. src/_resources/prompts/zh-CN/emoji_content_filtration.prompt +6 -0
  230. src/_resources/prompts/zh-CN/emoji_replace.prompt +12 -0
  231. src/_resources/prompts/zh-CN/emoji_selection.prompt +6 -0
  232. src/_resources/prompts/zh-CN/expression_evaluation.prompt +14 -0
  233. src/_resources/prompts/zh-CN/heuristic_memory_impression.prompt +15 -0
  234. src/_resources/prompts/zh-CN/image_description.prompt +1 -0
  235. src/_resources/prompts/zh-CN/jargon_compare_inference.prompt +14 -0
  236. src/_resources/prompts/zh-CN/jargon_inference_content_only.prompt +11 -0
  237. src/_resources/prompts/zh-CN/jargon_inference_with_context.prompt +19 -0
  238. src/_resources/prompts/zh-CN/learn_jargon.prompt +31 -0
  239. src/_resources/prompts/zh-CN/learn_style.prompt +28 -0
  240. src/_resources/prompts/zh-CN/maisaka_chat.prompt +28 -0
  241. src/_resources/prompts/zh-CN/maisaka_chat_focus.prompt +39 -0
  242. src/_resources/prompts/zh-CN/maisaka_chat_optimized.prompt +19 -0
  243. src/_resources/prompts/zh-CN/maisaka_replyer.prompt +6 -0
  244. src/_resources/prompts/zh-CN/memory_fuzzy_modify_plan.prompt +45 -0
  245. src/_resources/prompts/zh-CN/mid_term_memory_summary.prompt +10 -0
  246. src/_resources/prompts/zh-CN/reply_attachments.prompt +3 -0
  247. src/_resources/prompts/zh-CN/retro_private_replyer.prompt +13 -0
  248. src/_resources/prompts/zh-CN/retro_private_replyer_self.prompt +13 -0
  249. src/_resources/prompts/zh-CN/retro_replyer.prompt +17 -0
  250. src/_resources/prompts/zh-CN/retro_replyer_light.prompt +17 -0
  251. src/_resources/prompts/zh-CN/webui_ai_search.prompt +24 -0
  252. src/_resources/prompts/zh-CN/webui_ai_search_correction.prompt +1 -0
  253. src/_resources/prompts/zh-CN/webui_ai_search_evidence.prompt +3 -0
  254. src/_resources/prompts/zh-CN/webui_ai_search_final.prompt +1 -0
  255. src/_resources/pyproject.toml +156 -0
  256. src/chat/__init__.py +13 -0
  257. src/chat/heart_flow/heartFC_utils.py +29 -0
  258. src/chat/heart_flow/heartflow_manager.py +138 -0
  259. src/chat/heart_flow/heartflow_message_processor.py +104 -0
  260. src/chat/image_system/image_cache_cleanup.py +300 -0
  261. src/chat/image_system/image_manager.py +516 -0
  262. src/chat/message_receive/__init__.py +5 -0
  263. src/chat/message_receive/bot.py +893 -0
  264. src/chat/message_receive/chat_manager.py +540 -0
  265. src/chat/message_receive/image_receive_compressor.py +135 -0
  266. src/chat/message_receive/message.py +546 -0
  267. src/chat/message_receive/uni_message_sender.py +332 -0
  268. src/chat/replyer/expression_vector_index.py +3176 -0
  269. src/chat/replyer/maisaka_expression_selector.py +678 -0
  270. src/chat/replyer/maisaka_generator.py +25 -0
  271. src/chat/replyer/maisaka_generator_base.py +1457 -0
  272. src/chat/replyer/replyer_manager.py +62 -0
  273. src/chat/replyer/retro_prompt.py +512 -0
  274. src/chat/utils/llm_sentence_splitter.py +151 -0
  275. src/chat/utils/statistic.py +3170 -0
  276. src/chat/utils/typo_generator.py +495 -0
  277. src/chat/utils/utils.py +922 -0
  278. src/cli/__init__.py +3 -0
  279. src/cli/bot_console.py +281 -0
  280. src/cli/console.py +17 -0
  281. src/cli/input_reader.py +55 -0
  282. src/cli/maisaka_cli.py +122 -0
  283. src/cli/maisaka_cli_sender.py +28 -0
  284. src/cli/tool.py +62 -0
  285. src/common/__init__.py +1 -0
  286. src/common/data_models/__init__.py +28 -0
  287. src/common/data_models/chat_session_data_model.py +73 -0
  288. src/common/data_models/chat_target_info_data_model.py +73 -0
  289. src/common/data_models/embedding_service_data_models.py +31 -0
  290. src/common/data_models/expression_data_model.py +83 -0
  291. src/common/data_models/image_data_model.py +247 -0
  292. src/common/data_models/jargon_data_model.py +101 -0
  293. src/common/data_models/llm_data_model.py +23 -0
  294. src/common/data_models/llm_service_data_models.py +298 -0
  295. src/common/data_models/mai_message_data_model.py +226 -0
  296. src/common/data_models/message_component_data_model.py +560 -0
  297. src/common/data_models/person_info_data_model.py +177 -0
  298. src/common/data_models/reply_generation_data_models.py +275 -0
  299. src/common/data_models/tool_record_data_model.py +51 -0
  300. src/common/database/__init__.py +0 -0
  301. src/common/database/database.py +240 -0
  302. src/common/database/database_model.py +477 -0
  303. src/common/database/migrations/__init__.py +161 -0
  304. src/common/database/migrations/behavior_generic_tags.py +532 -0
  305. src/common/database/migrations/bootstrap.py +171 -0
  306. src/common/database/migrations/builtin.py +1966 -0
  307. src/common/database/migrations/exceptions.py +33 -0
  308. src/common/database/migrations/frozen_v2_schema.py +282 -0
  309. src/common/database/migrations/legacy_v1_to_v2.py +1300 -0
  310. src/common/database/migrations/manager.py +209 -0
  311. src/common/database/migrations/models.py +305 -0
  312. src/common/database/migrations/planner.py +108 -0
  313. src/common/database/migrations/progress.py +319 -0
  314. src/common/database/migrations/registry.py +98 -0
  315. src/common/database/migrations/resolver.py +135 -0
  316. src/common/database/migrations/schema.py +98 -0
  317. src/common/database/migrations/v10_to_v11.py +60 -0
  318. src/common/database/migrations/v11_to_v12.py +92 -0
  319. src/common/database/migrations/v12_to_v13.py +42 -0
  320. src/common/database/migrations/v13_to_v14.py +55 -0
  321. src/common/database/migrations/v14_to_v15.py +44 -0
  322. src/common/database/migrations/v15_to_v16.py +39 -0
  323. src/common/database/migrations/v16_to_v17.py +66 -0
  324. src/common/database/migrations/v17_to_v18.py +64 -0
  325. src/common/database/migrations/v18_to_v19.py +355 -0
  326. src/common/database/migrations/v19_to_v20.py +169 -0
  327. src/common/database/migrations/v20_to_v21.py +149 -0
  328. src/common/database/migrations/v21_to_v22.py +343 -0
  329. src/common/database/migrations/v22_to_v23.py +114 -0
  330. src/common/database/migrations/v23_to_v24.py +465 -0
  331. src/common/database/migrations/v24_to_v25.py +96 -0
  332. src/common/database/migrations/v25_to_v26.py +32 -0
  333. src/common/database/migrations/v26_to_v27.py +86 -0
  334. src/common/database/migrations/v27_to_v28.py +185 -0
  335. src/common/database/migrations/v28_to_v29.py +80 -0
  336. src/common/database/migrations/v29_to_v30.py +217 -0
  337. src/common/database/migrations/v2_to_v3.py +269 -0
  338. src/common/database/migrations/v30_to_v31.py +323 -0
  339. src/common/database/migrations/v31_to_v32.py +250 -0
  340. src/common/database/migrations/v32_to_v33.py +84 -0
  341. src/common/database/migrations/v33_to_v34.py +119 -0
  342. src/common/database/migrations/v34_to_v35.py +68 -0
  343. src/common/database/migrations/v35_to_v36.py +35 -0
  344. src/common/database/migrations/v36_to_v37.py +54 -0
  345. src/common/database/migrations/v37_to_v38.py +95 -0
  346. src/common/database/migrations/v38_to_v39.py +85 -0
  347. src/common/database/migrations/v39_to_v40.py +53 -0
  348. src/common/database/migrations/v3_to_v4.py +155 -0
  349. src/common/database/migrations/v40_to_v41.py +23 -0
  350. src/common/database/migrations/v41_to_v42.py +44 -0
  351. src/common/database/migrations/v42_to_v43.py +29 -0
  352. src/common/database/migrations/v4_to_v5.py +77 -0
  353. src/common/database/migrations/v5_to_v6.py +51 -0
  354. src/common/database/migrations/v6_to_v7.py +164 -0
  355. src/common/database/migrations/v7_to_v8.py +73 -0
  356. src/common/database/migrations/v8_to_v9.py +313 -0
  357. src/common/database/migrations/v9_to_v10.py +53 -0
  358. src/common/database/migrations/version_store.py +57 -0
  359. src/common/embedding_identity.py +43 -0
  360. src/common/event_loop_watchdog.py +72 -0
  361. src/common/i18n/__init__.py +70 -0
  362. src/common/i18n/exceptions.py +18 -0
  363. src/common/i18n/formatting.py +29 -0
  364. src/common/i18n/loaders.py +162 -0
  365. src/common/i18n/manager.py +201 -0
  366. src/common/logger.py +1009 -0
  367. src/common/logger_color_and_mapping.py +430 -0
  368. src/common/message_repository.py +287 -0
  369. src/common/message_server/__init__.py +19 -0
  370. src/common/message_server/api.py +190 -0
  371. src/common/message_server/server.py +130 -0
  372. src/common/operation_timing.py +52 -0
  373. src/common/paths.py +32 -0
  374. src/common/process_runner.py +142 -0
  375. src/common/prompt_i18n.py +416 -0
  376. src/common/remote.py +288 -0
  377. src/common/reply_effect_fingerprint.py +119 -0
  378. src/common/reply_effect_record_codec.py +40 -0
  379. src/common/runtime_loop.py +31 -0
  380. src/common/self_update.py +580 -0
  381. src/common/shutdown.py +55 -0
  382. src/common/update_notice.py +351 -0
  383. src/common/utils/image_path.py +39 -0
  384. src/common/utils/math_utils.py +122 -0
  385. src/common/utils/port_checker.py +97 -0
  386. src/common/utils/system_utils.py +11 -0
  387. src/common/utils/utils_action.py +32 -0
  388. src/common/utils/utils_config.py +613 -0
  389. src/common/utils/utils_file.py +56 -0
  390. src/common/utils/utils_image.py +257 -0
  391. src/common/utils/utils_message.py +871 -0
  392. src/common/utils/utils_person.py +55 -0
  393. src/common/utils/utils_session.py +42 -0
  394. src/common/utils/utils_voice.py +42 -0
  395. src/common/version.py +33 -0
  396. src/config/__init__.py +0 -0
  397. src/config/config.py +927 -0
  398. src/config/config_base.py +308 -0
  399. src/config/config_upgrade_hooks.py +675 -0
  400. src/config/config_utils.py +166 -0
  401. src/config/default_model_config.py +131 -0
  402. src/config/file_watcher.py +238 -0
  403. src/config/legacy_migration.py +424 -0
  404. src/config/legacy_upgrade_confirmation.py +225 -0
  405. src/config/model_configs.py +548 -0
  406. src/config/official_configs.py +6166 -0
  407. src/config/startup_bindings.py +201 -0
  408. src/core/__init__.py +6 -0
  409. src/core/announcement_manager.py +120 -0
  410. src/core/config_types.py +273 -0
  411. src/core/event_bus.py +225 -0
  412. src/core/local_operator.py +98 -0
  413. src/core/tooling.py +420 -0
  414. src/core/types.py +423 -0
  415. src/emoji_system/emoji_cache_cleanup.py +326 -0
  416. src/emoji_system/emoji_manager.py +1314 -0
  417. src/emoji_system/maisaka_tool.py +286 -0
  418. src/learners/expression_learner.py +927 -0
  419. src/learners/expression_review_store.py +216 -0
  420. src/learners/expression_style_utils.py +32 -0
  421. src/learners/expression_utils.py +299 -0
  422. src/learners/jargon_learner.py +873 -0
  423. src/learners/jargon_miner.py +885 -0
  424. src/learners/weighted_sampling.py +75 -0
  425. src/llm_models/LICENSE +21 -0
  426. src/llm_models/__init__.py +0 -0
  427. src/llm_models/exceptions.py +140 -0
  428. src/llm_models/generation_diagnostics.py +155 -0
  429. src/llm_models/image_normalizer.py +128 -0
  430. src/llm_models/model_client/__init__.py +29 -0
  431. src/llm_models/model_client/adapter_base.py +362 -0
  432. src/llm_models/model_client/base_client.py +678 -0
  433. src/llm_models/model_client/embedding_rate_limit.py +55 -0
  434. src/llm_models/model_client/gemini_client.py +1379 -0
  435. src/llm_models/model_client/image_embedding_protocols.py +397 -0
  436. src/llm_models/model_client/openai_client.py +2200 -0
  437. src/llm_models/model_client/openai_responses_client.py +817 -0
  438. src/llm_models/model_client/plugin_client.py +307 -0
  439. src/llm_models/openai_compat.py +165 -0
  440. src/llm_models/payload_content/__init__.py +51 -0
  441. src/llm_models/payload_content/context_item.py +699 -0
  442. src/llm_models/payload_content/context_protocol.py +243 -0
  443. src/llm_models/payload_content/native_tool.py +18 -0
  444. src/llm_models/payload_content/resp_format.py +239 -0
  445. src/llm_models/payload_content/tool_option.py +547 -0
  446. src/llm_models/request_snapshot.py +1311 -0
  447. src/llm_models/utils.py +276 -0
  448. src/llm_models/utils_model.py +1604 -0
  449. src/main.py +300 -0
  450. src/maisaka/attention_drift.py +116 -0
  451. src/maisaka/builtin_tool/__init__.py +236 -0
  452. src/maisaka/builtin_tool/context.py +530 -0
  453. src/maisaka/builtin_tool/fetch_history.py +62 -0
  454. src/maisaka/builtin_tool/provider.py +74 -0
  455. src/maisaka/builtin_tool/query_image_memory.py +103 -0
  456. src/maisaka/builtin_tool/query_memory.py +335 -0
  457. src/maisaka/builtin_tool/query_person_profile.py +152 -0
  458. src/maisaka/builtin_tool/read_tool_result.py +142 -0
  459. src/maisaka/builtin_tool/reply.py +695 -0
  460. src/maisaka/builtin_tool/send_emoji.py +543 -0
  461. src/maisaka/builtin_tool/show_emoji_list.py +76 -0
  462. src/maisaka/builtin_tool/subagent.py +549 -0
  463. src/maisaka/builtin_tool/switch_chat.py +78 -0
  464. src/maisaka/builtin_tool/tool_search.py +92 -0
  465. src/maisaka/builtin_tool/view_forward_message.py +153 -0
  466. src/maisaka/builtin_tool/wait.py +77 -0
  467. src/maisaka/chat_loop_service.py +1579 -0
  468. src/maisaka/context/__init__.py +1 -0
  469. src/maisaka/context/clear_context.py +42 -0
  470. src/maisaka/context/emoji_candidates.py +36 -0
  471. src/maisaka/context/history.py +384 -0
  472. src/maisaka/context/image_attachment.py +52 -0
  473. src/maisaka/context/message_adapter.py +126 -0
  474. src/maisaka/context/message_id_alias.py +52 -0
  475. src/maisaka/context/messages.py +825 -0
  476. src/maisaka/context/planner_messages.py +199 -0
  477. src/maisaka/context/post_processor.py +204 -0
  478. src/maisaka/context/usage.py +141 -0
  479. src/maisaka/display/__init__.py +25 -0
  480. src/maisaka/display/display_utils.py +144 -0
  481. src/maisaka/display/preview_path_utils.py +64 -0
  482. src/maisaka/display/prompt_cli_renderer.py +1110 -0
  483. src/maisaka/display/prompt_preview_logger.py +603 -0
  484. src/maisaka/display/runtime_mixin.py +709 -0
  485. src/maisaka/display/stage_status_board.py +129 -0
  486. src/maisaka/focus/__init__.py +10 -0
  487. src/maisaka/focus/manager.py +442 -0
  488. src/maisaka/focus/runtime_mixin.py +753 -0
  489. src/maisaka/jargon_context_matcher.py +363 -0
  490. src/maisaka/memory/__init__.py +1 -0
  491. src/maisaka/memory/heuristic_injector.py +393 -0
  492. src/maisaka/memory/image_injector.py +81 -0
  493. src/maisaka/memory/mid_term.py +995 -0
  494. src/maisaka/memory/person_profile.py +280 -0
  495. src/maisaka/mode_policy.py +28 -0
  496. src/maisaka/monitor/__init__.py +1 -0
  497. src/maisaka/monitor/event_store.py +351 -0
  498. src/maisaka/monitor/events.py +795 -0
  499. src/maisaka/monitor/message_payload.py +172 -0
  500. src/maisaka/monitor/planner_delta.py +77 -0
  501. src/maisaka/monitor/tool_media.py +58 -0
  502. src/maisaka/planner_reply_guard.py +69 -0
  503. src/maisaka/reasoning_engine.py +2186 -0
  504. src/maisaka/reply_effect/__init__.py +18 -0
  505. src/maisaka/reply_effect/image_utils.py +100 -0
  506. src/maisaka/reply_effect/judge.py +382 -0
  507. src/maisaka/reply_effect/models.py +185 -0
  508. src/maisaka/reply_effect/path_utils.py +24 -0
  509. src/maisaka/reply_effect/quote_utils.py +32 -0
  510. src/maisaka/reply_effect/scoring.py +205 -0
  511. src/maisaka/reply_effect/storage.py +380 -0
  512. src/maisaka/reply_effect/tracker.py +568 -0
  513. src/maisaka/runtime.py +2218 -0
  514. src/maisaka/subagent_tasks.py +171 -0
  515. src/maisaka/tool_result_condenser.py +143 -0
  516. src/maisaka/tool_result_store.py +57 -0
  517. src/maisaka/turn_trigger/__init__.py +9 -0
  518. src/maisaka/turn_trigger/dynamic_gate.py +211 -0
  519. src/maisaka/turn_trigger/gates.py +252 -0
  520. src/maisaka/turn_trigger/idle_backoff.py +93 -0
  521. src/maisaka/turn_trigger/reply_likelihood.py +88 -0
  522. src/maisaka/turn_trigger/scheduler.py +113 -0
  523. src/maisaka/utils/__init__.py +1 -0
  524. src/maisaka/utils/tool_post_execution.py +76 -0
  525. src/maisaka/utils/tool_record_payload.py +155 -0
  526. src/maisaka/visual/__init__.py +1 -0
  527. src/maisaka/visual/chat_history_refresher.py +303 -0
  528. src/maisaka/visual/message_limiter.py +69 -0
  529. src/maisaka/visual/mode_utils.py +33 -0
  530. src/manager/async_task_manager.py +193 -0
  531. src/manager/local_store_manager.py +126 -0
  532. src/mcp_module/__init__.py +9 -0
  533. src/mcp_module/config.py +174 -0
  534. src/mcp_module/connection.py +726 -0
  535. src/mcp_module/hooks.py +20 -0
  536. src/mcp_module/host_llm_bridge.py +654 -0
  537. src/mcp_module/manager.py +680 -0
  538. src/mcp_module/models.py +437 -0
  539. src/mcp_module/provider.py +62 -0
  540. src/mcp_module/service.py +308 -0
  541. src/mcp_module/stdio_filter.py +177 -0
  542. src/person_info/__init__.py +0 -0
  543. src/person_info/person_info.py +600 -0
  544. src/platform_io/__init__.py +39 -0
  545. src/platform_io/adapter_policy.py +839 -0
  546. src/platform_io/avatar.py +197 -0
  547. src/platform_io/dedupe.py +133 -0
  548. src/platform_io/drivers/__init__.py +11 -0
  549. src/platform_io/drivers/base.py +104 -0
  550. src/platform_io/drivers/legacy_driver.py +92 -0
  551. src/platform_io/drivers/plugin_driver.py +213 -0
  552. src/platform_io/manager.py +716 -0
  553. src/platform_io/outbound_tracker.py +286 -0
  554. src/platform_io/registry.py +70 -0
  555. src/platform_io/route_key_factory.py +174 -0
  556. src/platform_io/routing.py +141 -0
  557. src/platform_io/types.py +265 -0
  558. src/plugin_runtime/__init__.py +60 -0
  559. src/plugin_runtime/avatar_provider.py +64 -0
  560. src/plugin_runtime/capabilities/__init__.py +11 -0
  561. src/plugin_runtime/capabilities/components.py +977 -0
  562. src/plugin_runtime/capabilities/core.py +868 -0
  563. src/plugin_runtime/capabilities/data.py +1415 -0
  564. src/plugin_runtime/capabilities/registry.py +116 -0
  565. src/plugin_runtime/capabilities/render.py +131 -0
  566. src/plugin_runtime/compat_policy.py +47 -0
  567. src/plugin_runtime/component_query.py +1119 -0
  568. src/plugin_runtime/dependency_pipeline.py +547 -0
  569. src/plugin_runtime/docker_layout_migration.py +275 -0
  570. src/plugin_runtime/hook_catalog.py +52 -0
  571. src/plugin_runtime/hook_payloads.py +197 -0
  572. src/plugin_runtime/hook_schema_utils.py +31 -0
  573. src/plugin_runtime/host/__init__.py +1 -0
  574. src/plugin_runtime/host/api_registry.py +353 -0
  575. src/plugin_runtime/host/authorization.py +67 -0
  576. src/plugin_runtime/host/capability_service.py +91 -0
  577. src/plugin_runtime/host/circuit_breaker.py +243 -0
  578. src/plugin_runtime/host/component_registry.py +1336 -0
  579. src/plugin_runtime/host/component_timeout.py +32 -0
  580. src/plugin_runtime/host/event_dispatcher.py +228 -0
  581. src/plugin_runtime/host/hook_dispatcher.py +711 -0
  582. src/plugin_runtime/host/hook_spec_registry.py +190 -0
  583. src/plugin_runtime/host/logger_bridge.py +64 -0
  584. src/plugin_runtime/host/message_gateway.py +116 -0
  585. src/plugin_runtime/host/message_utils.py +572 -0
  586. src/plugin_runtime/host/reply_extension_schema.py +65 -0
  587. src/plugin_runtime/host/reply_extensions.py +204 -0
  588. src/plugin_runtime/host/rpc_server.py +509 -0
  589. src/plugin_runtime/host/supervisor.py +2269 -0
  590. src/plugin_runtime/integration.py +2063 -0
  591. src/plugin_runtime/local_sdk.py +147 -0
  592. src/plugin_runtime/protocol/__init__.py +1 -0
  593. src/plugin_runtime/protocol/codec.py +71 -0
  594. src/plugin_runtime/protocol/envelope.py +600 -0
  595. src/plugin_runtime/protocol/errors.py +77 -0
  596. src/plugin_runtime/runner/__init__.py +1 -0
  597. src/plugin_runtime/runner/log_handler.py +239 -0
  598. src/plugin_runtime/runner/manifest_validator.py +1517 -0
  599. src/plugin_runtime/runner/plugin_loader.py +711 -0
  600. src/plugin_runtime/runner/plugin_paths.py +59 -0
  601. src/plugin_runtime/runner/rpc_client.py +336 -0
  602. src/plugin_runtime/runner/runner_main.py +2670 -0
  603. src/plugin_runtime/tool_provider.py +58 -0
  604. src/plugin_runtime/transport/__init__.py +1 -0
  605. src/plugin_runtime/transport/base.py +116 -0
  606. src/plugin_runtime/transport/factory.py +57 -0
  607. src/plugin_runtime/transport/named_pipe.py +206 -0
  608. src/plugin_runtime/transport/tcp.py +62 -0
  609. src/plugin_runtime/transport/uds.py +133 -0
  610. src/plugin_runtime/update_compatibility_notice.py +303 -0
  611. src/plugin_runtime/upload_store.py +173 -0
  612. src/plugin_runtime/webui_schema.py +571 -0
  613. src/plugins/__init__.py +1 -0
  614. src/plugins/built_in/plugin_management/_manifest.json +48 -0
  615. src/plugins/built_in/plugin_management/plugin.py +665 -0
  616. src/prompt/prompt_manager.py +440 -0
  617. src/runtime.py +688 -0
  618. src/services/__init__.py +7 -0
  619. src/services/bot_account_service.py +327 -0
  620. src/services/database_service.py +203 -0
  621. src/services/embedding_service.py +271 -0
  622. src/services/expression_vector_service.py +106 -0
  623. src/services/html_render_service.py +945 -0
  624. src/services/image_path_maintenance_service.py +454 -0
  625. src/services/image_writeback_journal.py +98 -0
  626. src/services/llm_cache_stats.py +1521 -0
  627. src/services/llm_service.py +799 -0
  628. src/services/memory_flow_service.py +1019 -0
  629. src/services/memory_service.py +528 -0
  630. src/services/message_service.py +295 -0
  631. src/services/message_word_frequency_service.py +595 -0
  632. src/services/news_service.py +123 -0
  633. src/services/person_fact_reverification.py +90 -0
  634. src/services/person_fact_verifier.py +79 -0
  635. src/services/send_service.py +1483 -0
  636. src/services/service_task_resolver.py +59 -0
  637. src/services/statistics_aggregation_service.py +349 -0
  638. src/services/statistics_service.py +850 -0
  639. src/services/telemetry_stats_service.py +294 -0
  640. src/services/tool_record_cleanup_service.py +205 -0
  641. src/webui/app.py +374 -0
  642. src/webui/config_schema.py +481 -0
  643. src/webui/core/__init__.py +32 -0
  644. src/webui/core/auth.py +153 -0
  645. src/webui/core/rate_limiter.py +247 -0
  646. src/webui/core/security.py +361 -0
  647. src/webui/dependencies.py +70 -0
  648. src/webui/logs_ws.py +195 -0
  649. src/webui/middleware/__init__.py +17 -0
  650. src/webui/middleware/anti_crawler.py +824 -0
  651. src/webui/middleware/compression.py +88 -0
  652. src/webui/routers/__init__.py +33 -0
  653. src/webui/routers/avatar.py +250 -0
  654. src/webui/routers/bot_accounts.py +103 -0
  655. src/webui/routers/chat/__init__.py +18 -0
  656. src/webui/routers/chat/routes.py +1715 -0
  657. src/webui/routers/chat/serializers.py +248 -0
  658. src/webui/routers/chat/service.py +1489 -0
  659. src/webui/routers/config.py +1964 -0
  660. src/webui/routers/data_transfer.py +769 -0
  661. src/webui/routers/emoji/__init__.py +3 -0
  662. src/webui/routers/emoji/routes.py +1230 -0
  663. src/webui/routers/emoji/schemas.py +182 -0
  664. src/webui/routers/emoji/support.py +143 -0
  665. src/webui/routers/expression.py +2298 -0
  666. src/webui/routers/jargon.py +1319 -0
  667. src/webui/routers/mcp.py +99 -0
  668. src/webui/routers/memory.py +5589 -0
  669. src/webui/routers/model.py +910 -0
  670. src/webui/routers/news.py +23 -0
  671. src/webui/routers/person.py +427 -0
  672. src/webui/routers/plugin/__init__.py +31 -0
  673. src/webui/routers/plugin/catalog.py +218 -0
  674. src/webui/routers/plugin/config_routes.py +592 -0
  675. src/webui/routers/plugin/icon_routes.py +71 -0
  676. src/webui/routers/plugin/management.py +908 -0
  677. src/webui/routers/plugin/marketplace.py +116 -0
  678. src/webui/routers/plugin/progress.py +172 -0
  679. src/webui/routers/plugin/release_install.py +313 -0
  680. src/webui/routers/plugin/releases.py +181 -0
  681. src/webui/routers/plugin/runtime_routes.py +243 -0
  682. src/webui/routers/plugin/schemas.py +131 -0
  683. src/webui/routers/plugin/stats_proxy.py +127 -0
  684. src/webui/routers/plugin/support.py +366 -0
  685. src/webui/routers/plugin/webui_extensions.py +346 -0
  686. src/webui/routers/plugin/zip_install.py +125 -0
  687. src/webui/routers/reasoning_process.py +2326 -0
  688. src/webui/routers/reply_effects.py +926 -0
  689. src/webui/routers/search.py +249 -0
  690. src/webui/routers/self_update.py +194 -0
  691. src/webui/routers/statistics.py +61 -0
  692. src/webui/routers/system.py +1790 -0
  693. src/webui/routers/user_emoji.py +168 -0
  694. src/webui/routers/websocket/__init__.py +7 -0
  695. src/webui/routers/websocket/auth.py +104 -0
  696. src/webui/routers/websocket/manager.py +373 -0
  697. src/webui/routers/websocket/plugin_runtime.py +54 -0
  698. src/webui/routers/websocket/unified.py +761 -0
  699. src/webui/routes.py +416 -0
  700. src/webui/schemas/__init__.py +18 -0
  701. src/webui/schemas/news.py +24 -0
  702. src/webui/schemas/statistics.py +173 -0
  703. src/webui/services/__init__.py +1 -0
  704. src/webui/services/ai_search_agent.py +509 -0
  705. src/webui/services/ai_search_documents.py +329 -0
  706. src/webui/services/ai_search_grounding.py +103 -0
  707. src/webui/services/ai_search_local_config.py +129 -0
  708. src/webui/services/ai_search_models.py +98 -0
  709. src/webui/services/git_mirror_service.py +1024 -0
  710. src/webui/services/plugin_market_service.py +56 -0
  711. src/webui/utils/__init__.py +1 -0
  712. src/webui/utils/http_client.py +27 -0
  713. src/webui/utils/network_security.py +133 -0
  714. src/webui/utils/toml_utils.py +124 -0
  715. src/webui/version_compatibility.py +180 -0
  716. src/webui/webui_server.py +422 -0
@@ -0,0 +1,580 @@
1
+ """主程序自更新。
2
+
3
+ Worker(WebUI)只负责检查更新与登记更新请求;真正改动代码与依赖的步骤由 Runner 在
4
+ 两次 Worker 之间执行。此时没有业务进程占用文件,失败时也能在新代码运行前回滚。
5
+
6
+ 本模块会被 Runner 加载,只能依赖标准库。
7
+ """
8
+
9
+ from dataclasses import asdict, dataclass, field
10
+ from pathlib import Path
11
+ from typing import Any, Dict, List, Optional, Set, Tuple
12
+ from urllib.parse import urlsplit
13
+
14
+ import importlib.util
15
+ import json
16
+ import os
17
+ import re
18
+ import shutil
19
+ import subprocess
20
+ import sys
21
+ import time
22
+ import tomllib
23
+
24
+ from .paths import PACKAGED_INSTALL, SOURCE_ROOT, get_instance_root
25
+ from .version import read_project_version
26
+
27
+ PROJECT_ROOT = get_instance_root() if PACKAGED_INSTALL else SOURCE_ROOT
28
+
29
+ # 更新只会拉取这个仓库:直连官方地址,或经由插件市场镜像源配置里启用的镜像。
30
+ # 不读取本地 remote 配置,也不接受任何外部传入的地址。
31
+ OFFICIAL_REPOSITORY_PATH = "Mai-with-u/MaiBot"
32
+ OFFICIAL_REPOSITORY_URL = f"https://github.com/{OFFICIAL_REPOSITORY_PATH}.git"
33
+ OFFICIAL_SOURCE_ID = "github"
34
+ SUPPORTED_BRANCHES: Tuple[str, ...] = ("main", "dev")
35
+ DEPENDENCY_FILES = frozenset({"pyproject.toml", "uv.lock", "requirements.txt"})
36
+
37
+ PENDING_MAX_AGE_SECONDS = 600.0
38
+ GIT_TIMEOUT_SECONDS = 120.0
39
+ GIT_FETCH_TIMEOUT_SECONDS = 300.0
40
+ DEPENDENCY_TIMEOUT_SECONDS = 1800.0
41
+ MAX_LISTED_COMMITS = 30
42
+ MAX_LISTED_DIRTY_FILES = 20
43
+
44
+ _COMMIT_PATTERN = re.compile(r"^[0-9a-f]{40}$")
45
+ _STATE_DIR_PARTS = ("data", "self_update")
46
+ # 与插件市场共用的镜像源配置,由 GitMirrorConfig 维护。
47
+ _MIRROR_CONFIG_PARTS = ("data", "webui.json")
48
+ # 连接上但几乎不传数据的镜像尽快放弃,换下一个来源。
49
+ _GIT_STALL_OPTIONS = ("-c", "http.lowSpeedLimit=1000", "-c", "http.lowSpeedTime=30")
50
+ _COMMIT_FIELD_SEPARATOR = "\x1f"
51
+
52
+ BLOCKER_MESSAGES: Dict[str, str] = {
53
+ "package_install": "安装版请停止 MaiBot 后执行 uv tool upgrade maibot",
54
+ "git_missing": "未找到 Git,无法自更新",
55
+ "not_git_repo": "当前部署不是 Git 仓库(如 Docker 镜像或压缩包),请按对应部署方式更新",
56
+ "detached_head": "当前不在任何分支上(分离头指针),无法自更新",
57
+ "unsupported_branch": f"仅支持在 {' / '.join(SUPPORTED_BRANCHES)} 分支上自更新",
58
+ "dirty_worktree": "存在未提交的本地改动,请先提交或还原后再更新",
59
+ "diverged": "本地存在官方分支没有的提交,无法快进更新",
60
+ "no_dependency_tool": "本次更新包含依赖变更,但未找到可用的依赖安装方式(uv 或 pip)",
61
+ "python_requirement_changed": "本次更新修改了 Python 版本要求,请手动更新",
62
+ }
63
+
64
+
65
+ class SelfUpdateError(Exception):
66
+ """自更新流程中的可预期失败。"""
67
+
68
+ def __init__(self, code: str, message: str) -> None:
69
+ super().__init__(message)
70
+ self.code = code
71
+ self.message = message
72
+
73
+
74
+ @dataclass
75
+ class LocalState:
76
+ """本地仓库状态,不联网即可得到。"""
77
+
78
+ version: str
79
+ blocker: Optional[str] = None # 导致完全无法检查更新的原因
80
+ branch: Optional[str] = None
81
+ commit: Optional[str] = None
82
+ dirty_files: List[str] = field(default_factory=list)
83
+ dependency_tool: Optional[str] = None
84
+
85
+
86
+ @dataclass
87
+ class UpdateSource:
88
+ """一个可以取回官方仓库的来源:官方地址或某个镜像。"""
89
+
90
+ id: str
91
+ name: str
92
+ url: str
93
+
94
+
95
+ @dataclass
96
+ class UpdateCommit:
97
+ """待更新的单个提交。"""
98
+
99
+ short_id: str
100
+ summary: str
101
+ committed_at: str
102
+
103
+
104
+ @dataclass
105
+ class UpdateCheck:
106
+ """一次更新检查的结论。"""
107
+
108
+ local: LocalState
109
+ checked_at: Optional[float] = None # None 表示尚未联网检查
110
+ latest_commit: Optional[str] = None
111
+ latest_version: Optional[str] = None
112
+ behind: int = 0
113
+ ahead: int = 0
114
+ dependencies_changed: bool = False
115
+ commits: List[UpdateCommit] = field(default_factory=list)
116
+ blockers: List[str] = field(default_factory=list)
117
+ source: Optional[UpdateSource] = None # 本次检查实际使用的来源
118
+
119
+ @property
120
+ def update_available(self) -> bool:
121
+ return self.behind > 0
122
+
123
+ @property
124
+ def can_apply(self) -> bool:
125
+ return self.update_available and not self.blockers
126
+
127
+
128
+ @dataclass
129
+ class PendingUpdate:
130
+ """Worker 登记、等待 Runner 执行的更新请求。"""
131
+
132
+ target_commit: str
133
+ base_commit: str
134
+ branch: str
135
+ requested_at: float
136
+
137
+
138
+ @dataclass
139
+ class UpdateResult:
140
+ """Runner 执行一次更新后的结果,供 WebUI 展示。"""
141
+
142
+ success: bool
143
+ started_at: float
144
+ finished_at: float = 0.0
145
+ branch: str = ""
146
+ from_commit: str = ""
147
+ to_commit: str = ""
148
+ from_version: str = ""
149
+ to_version: str = ""
150
+ stage: str = "" # 结束时所处阶段:request / precheck / verify / merge / dependencies / done
151
+ error_code: str = ""
152
+ error: str = ""
153
+ rolled_back: bool = False
154
+ rollback_error: str = ""
155
+ dependencies_synced: bool = False
156
+ source_name: str = ""
157
+
158
+
159
+ def _state_dir(root: Path) -> Path:
160
+ instance_root = get_instance_root() if root == PROJECT_ROOT else root
161
+ return instance_root.joinpath(*_STATE_DIR_PARTS)
162
+
163
+
164
+ def _pending_path(root: Path) -> Path:
165
+ return _state_dir(root) / "pending.json"
166
+
167
+
168
+ def _result_path(root: Path) -> Path:
169
+ return _state_dir(root) / "last_result.json"
170
+
171
+
172
+ def _write_json(path: Path, payload: Dict[str, Any]) -> None:
173
+ """先写临时文件再替换,避免读到写了一半的状态文件。"""
174
+
175
+ path.parent.mkdir(parents=True, exist_ok=True)
176
+ temp_path = path.with_name(f"{path.name}.tmp")
177
+ temp_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
178
+ os.replace(temp_path, path)
179
+
180
+
181
+ def _run_command(command: List[str], root: Path, timeout: float) -> "subprocess.CompletedProcess[str]":
182
+ env = os.environ.copy()
183
+ # 凭据提示会让无人值守的更新一直挂起;官方仓库是公开的,不需要凭据。
184
+ env["GIT_TERMINAL_PROMPT"] = "0"
185
+ return subprocess.run(
186
+ command,
187
+ cwd=root,
188
+ env=env,
189
+ stdin=subprocess.DEVNULL,
190
+ capture_output=True,
191
+ text=True,
192
+ encoding="utf-8",
193
+ errors="replace",
194
+ timeout=timeout,
195
+ )
196
+
197
+
198
+ def _git(root: Path, *args: str, timeout: float = GIT_TIMEOUT_SECONDS) -> str:
199
+ """执行必须成功的 Git 命令,返回未裁剪的标准输出。"""
200
+
201
+ command_text = f"git {' '.join(args)}"
202
+ try:
203
+ result = _run_command(["git", *args], root, timeout)
204
+ except subprocess.TimeoutExpired as exc:
205
+ raise SelfUpdateError("git_timeout", f"Git 命令超时: {command_text}") from exc
206
+ if result.returncode != 0:
207
+ output = result.stderr.strip() or result.stdout.strip()
208
+ raise SelfUpdateError("git_failed", f"Git 命令失败: {command_text}\n{output}")
209
+ return result.stdout
210
+
211
+
212
+ def _is_ancestor(root: Path, ancestor: str, descendant: str) -> bool:
213
+ result = _run_command(["git", "merge-base", "--is-ancestor", ancestor, descendant], root, GIT_TIMEOUT_SECONDS)
214
+ if result.returncode in (0, 1):
215
+ return result.returncode == 0
216
+ raise SelfUpdateError("git_failed", f"无法判断提交关系: {result.stderr.strip()}")
217
+
218
+
219
+ def _parse_pyproject(text: str) -> Tuple[str, str]:
220
+ """返回 (主程序版本号, Python 版本要求)。"""
221
+
222
+ project: Dict[str, Any] = tomllib.loads(text)["project"]
223
+ return str(project["version"]).strip(), str(project.get("requires-python", "")).strip()
224
+
225
+
226
+ def _read_pyproject_at(root: Path, commit: str) -> Tuple[str, str]:
227
+ return _parse_pyproject(_git(root, "show", f"{commit}:pyproject.toml"))
228
+
229
+
230
+ def _changed_files(root: Path, base: str, target: str) -> Set[str]:
231
+ output = _git(root, "diff", "--name-only", base, target)
232
+ return {line.strip() for line in output.splitlines() if line.strip()}
233
+
234
+
235
+ def _list_commits(root: Path, base: str, target: str) -> List[UpdateCommit]:
236
+ log_format = _COMMIT_FIELD_SEPARATOR.join(("%h", "%s", "%cI"))
237
+ output = _git(root, "log", f"--format={log_format}", "-n", str(MAX_LISTED_COMMITS), f"{base}..{target}")
238
+ commits: List[UpdateCommit] = []
239
+ for line in output.splitlines():
240
+ short_id, summary, committed_at = line.split(_COMMIT_FIELD_SEPARATOR)
241
+ commits.append(UpdateCommit(short_id=short_id, summary=summary, committed_at=committed_at))
242
+ return commits
243
+
244
+
245
+ def detect_dependency_tool(root: Path = PROJECT_ROOT) -> Optional[str]:
246
+ """判断当前运行环境的依赖应当用哪种方式同步。"""
247
+
248
+ # uv 管理的项目环境按 uv.lock 同步,得到与发布时一致的依赖版本。
249
+ in_project_venv = Path(sys.prefix).resolve() == (root / ".venv").resolve()
250
+ if in_project_venv and (root / "uv.lock").is_file() and shutil.which("uv") is not None:
251
+ return "uv"
252
+ if importlib.util.find_spec("pip") is not None and (root / "requirements.txt").is_file():
253
+ return "pip"
254
+ return None
255
+
256
+
257
+ def _dependency_command(tool: str) -> List[str]:
258
+ if tool == "uv":
259
+ # --inexact:不卸载锁文件之外的包(插件依赖装在同一个环境里)。
260
+ return ["uv", "sync", "--frozen", "--inexact", "--no-dev", "--no-install-project"]
261
+ return [sys.executable, "-m", "pip", "install", "--disable-pip-version-check", "-r", "requirements.txt"]
262
+
263
+
264
+ def _sync_dependencies(root: Path, tool: str) -> None:
265
+ command = _dependency_command(tool)
266
+ try:
267
+ result = _run_command(command, root, DEPENDENCY_TIMEOUT_SECONDS)
268
+ except subprocess.TimeoutExpired as exc:
269
+ raise SelfUpdateError("dependency_timeout", f"依赖同步超时: {' '.join(command)}") from exc
270
+ if result.returncode != 0:
271
+ output = (result.stderr.strip() or result.stdout.strip())[-4000:]
272
+ raise SelfUpdateError("dependency_failed", f"依赖同步失败: {' '.join(command)}\n{output}")
273
+
274
+
275
+ def inspect_local(root: Path = PROJECT_ROOT) -> LocalState:
276
+ """读取本地仓库状态;只执行只读的 Git 命令。"""
277
+
278
+ if PACKAGED_INSTALL:
279
+ return LocalState(version=read_project_version(), blocker="package_install")
280
+ version, _ = _parse_pyproject((root / "pyproject.toml").read_text(encoding="utf-8"))
281
+ state = LocalState(version=version, dependency_tool=detect_dependency_tool(root))
282
+ if shutil.which("git") is None:
283
+ state.blocker = "git_missing"
284
+ return state
285
+ if not (root / ".git").exists():
286
+ state.blocker = "not_git_repo"
287
+ return state
288
+
289
+ state.commit = _git(root, "rev-parse", "HEAD").strip()
290
+ branch_result = _run_command(["git", "symbolic-ref", "--short", "-q", "HEAD"], root, GIT_TIMEOUT_SECONDS)
291
+ if branch_result.returncode != 0:
292
+ state.blocker = "detached_head"
293
+ return state
294
+ state.branch = branch_result.stdout.strip()
295
+ if state.branch not in SUPPORTED_BRANCHES:
296
+ state.blocker = "unsupported_branch"
297
+ return state
298
+
299
+ # 未跟踪文件不影响快进;若与新版本文件冲突,Git 会拒绝合并且不改动任何文件。
300
+ status = _git(root, "status", "--porcelain", "--untracked-files=no")
301
+ state.dirty_files = [line[3:] for line in status.splitlines() if line.strip()][:MAX_LISTED_DIRTY_FILES]
302
+ return state
303
+
304
+
305
+ def _collect_apply_blockers(root: Path, local: LocalState, target: str, changed: Set[str]) -> List[str]:
306
+ """列出阻止把本地仓库更新到 target 的原因;检查、登记、执行三处共用同一套判断。"""
307
+
308
+ blockers: List[str] = []
309
+ if local.dirty_files:
310
+ blockers.append("dirty_worktree")
311
+ if not _is_ancestor(root, "HEAD", target):
312
+ blockers.append("diverged")
313
+ if DEPENDENCY_FILES & changed and local.dependency_tool is None:
314
+ blockers.append("no_dependency_tool")
315
+ # Python 版本要求变化时,uv 可能重建整个虚拟环境,交给用户手动处理。
316
+ if _read_pyproject_at(root, "HEAD")[1] != _read_pyproject_at(root, target)[1]:
317
+ blockers.append("python_requirement_changed")
318
+ return blockers
319
+
320
+
321
+ def evaluate_update(local: LocalState, target: str, checked_at: float, root: Path = PROJECT_ROOT) -> UpdateCheck:
322
+ """基于已经取回的目标提交计算更新结论,不联网。"""
323
+
324
+ if local.commit is None:
325
+ raise ValueError("本地仓库状态缺少当前提交,无法评估更新")
326
+
327
+ counts = _git(root, "rev-list", "--left-right", "--count", f"{local.commit}...{target}").split()
328
+ changed = _changed_files(root, local.commit, target)
329
+ return UpdateCheck(
330
+ local=local,
331
+ checked_at=checked_at,
332
+ latest_commit=target,
333
+ latest_version=_read_pyproject_at(root, target)[0],
334
+ ahead=int(counts[0]),
335
+ behind=int(counts[1]),
336
+ dependencies_changed=bool(DEPENDENCY_FILES & changed),
337
+ commits=_list_commits(root, local.commit, target),
338
+ blockers=_collect_apply_blockers(root, local, target, changed),
339
+ )
340
+
341
+
342
+ def load_update_sources(root: Path = PROJECT_ROOT) -> List[UpdateSource]:
343
+ """按优先级列出更新来源,与插件市场使用同一份镜像源配置。
344
+
345
+ 尚未生成镜像源配置时只有官方地址。
346
+ """
347
+
348
+ official = UpdateSource(id=OFFICIAL_SOURCE_ID, name="GitHub 官方源", url=OFFICIAL_REPOSITORY_URL)
349
+ instance_root = get_instance_root() if root == PROJECT_ROOT else root
350
+ config_path = instance_root.joinpath(*_MIRROR_CONFIG_PARTS)
351
+ if not config_path.is_file():
352
+ return [official]
353
+ mirrors: List[Dict[str, Any]] = json.loads(config_path.read_text(encoding="utf-8")).get("git_mirrors", [])
354
+ if not mirrors:
355
+ return [official]
356
+
357
+ sources: List[UpdateSource] = []
358
+ for mirror in sorted(mirrors, key=lambda item: item.get("priority", 999)):
359
+ if not mirror.get("enabled", False):
360
+ continue
361
+ mirror_id = str(mirror["id"])
362
+ if mirror_id == OFFICIAL_SOURCE_ID:
363
+ sources.append(official)
364
+ continue
365
+ prefix = str(mirror["clone_prefix"]).rstrip("/")
366
+ parsed = urlsplit(prefix)
367
+ # 只走 HTTPS:其他传输方式(本地路径、ext 等)不应出现在更新来源里。
368
+ if parsed.scheme != "https" or not parsed.hostname:
369
+ raise SelfUpdateError("invalid_source", f"镜像源 {mirror_id} 的克隆前缀不是合法的 HTTPS 地址")
370
+ sources.append(
371
+ UpdateSource(
372
+ id=mirror_id,
373
+ name=str(mirror.get("name") or mirror_id),
374
+ url=f"{prefix}/{OFFICIAL_REPOSITORY_PATH}.git",
375
+ )
376
+ )
377
+ if not sources:
378
+ raise SelfUpdateError(
379
+ "no_update_source", "镜像源配置中没有启用任何来源,请在插件市场的镜像源设置里启用至少一个"
380
+ )
381
+ return sources
382
+
383
+
384
+ def _fetch_branch_head(root: Path, branch: str) -> Tuple[str, UpdateSource]:
385
+ """依次尝试各个来源取回官方仓库的指定分支,返回其最新提交与实际使用的来源。"""
386
+
387
+ failures: List[str] = []
388
+ for source in load_update_sources(root):
389
+ try:
390
+ _git(
391
+ root,
392
+ *_GIT_STALL_OPTIONS,
393
+ "fetch",
394
+ "--no-tags",
395
+ "--quiet",
396
+ source.url,
397
+ f"refs/heads/{branch}",
398
+ timeout=GIT_FETCH_TIMEOUT_SECONDS,
399
+ )
400
+ except SelfUpdateError as exc:
401
+ reason = exc.message.strip().splitlines()[-1]
402
+ failures.append(f"- {source.name}: {reason}")
403
+ continue
404
+ return _git(root, "rev-parse", "FETCH_HEAD^{commit}").strip(), source
405
+ raise SelfUpdateError("fetch_failed", "所有更新来源均获取失败:\n" + "\n".join(failures))
406
+
407
+
408
+ def check_for_update(root: Path = PROJECT_ROOT) -> UpdateCheck:
409
+ """联网检查官方仓库是否有新版本。"""
410
+
411
+ local = inspect_local(root)
412
+ if local.blocker is not None:
413
+ return UpdateCheck(local=local, blockers=[local.blocker])
414
+ if local.branch is None:
415
+ raise ValueError("本地仓库状态缺少当前分支,无法检查更新")
416
+ target, source = _fetch_branch_head(root, local.branch)
417
+ check = evaluate_update(local, target, time.time(), root)
418
+ check.source = source
419
+ return check
420
+
421
+
422
+ def request_update(expected_commit: str, root: Path = PROJECT_ROOT) -> PendingUpdate:
423
+ """登记更新请求,等待 Runner 在下次重启时执行。
424
+
425
+ expected_commit 是用户在界面上确认过的目标提交;Runner 只会更新到这个提交。
426
+ """
427
+
428
+ if _COMMIT_PATTERN.fullmatch(expected_commit) is None:
429
+ raise SelfUpdateError("invalid_commit", "目标提交格式不正确")
430
+
431
+ local = inspect_local(root)
432
+ if local.blocker is not None:
433
+ raise SelfUpdateError(local.blocker, BLOCKER_MESSAGES[local.blocker])
434
+ if local.branch is None or local.commit is None:
435
+ raise ValueError("本地仓库状态缺少当前分支或提交,无法登记更新")
436
+ if expected_commit == local.commit:
437
+ raise SelfUpdateError("already_up_to_date", "当前已经是这个版本")
438
+
439
+ object_check = _run_command(["git", "cat-file", "-e", f"{expected_commit}^{{commit}}"], root, GIT_TIMEOUT_SECONDS)
440
+ if object_check.returncode != 0:
441
+ raise SelfUpdateError("unknown_commit", "本地没有这个目标提交,请重新检查更新")
442
+
443
+ changed = _changed_files(root, local.commit, expected_commit)
444
+ blockers = _collect_apply_blockers(root, local, expected_commit, changed)
445
+ if blockers:
446
+ raise SelfUpdateError(blockers[0], BLOCKER_MESSAGES[blockers[0]])
447
+
448
+ pending = PendingUpdate(
449
+ target_commit=expected_commit,
450
+ base_commit=local.commit,
451
+ branch=local.branch,
452
+ requested_at=time.time(),
453
+ )
454
+ _write_json(_pending_path(root), asdict(pending))
455
+ return pending
456
+
457
+
458
+ def clear_pending_update(root: Path = PROJECT_ROOT) -> None:
459
+ """丢弃尚未执行的更新请求。"""
460
+
461
+ _pending_path(root).unlink(missing_ok=True)
462
+
463
+
464
+ def read_last_result(root: Path = PROJECT_ROOT) -> Optional[UpdateResult]:
465
+ """读取最近一次更新的执行结果。"""
466
+
467
+ result_path = _result_path(root)
468
+ if not result_path.is_file():
469
+ return None
470
+ return UpdateResult(**json.loads(result_path.read_text(encoding="utf-8")))
471
+
472
+
473
+ def _execute_update(pending: PendingUpdate, result: UpdateResult, logger: Any, root: Path) -> None:
474
+ result.stage = "precheck"
475
+ if time.time() - pending.requested_at > PENDING_MAX_AGE_SECONDS:
476
+ raise SelfUpdateError("request_expired", "更新请求已过期,请重新发起")
477
+ local = inspect_local(root)
478
+ if local.blocker is not None:
479
+ raise SelfUpdateError(local.blocker, BLOCKER_MESSAGES[local.blocker])
480
+ if local.branch != pending.branch or local.commit != pending.base_commit:
481
+ raise SelfUpdateError("state_changed", "登记更新之后本地仓库已发生变化,请重新检查更新")
482
+ result.from_version = local.version
483
+
484
+ # 重新取回官方分支确认:目标提交必须在它的历史里,状态文件本身不被信任。
485
+ # 来源只从镜像源配置推导,状态文件无法指定地址。
486
+ result.stage = "verify"
487
+ logger.info(f"自更新:正在确认官方仓库的 {pending.branch} 分支")
488
+ official_head, source = _fetch_branch_head(root, pending.branch)
489
+ result.source_name = source.name
490
+ logger.info(f"自更新:更新来源 {source.name}")
491
+ if not _is_ancestor(root, pending.target_commit, official_head):
492
+ raise SelfUpdateError("untrusted_commit", f"目标提交不在官方仓库 {pending.branch} 分支的历史中")
493
+ changed = _changed_files(root, pending.base_commit, pending.target_commit)
494
+ blockers = _collect_apply_blockers(root, local, pending.target_commit, changed)
495
+ if blockers:
496
+ raise SelfUpdateError(blockers[0], BLOCKER_MESSAGES[blockers[0]])
497
+ result.to_version = _read_pyproject_at(root, pending.target_commit)[0]
498
+
499
+ result.stage = "merge"
500
+ logger.info(f"自更新:{pending.base_commit[:10]} -> {pending.target_commit[:10]}")
501
+ try:
502
+ _git(root, "merge", "--ff-only", pending.target_commit)
503
+ except SelfUpdateError:
504
+ # 文件被占用等原因可能让检出停在一半,需要把工作区恢复到更新前。
505
+ _rollback(pending, None, result, logger, root)
506
+ raise
507
+
508
+ dependencies_changed = bool(DEPENDENCY_FILES & changed)
509
+ if dependencies_changed and local.dependency_tool is not None:
510
+ result.stage = "dependencies"
511
+ logger.info(f"自更新:正在同步依赖({local.dependency_tool}),可能需要几分钟")
512
+ try:
513
+ _sync_dependencies(root, local.dependency_tool)
514
+ except SelfUpdateError:
515
+ _rollback(pending, local.dependency_tool, result, logger, root)
516
+ raise
517
+ result.dependencies_synced = True
518
+
519
+ result.stage = "done"
520
+ result.success = True
521
+
522
+
523
+ def _rollback(
524
+ pending: PendingUpdate,
525
+ dependency_tool: Optional[str],
526
+ result: UpdateResult,
527
+ logger: Any,
528
+ root: Path,
529
+ ) -> None:
530
+ """退回更新前的提交;dependency_tool 不为空时把依赖也同步回旧版本。"""
531
+
532
+ logger.error(f"自更新:{result.stage} 阶段失败,正在回滚到更新前的版本")
533
+ try:
534
+ # precheck 已确认没有未提交的已跟踪改动,这里丢弃的只有本次更新写入的内容。
535
+ _git(root, "reset", "--hard", pending.base_commit)
536
+ if dependency_tool is not None:
537
+ _sync_dependencies(root, dependency_tool)
538
+ except SelfUpdateError as exc:
539
+ result.rollback_error = exc.message
540
+ logger.error(f"自更新:回滚失败,请手动检查仓库与依赖状态: {exc.message}")
541
+ return
542
+ result.rolled_back = True
543
+
544
+
545
+ def apply_pending_update(logger: Any, root: Path = PROJECT_ROOT) -> Optional[UpdateResult]:
546
+ """由 Runner 在两次 Worker 之间调用:执行已登记的更新并记录结果。
547
+
548
+ 没有待执行的请求时返回 None。无论成功与否,请求都只会被执行一次。
549
+ 更新成功后,调用方应让新代码的 Runner 接手(见 process_runner.run_runner)。
550
+ """
551
+
552
+ pending_path = _pending_path(root)
553
+ if not pending_path.is_file():
554
+ return None
555
+
556
+ raw_request = pending_path.read_text(encoding="utf-8")
557
+ # 先删除请求再执行:即使更新中途失败,下一次重启也不会重复尝试。
558
+ pending_path.unlink()
559
+
560
+ result = UpdateResult(success=False, started_at=time.time(), stage="request")
561
+ try:
562
+ pending = PendingUpdate(**json.loads(raw_request))
563
+ result.branch = pending.branch
564
+ result.from_commit = pending.base_commit
565
+ result.to_commit = pending.target_commit
566
+ _execute_update(pending, result, logger, root)
567
+ except SelfUpdateError as exc:
568
+ result.error_code = exc.code
569
+ result.error = exc.message
570
+ except (OSError, TypeError, ValueError, subprocess.SubprocessError) as exc:
571
+ result.error_code = "unexpected_error"
572
+ result.error = f"{type(exc).__name__}: {exc}"
573
+
574
+ result.finished_at = time.time()
575
+ _write_json(_result_path(root), asdict(result))
576
+ if result.success:
577
+ logger.info(f"自更新完成:{result.from_version} -> {result.to_version},正在启动新版本")
578
+ else:
579
+ logger.error(f"自更新失败(阶段 {result.stage}),继续使用当前版本: {result.error}")
580
+ return result
src/common/shutdown.py ADDED
@@ -0,0 +1,55 @@
1
+ """进程级关停状态。"""
2
+
3
+ from threading import Event
4
+ from types import FrameType
5
+ from typing import Callable, Iterator
6
+
7
+ import contextlib
8
+ import signal
9
+
10
+ _shutdown_requested = Event()
11
+
12
+
13
+ def request_shutdown(reason: str = "") -> None:
14
+ """标记当前进程正在关停。"""
15
+
16
+ del reason
17
+ _shutdown_requested.set()
18
+
19
+
20
+ def is_shutdown_requested() -> bool:
21
+ """返回当前进程是否已经进入关停流程。"""
22
+
23
+ return _shutdown_requested.is_set()
24
+
25
+
26
+ @contextlib.contextmanager
27
+ def application_signal_handlers(handler: Callable[[int, FrameType | None], None]) -> Iterator[None]:
28
+ """Worker 统一拥有停止信号,包括进程内的第三方 Uvicorn 服务。
29
+
30
+ Uvicorn 的 capture_signals 会覆盖应用 handler,并在退出时重新发送信号。
31
+ 在 Worker 生命周期内关闭这个捕获层,避免 HTTP 服务关闭代替应用关闭。
32
+ 作用域退出时还原;不影响 Runner、独立服务器或其他进程。
33
+ """
34
+ from uvicorn import Server
35
+
36
+ signals = [signal.SIGINT, signal.SIGTERM]
37
+ if hasattr(signal, "SIGBREAK"):
38
+ signals.append(signal.SIGBREAK)
39
+ # 依赖 uvicorn 非公开调用链 Server.serve() → capture_signals();uv.lock 锁定并验证于 0.45.0。
40
+ # maim_message 的额外 API 会自行创建 uvicorn.Server,无法只在本项目构造处子类化,因此替换类属性。
41
+ # 先读取属性:未来版本移除该方法时,在修改任何信号处理器之前明确失败。
42
+ capture_signals = Server.capture_signals
43
+ previous = {sig: signal.signal(sig, handler) for sig in signals}
44
+
45
+ @contextlib.contextmanager
46
+ def application_owned_signals(_server):
47
+ yield
48
+
49
+ Server.capture_signals = application_owned_signals
50
+ try:
51
+ yield
52
+ finally:
53
+ Server.capture_signals = capture_signals
54
+ for sig, original in previous.items():
55
+ signal.signal(sig, original)