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,1018 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Tuple
6
+
7
+ import asyncio
8
+ import base64
9
+ import json
10
+ import os
11
+ import stat
12
+ import time
13
+ import uuid
14
+
15
+ import tomlkit
16
+
17
+ from src.common.logger import get_logger
18
+ from src.common.runtime_loop import run_on_main_loop
19
+ from src.common.utils.utils_config import AMemorixConfigUtils
20
+ from src.config.official_configs import AMemorixConfig
21
+ from src.webui.utils.toml_utils import _update_toml_doc
22
+
23
+ from .core.runtime.admin_contracts import (
24
+ AdminContractError,
25
+ dispatch_admin_command,
26
+ is_admin_component,
27
+ parse_admin_command,
28
+ )
29
+ from .paths import default_data_dir, repo_root, resolve_repo_path, schema_path
30
+ from .runtime_registry import set_runtime_kernel
31
+
32
+ if TYPE_CHECKING:
33
+ from .core.runtime.sdk_memory_kernel import SDKMemoryKernel
34
+
35
+ logger = get_logger("a_memorix.host_service")
36
+
37
+ _INTERNAL_CONFIG_FIELDS = {"field_docs", "_validate_any", "suppress_any_warning"}
38
+
39
+
40
+ def _get_config_manager():
41
+ from src.config.config import config_manager
42
+
43
+ return config_manager
44
+
45
+
46
+ def _get_bot_config_path() -> Path:
47
+ from src.config.config import BOT_CONFIG_PATH
48
+
49
+ return BOT_CONFIG_PATH
50
+
51
+
52
+ def _to_builtin_data(obj: Any) -> Any:
53
+ if hasattr(obj, "unwrap"):
54
+ try:
55
+ obj = obj.unwrap()
56
+ except (AttributeError, TypeError, ValueError) as exc:
57
+ logger.debug(f"配置值 unwrap 失败,保留原值: type={type(obj).__name__}, error={exc}")
58
+
59
+ if isinstance(obj, dict):
60
+ return {str(key): _to_builtin_data(value) for key, value in obj.items()}
61
+ if isinstance(obj, list):
62
+ return [_to_builtin_data(value) for value in obj]
63
+ return obj
64
+
65
+
66
+ def _strip_internal_config_fields(obj: Any) -> Any:
67
+ if isinstance(obj, dict):
68
+ return {
69
+ str(key): _strip_internal_config_fields(value)
70
+ for key, value in obj.items()
71
+ if str(key) not in _INTERNAL_CONFIG_FIELDS
72
+ }
73
+ if isinstance(obj, list):
74
+ return [_strip_internal_config_fields(value) for value in obj]
75
+ return obj
76
+
77
+
78
+ def _backup_config_file(path: Path) -> Optional[Path]:
79
+ if not path.exists():
80
+ return None
81
+ backup_name = f"{path.name}.backup.{datetime.now().strftime('%Y%m%d%H%M%S%f')}.{uuid.uuid4().hex}"
82
+ backup_path = path.parent / backup_name
83
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0)
84
+ try:
85
+ with os.fdopen(os.open(backup_path, flags, 0o600), "wb") as handle:
86
+ handle.write(path.read_bytes())
87
+ handle.flush()
88
+ os.fsync(handle.fileno())
89
+ os.chmod(backup_path, stat.S_IMODE(path.stat().st_mode))
90
+ except Exception:
91
+ backup_path.unlink(missing_ok=True)
92
+ raise
93
+ return backup_path
94
+
95
+
96
+ class AMemorixHostService:
97
+ def __init__(self) -> None:
98
+ self._lock = asyncio.Lock()
99
+ self._config_update_lock = asyncio.Lock()
100
+ self._queue_lock = asyncio.Lock()
101
+ self._kernel: Optional[SDKMemoryKernel] = None
102
+ self._startup_task: Optional[asyncio.Task] = None
103
+ self._runtime_state = "stopped"
104
+ self._startup_error = ""
105
+ self._startup_error_stage = ""
106
+ self._startup_started_at: Optional[float] = None
107
+ self._startup_finished_at: Optional[float] = None
108
+ self._startup_queue_pending_count = 0
109
+ self._startup_queue_cache_loaded = False
110
+ self._config_cache: Dict[str, Any] | None = None
111
+ self._config_reload_callback_suppressed_task: Optional[asyncio.Task[Any]] = None
112
+ self._reload_callback_registered = False
113
+
114
+ async def start(self) -> None:
115
+ if not self.is_enabled():
116
+ logger.info("A_Memorix 未启用,跳过长期记忆运行时初始化")
117
+ return
118
+ await self._ensure_startup_task()
119
+
120
+ async def stop(self) -> None:
121
+ async with self._lock:
122
+ await self._shutdown_locked()
123
+
124
+ async def reload(self) -> None:
125
+ async with self._lock:
126
+ await self._shutdown_locked()
127
+ self._config_cache = None
128
+ config = self._read_config()
129
+
130
+ if self._is_enabled_config(config):
131
+ await self._ensure_startup_task()
132
+ else:
133
+ logger.info("A_Memorix 配置为未启用,运行时保持关闭")
134
+
135
+ def get_config_path(self) -> Path:
136
+ return _get_bot_config_path()
137
+
138
+ def get_schema_path(self) -> Path:
139
+ return schema_path()
140
+
141
+ def get_config_schema(self) -> Dict[str, Any]:
142
+ path = self.get_schema_path()
143
+ if not path.exists():
144
+ return {
145
+ "plugin_id": "a_memorix",
146
+ "plugin_info": {
147
+ "name": "A_Memorix",
148
+ "version": "",
149
+ "description": "A_Memorix 配置结构",
150
+ "author": "A_Dawn",
151
+ },
152
+ "sections": {},
153
+ "layout": {"type": "auto", "tabs": []},
154
+ }
155
+
156
+ with path.open("r", encoding="utf-8") as handle:
157
+ return json.load(handle)
158
+
159
+ def get_config(self) -> Dict[str, Any]:
160
+ return dict(self._read_config())
161
+
162
+ def get_runtime_data_dir(self) -> Path:
163
+ return self._runtime_data_dir()
164
+
165
+ def is_enabled(self) -> bool:
166
+ return self._is_enabled_config(self._read_config())
167
+
168
+ @staticmethod
169
+ def _is_enabled_config(config: Dict[str, Any]) -> bool:
170
+ plugin_config = config.get("plugin") if isinstance(config, dict) else None
171
+ if not isinstance(plugin_config, dict):
172
+ return True
173
+ return bool(plugin_config.get("enabled", True))
174
+
175
+ def _build_default_config(self) -> Dict[str, Any]:
176
+ return self._config_model_to_runtime_dict(AMemorixConfig())
177
+
178
+ def get_raw_config_with_meta(self) -> Dict[str, Any]:
179
+ config = self.get_config()
180
+ default_config = self._build_default_config()
181
+ raw_doc = tomlkit.document()
182
+ raw_doc.add("a_memorix", config)
183
+ return {
184
+ "config": tomlkit.dumps(raw_doc),
185
+ "exists": self.get_config_path().exists(),
186
+ "using_default": config == default_config,
187
+ }
188
+
189
+ def get_raw_config(self) -> str:
190
+ payload = self.get_raw_config_with_meta()
191
+ return str(payload.get("config", "") or "")
192
+
193
+ async def update_raw_config(self, raw_config: str) -> Dict[str, Any]:
194
+ loaded = tomlkit.loads(raw_config)
195
+ raw_payload = _to_builtin_data(loaded) if isinstance(loaded, dict) else {}
196
+ config_payload = raw_payload.get("a_memorix") if isinstance(raw_payload.get("a_memorix"), dict) else raw_payload
197
+ path, backup_path = await self._write_config_to_bot_config(config_payload, replace=True)
198
+ return {
199
+ "success": True,
200
+ "message": "配置已保存",
201
+ "backup_path": str(backup_path) if backup_path is not None else "",
202
+ "config_path": str(path),
203
+ }
204
+
205
+ async def update_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
206
+ path, backup_path = await self._write_config_to_bot_config(config)
207
+ return {
208
+ "success": True,
209
+ "message": "配置已保存",
210
+ "backup_path": str(backup_path) if backup_path is not None else "",
211
+ "config_path": str(path),
212
+ }
213
+
214
+ async def invoke(
215
+ self,
216
+ component_name: str,
217
+ args: Dict[str, Any] | None = None,
218
+ *,
219
+ timeout_ms: Optional[int] = None,
220
+ ) -> Any:
221
+ """将跨线程管理请求统一投递到主循环,保证内核的异步锁有效。"""
222
+ return await run_on_main_loop(self._invoke_on_runtime(component_name, args, timeout_ms=timeout_ms))
223
+
224
+ async def _invoke_on_runtime(
225
+ self,
226
+ component_name: str,
227
+ args: Dict[str, Any] | None = None,
228
+ *,
229
+ timeout_ms: Optional[int] = None,
230
+ ) -> Any:
231
+ """将 MaiBot 宿主请求路由到共享 A_Memorix 内核。
232
+
233
+ 本层负责启动状态、宿主参数适配、共享聊天范围、启动期写入排队和管理命令
234
+ 分发;检索、写入及维护操作的业务语义由内核服务负责。
235
+ """
236
+ if timeout_ms is None:
237
+ return await self._invoke(component_name, args)
238
+ if not isinstance(timeout_ms, int) or isinstance(timeout_ms, bool) or timeout_ms <= 0:
239
+ raise ValueError("timeout_ms 必须是正整数")
240
+ try:
241
+ async with asyncio.timeout(timeout_ms / 1000):
242
+ return await self._invoke(component_name, args)
243
+ except TimeoutError as exc:
244
+ raise TimeoutError(f"A_Memorix 调用超时: component={component_name}, timeout_ms={timeout_ms}") from exc
245
+
246
+ async def _invoke(self, component_name: str, args: Dict[str, Any] | None = None) -> Any:
247
+ payload = args or {}
248
+ if not self.is_enabled():
249
+ return self._disabled_response(component_name)
250
+ kernel = self._kernel
251
+ if kernel is None or self._runtime_state != "ready":
252
+ if self._runtime_state == "stopped":
253
+ await self._ensure_startup_task()
254
+ return await self._unavailable_response(component_name, payload)
255
+
256
+ if component_name == "search_memory":
257
+ from .core.runtime.sdk_memory_kernel import KernelSearchRequest
258
+
259
+ chat_id = str(payload.get("chat_id", "") or "").strip()
260
+ config = self._read_config()
261
+ global_memory_sharing_enabled = bool(config.get("global_memory_sharing_enabled", False))
262
+ search_chat_id = "" if global_memory_sharing_enabled else chat_id
263
+ shared_chat_ids = ()
264
+ if not global_memory_sharing_enabled:
265
+ shared_chat_ids = tuple(AMemorixConfigUtils.get_shared_memory_session_ids(chat_id))
266
+
267
+ return await kernel.search_memory(
268
+ KernelSearchRequest(
269
+ query=str(payload.get("query", "") or ""),
270
+ limit=int(payload.get("limit", 5) or 5),
271
+ mode=str(payload.get("mode", "search") or "search"),
272
+ chat_id=search_chat_id,
273
+ shared_chat_ids=shared_chat_ids,
274
+ person_id=str(payload.get("person_id", "") or ""),
275
+ time_start=payload.get("time_start"),
276
+ time_end=payload.get("time_end"),
277
+ respect_filter=bool(payload.get("respect_filter", True)),
278
+ user_id=str(payload.get("user_id", "") or "").strip(),
279
+ group_id=str(payload.get("group_id", "") or "").strip(),
280
+ )
281
+ )
282
+
283
+ if component_name == "enqueue_feedback_task":
284
+ return await kernel.enqueue_feedback_task(
285
+ query_tool_id=str(payload.get("query_tool_id", "") or ""),
286
+ session_id=str(payload.get("session_id", "") or ""),
287
+ query_timestamp=payload.get("query_timestamp"),
288
+ structured_content=payload.get("structured_content")
289
+ if isinstance(payload.get("structured_content"), dict)
290
+ else {},
291
+ )
292
+
293
+ if component_name in {"ingest_summary", "ingest_text"}:
294
+ return await self._dispatch_ingest_write(kernel, component_name, payload)
295
+
296
+ if component_name == "get_person_profile":
297
+ return await kernel.get_person_profile(
298
+ person_id=str(payload.get("person_id", "") or ""),
299
+ chat_id=str(payload.get("chat_id", "") or ""),
300
+ limit=max(1, int(payload.get("limit", 10) or 10)),
301
+ )
302
+
303
+ if component_name == "maintain_memory":
304
+ return await kernel.maintain_memory(
305
+ action=str(payload.get("action", "") or ""),
306
+ target=str(payload.get("target", "") or ""),
307
+ hours=payload.get("hours"),
308
+ reason=str(payload.get("reason", "") or ""),
309
+ limit=max(1, int(payload.get("limit", 50) or 50)),
310
+ )
311
+
312
+ if component_name == "memory_stats":
313
+ return kernel.memory_stats()
314
+
315
+ if component_name == "image_memory":
316
+ action = str(payload.get("action") or "").strip().lower()
317
+ scoped_payload = dict(payload)
318
+ if action in {"search", "get"} and "chat_ids" not in scoped_payload:
319
+ chat_id = str(payload.get("chat_id") or "").strip()
320
+ config = self._read_config()
321
+ if bool(config.get("global_memory_sharing_enabled", False)):
322
+ scoped_payload["chat_ids"] = None
323
+ else:
324
+ scoped_payload["chat_ids"] = [
325
+ item
326
+ for item in (chat_id, *AMemorixConfigUtils.get_shared_memory_session_ids(chat_id))
327
+ if item
328
+ ]
329
+ scoped_payload.pop("action", None)
330
+ return await kernel.image_memory(action=action, **scoped_payload)
331
+
332
+ if is_admin_component(component_name):
333
+ try:
334
+ command = parse_admin_command(component_name, payload)
335
+ except AdminContractError as exc:
336
+ return exc.to_response()
337
+ result = await dispatch_admin_command(kernel, command)
338
+ if command.component_name == "memory_runtime_admin" and isinstance(result, dict):
339
+ return {**result, **self._startup_status_payload()}
340
+ return result
341
+
342
+ raise RuntimeError(f"不支持的 A_Memorix 调用: {component_name}")
343
+
344
+ async def _ensure_kernel(self) -> SDKMemoryKernel:
345
+ async with self._lock:
346
+ if self._kernel is None:
347
+ raise RuntimeError(self._startup_error or "A_Memorix 正在初始化")
348
+ return self._kernel
349
+
350
+ async def _ensure_startup_task(self) -> None:
351
+ await self._ensure_startup_queue_cache()
352
+ async with self._lock:
353
+ if self._kernel is not None and self._runtime_state == "ready":
354
+ return
355
+ if self._startup_task is not None and not self._startup_task.done():
356
+ return
357
+ self._runtime_state = "starting"
358
+ self._startup_error = ""
359
+ self._startup_error_stage = ""
360
+ self._startup_started_at = time.time()
361
+ self._startup_finished_at = None
362
+ self._startup_task = asyncio.create_task(self._startup_kernel_task(), name="A_Memorix.host_startup")
363
+
364
+ async def _startup_kernel_task(self) -> None:
365
+ from .core.runtime.sdk_memory_kernel import SDKMemoryKernel
366
+
367
+ kernel: Optional[SDKMemoryKernel] = None
368
+ try:
369
+ config = self._read_config()
370
+ if not self._is_enabled_config(config):
371
+ async with self._lock:
372
+ self._runtime_state = "stopped"
373
+ self._startup_finished_at = time.time()
374
+ return
375
+ async with self._lock:
376
+ self._runtime_state = "migrating"
377
+ self._startup_error_stage = "startup_migration"
378
+ kernel = SDKMemoryKernel(plugin_root=repo_root(), config=config)
379
+ await kernel.initialize()
380
+ async with self._lock:
381
+ self._kernel = kernel
382
+ self._runtime_state = "ready"
383
+ self._startup_error = ""
384
+ self._startup_error_stage = ""
385
+ self._startup_finished_at = time.time()
386
+ set_runtime_kernel(kernel)
387
+ await self._replay_startup_write_queue(kernel)
388
+ async with self._lock:
389
+ if self._kernel is kernel and self._runtime_state == "ready":
390
+ self._startup_finished_at = time.time()
391
+ except asyncio.CancelledError:
392
+ if kernel is not None:
393
+ shutdown = getattr(kernel, "shutdown", None)
394
+ if callable(shutdown):
395
+ await shutdown()
396
+ else:
397
+ kernel.close()
398
+ if self._kernel is kernel:
399
+ self._kernel = None
400
+ self._runtime_state = "stopped"
401
+ self._startup_finished_at = time.time()
402
+ set_runtime_kernel(None)
403
+ raise
404
+ except Exception as exc:
405
+ if kernel is not None:
406
+ shutdown = getattr(kernel, "shutdown", None)
407
+ if callable(shutdown):
408
+ await shutdown()
409
+ else:
410
+ kernel.close()
411
+ set_runtime_kernel(None)
412
+ async with self._lock:
413
+ self._kernel = None
414
+ self._runtime_state = "failed"
415
+ self._startup_error = str(exc)
416
+ if not self._startup_error_stage:
417
+ self._startup_error_stage = "startup"
418
+ self._startup_finished_at = time.time()
419
+ logger.error(f"A_Memorix 后台初始化失败: {exc}", exc_info=True)
420
+
421
+ def _read_config(self) -> Dict[str, Any]:
422
+ if self._config_cache is not None:
423
+ return dict(self._config_cache)
424
+
425
+ try:
426
+ config_model = _get_config_manager().get_global_config().a_memorix
427
+ except Exception as exc:
428
+ logger.warning(f"读取 A_Memorix 主配置失败,使用默认值: {exc}")
429
+ defaults = self._build_default_config()
430
+ self._config_cache = defaults
431
+ return dict(defaults)
432
+
433
+ self._config_cache = self._config_model_to_runtime_dict(config_model)
434
+ return dict(self._config_cache)
435
+
436
+ def _runtime_data_dir(self) -> Path:
437
+ config = self._read_config()
438
+ storage_cfg = config.get("storage") if isinstance(config, dict) else {}
439
+ data_dir = "./data"
440
+ if isinstance(storage_cfg, dict):
441
+ data_dir = str(storage_cfg.get("data_dir", data_dir) or data_dir)
442
+ return resolve_repo_path(data_dir, fallback=default_data_dir())
443
+
444
+ def _startup_queue_path(self) -> Path:
445
+ return self._runtime_data_dir() / "startup_write_queue.jsonl"
446
+
447
+ def _startup_queue_done_path(self) -> Path:
448
+ return self._runtime_data_dir() / "startup_write_queue.done.jsonl"
449
+
450
+ def _startup_queue_failed_path(self) -> Path:
451
+ return self._runtime_data_dir() / "startup_write_queue.failed.jsonl"
452
+
453
+ @staticmethod
454
+ def _ensure_jsonl_append_boundary(path: Path) -> None:
455
+ if not path.exists() or path.stat().st_size <= 0:
456
+ return
457
+ with path.open("rb+") as handle:
458
+ handle.seek(-1, os.SEEK_END)
459
+ if handle.read(1) == b"\n":
460
+ return
461
+ handle.seek(0, os.SEEK_END)
462
+ handle.write(b"\n")
463
+ handle.flush()
464
+ os.fsync(handle.fileno())
465
+
466
+ def _append_jsonl_sync(self, path: Path, payload: Dict[str, Any]) -> None:
467
+ path.parent.mkdir(parents=True, exist_ok=True)
468
+ line = json.dumps(payload, ensure_ascii=False, sort_keys=True)
469
+ self._ensure_jsonl_append_boundary(path)
470
+ with path.open("a", encoding="utf-8") as handle:
471
+ handle.write(line + "\n")
472
+ handle.flush()
473
+ os.fsync(handle.fileno())
474
+
475
+ async def _append_jsonl(self, path: Path, payload: Dict[str, Any]) -> None:
476
+ async with self._queue_lock:
477
+ await asyncio.to_thread(self._append_jsonl_sync, path, payload)
478
+
479
+ def _read_jsonl(self, path: Path) -> list[Dict[str, Any]]:
480
+ if not path.exists():
481
+ return []
482
+ rows: list[Dict[str, Any]] = []
483
+ with path.open("r", encoding="utf-8") as handle:
484
+ for line in handle:
485
+ token = line.strip()
486
+ if not token:
487
+ continue
488
+ try:
489
+ payload = json.loads(token)
490
+ except Exception:
491
+ continue
492
+ if isinstance(payload, dict):
493
+ rows.append(payload)
494
+ return rows
495
+
496
+ def _queue_done_ids(self) -> set[str]:
497
+ return {
498
+ str(item.get("record_id", "") or "").strip()
499
+ for item in self._read_jsonl(self._startup_queue_done_path())
500
+ if str(item.get("record_id", "") or "").strip()
501
+ }
502
+
503
+ def _startup_queue_pending_records(self) -> list[Dict[str, Any]]:
504
+ done_ids = self._queue_done_ids()
505
+ records = []
506
+ for item in self._read_jsonl(self._startup_queue_path()):
507
+ record_id = str(item.get("record_id", "") or "").strip()
508
+ component_name = str(item.get("component_name", "") or "").strip()
509
+ if not record_id or record_id in done_ids:
510
+ continue
511
+ if component_name not in {"ingest_summary", "ingest_text", "image_memory"}:
512
+ continue
513
+ records.append(item)
514
+ records.sort(key=lambda item: float(item.get("created_at", 0.0) or 0.0))
515
+ return records
516
+
517
+ async def _ensure_startup_queue_cache(self) -> None:
518
+ if self._startup_queue_cache_loaded:
519
+ return
520
+ async with self._queue_lock:
521
+ if self._startup_queue_cache_loaded:
522
+ return
523
+ records = await asyncio.to_thread(self._startup_queue_pending_records)
524
+ self._startup_queue_pending_count = len(records)
525
+ self._startup_queue_cache_loaded = True
526
+
527
+ def _startup_status_payload(self) -> Dict[str, Any]:
528
+ memory_enabled = self.is_enabled()
529
+ runtime_status: Dict[str, Any] = {}
530
+ if self._kernel is not None and self._runtime_state == "ready":
531
+ runtime_status = self._kernel._runtime_capability_status()
532
+ return {
533
+ "enabled": memory_enabled,
534
+ "memory_enabled": memory_enabled,
535
+ "runtime_ready": bool(runtime_status.get("runtime_ready", self._runtime_state == "ready")),
536
+ "retrieval_ready": bool(runtime_status.get("retrieval_ready", False)),
537
+ "degraded": bool(runtime_status.get("degraded", False)),
538
+ "retrieval_mode": str(runtime_status.get("retrieval_mode", "unavailable")),
539
+ "available_channels": list(runtime_status.get("available_channels", [])),
540
+ "unavailable_channels": list(runtime_status.get("unavailable_channels", [])),
541
+ "vector_health": dict(runtime_status.get("vector_health", {})),
542
+ "startup_state": self._runtime_state,
543
+ "initializing": self._runtime_state in {"starting", "migrating"},
544
+ "initialization_failed": self._runtime_state == "failed",
545
+ "error_stage": self._startup_error_stage,
546
+ "error": self._startup_error,
547
+ "startup_started_at": self._startup_started_at,
548
+ "startup_finished_at": self._startup_finished_at,
549
+ "startup_queue_pending": self._startup_queue_pending_count,
550
+ "data_dir": str(self._runtime_data_dir()),
551
+ }
552
+
553
+ async def _enqueue_startup_write(self, component_name: str, payload: Dict[str, Any]) -> Dict[str, Any]:
554
+ queued_payload = dict(payload)
555
+ image_bytes = queued_payload.pop("image_bytes", None)
556
+ if image_bytes is not None:
557
+ if not isinstance(image_bytes, (bytes, bytearray, memoryview)):
558
+ raise TypeError("启动队列中的 image_bytes 必须是字节数据")
559
+ queued_payload["image_base64"] = base64.b64encode(bytes(image_bytes)).decode("ascii")
560
+ record = {
561
+ "record_id": uuid.uuid4().hex,
562
+ "component_name": component_name,
563
+ "payload": queued_payload,
564
+ "created_at": time.time(),
565
+ }
566
+ await self._append_jsonl(self._startup_queue_path(), record)
567
+ self._startup_queue_pending_count += 1
568
+ return {
569
+ "success": True,
570
+ "queued": True,
571
+ "initializing": True,
572
+ "reason": "a_memorix_initializing_queued",
573
+ "record_id": record["record_id"],
574
+ "stored_ids": [],
575
+ "skipped_ids": [],
576
+ "detail": "A_Memorix 正在初始化,写入已进入启动队列",
577
+ }
578
+
579
+ async def _unavailable_response(self, component_name: str, payload: Dict[str, Any]) -> Dict[str, Any]:
580
+ status = self._startup_status_payload()
581
+ initializing = bool(status.get("initializing"))
582
+ failed = bool(status.get("initialization_failed"))
583
+ image_write = component_name == "image_memory" and str(payload.get("action") or "").strip().lower() in {
584
+ "ingest",
585
+ "describe",
586
+ }
587
+ if initializing and (component_name in {"ingest_summary", "ingest_text"} or image_write):
588
+ return await self._enqueue_startup_write(component_name, payload)
589
+
590
+ reason = "a_memorix_initializing" if initializing else "a_memorix_initialization_failed"
591
+ message = "A_Memorix 正在初始化" if initializing else f"A_Memorix 初始化失败: {self._startup_error}"
592
+ base = {
593
+ "success": component_name == "memory_runtime_admin"
594
+ or (not failed and component_name in {"search_memory", "get_person_profile", "memory_stats"}),
595
+ "reason": reason,
596
+ "message": message,
597
+ **status,
598
+ }
599
+ if component_name == "search_memory":
600
+ return {**base, "summary": "", "hits": [], "filtered": False}
601
+ if component_name == "get_person_profile":
602
+ return {**base, "summary": "", "traits": [], "evidence": []}
603
+ if component_name == "memory_stats":
604
+ return {**base, "paragraph_count": 0, "relation_count": 0, "episode_count": 0}
605
+ if component_name == "image_memory":
606
+ return {**base, "success": False, "hits": [], "error": message}
607
+ if component_name == "memory_runtime_admin":
608
+ return base
609
+ if component_name in {"ingest_summary", "ingest_text"} or image_write:
610
+ return {
611
+ **base,
612
+ "success": False,
613
+ "queued": False,
614
+ "stored_ids": [],
615
+ "skipped_ids": [reason],
616
+ "detail": message,
617
+ }
618
+ if component_name == "enqueue_feedback_task":
619
+ return {**base, "queued": False}
620
+ return {**base, "success": False, "error": message}
621
+
622
+ async def _dispatch_ingest_write(
623
+ self, kernel: SDKMemoryKernel, component_name: str, payload: Dict[str, Any]
624
+ ) -> Any:
625
+ if component_name == "ingest_summary":
626
+ return await kernel.ingest_summary(
627
+ external_id=str(payload.get("external_id", "") or ""),
628
+ chat_id=str(payload.get("chat_id", "") or ""),
629
+ text=str(payload.get("text", "") or ""),
630
+ participants=list(payload.get("participants") or []),
631
+ time_start=payload.get("time_start"),
632
+ time_end=payload.get("time_end"),
633
+ tags=list(payload.get("tags") or []),
634
+ metadata=payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {},
635
+ respect_filter=bool(payload.get("respect_filter", True)),
636
+ user_id=str(payload.get("user_id", "") or "").strip(),
637
+ group_id=str(payload.get("group_id", "") or "").strip(),
638
+ )
639
+ if component_name == "ingest_text":
640
+ return await kernel.ingest_text(
641
+ external_id=str(payload.get("external_id", "") or ""),
642
+ source_type=str(payload.get("source_type", "") or ""),
643
+ text=str(payload.get("text", "") or ""),
644
+ chat_id=str(payload.get("chat_id", "") or ""),
645
+ person_ids=list(payload.get("person_ids") or []),
646
+ participants=list(payload.get("participants") or []),
647
+ timestamp=payload.get("timestamp"),
648
+ time_start=payload.get("time_start"),
649
+ time_end=payload.get("time_end"),
650
+ tags=list(payload.get("tags") or []),
651
+ metadata=payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {},
652
+ entities=list(payload.get("entities") or []),
653
+ relations=list(payload.get("relations") or []),
654
+ respect_filter=bool(payload.get("respect_filter", True)),
655
+ user_id=str(payload.get("user_id", "") or "").strip(),
656
+ group_id=str(payload.get("group_id", "") or "").strip(),
657
+ )
658
+ if component_name == "image_memory":
659
+ action = str(payload.get("action") or "").strip().lower()
660
+ if action not in {"ingest", "describe"}:
661
+ raise ValueError(f"不支持的启动队列图片写入操作: {action}")
662
+ image_payload = dict(payload)
663
+ image_payload.pop("action", None)
664
+ return await kernel.image_memory(action=action, **image_payload)
665
+ raise ValueError(f"不支持的启动队列写入类型: {component_name}")
666
+
667
+ async def _replay_startup_write_queue(self, kernel: SDKMemoryKernel) -> None:
668
+ records = await asyncio.to_thread(self._startup_queue_pending_records)
669
+ self._startup_queue_pending_count = len(records)
670
+ self._startup_queue_cache_loaded = True
671
+ if not records:
672
+ return
673
+ logger.info(f"A_Memorix 开始回放启动写入队列: pending={len(records)}")
674
+ for record in records:
675
+ record_id = str(record.get("record_id", "") or "").strip()
676
+ component_name = str(record.get("component_name", "") or "").strip()
677
+ payload = record.get("payload") if isinstance(record.get("payload"), dict) else {}
678
+ try:
679
+ result = await self._dispatch_ingest_write(kernel, component_name, payload)
680
+ if isinstance(result, dict) and result.get("success") is False:
681
+ raise RuntimeError(str(result.get("detail") or result.get("error") or result))
682
+ await self._append_jsonl(
683
+ self._startup_queue_done_path(),
684
+ {
685
+ "record_id": record_id,
686
+ "component_name": component_name,
687
+ "finished_at": time.time(),
688
+ "result": result if isinstance(result, dict) else {},
689
+ },
690
+ )
691
+ self._startup_queue_pending_count = max(0, self._startup_queue_pending_count - 1)
692
+ except Exception as exc:
693
+ await self._append_jsonl(
694
+ self._startup_queue_failed_path(),
695
+ {
696
+ "record_id": record_id,
697
+ "component_name": component_name,
698
+ "failed_at": time.time(),
699
+ "error": str(exc),
700
+ },
701
+ )
702
+ logger.warning(f"A_Memorix 启动队列回放失败: record={record_id}, error={exc}")
703
+
704
+ @staticmethod
705
+ def _config_model_to_runtime_dict(config_model: AMemorixConfig) -> Dict[str, Any]:
706
+ payload = config_model.model_dump(mode="json")
707
+ web_config = payload.get("web")
708
+ if isinstance(web_config, dict) and "import_config" in web_config:
709
+ web_config["import"] = web_config.pop("import_config")
710
+ payload = _to_builtin_data(payload) if isinstance(payload, dict) else {}
711
+ return _strip_internal_config_fields(payload) if isinstance(payload, dict) else {}
712
+
713
+ @staticmethod
714
+ def _runtime_dict_to_bot_config_dict(config: Dict[str, Any]) -> Dict[str, Any]:
715
+ payload = _to_builtin_data(config)
716
+ if not isinstance(payload, dict):
717
+ return {}
718
+ payload = _strip_internal_config_fields(payload)
719
+ web_config = payload.get("web")
720
+ if isinstance(web_config, dict) and "import_config" in web_config and "import" not in web_config:
721
+ web_config["import"] = web_config.pop("import_config")
722
+ return payload
723
+
724
+ @staticmethod
725
+ def _validate_bot_config_dict(config: Dict[str, Any]) -> None:
726
+ payload = _to_builtin_data(config)
727
+ if not isinstance(payload, dict):
728
+ raise TypeError("A_Memorix 配置必须是对象")
729
+ web_config = payload.get("web")
730
+ if isinstance(web_config, dict) and "import" in web_config and "import_config" not in web_config:
731
+ web_config["import_config"] = web_config.pop("import")
732
+ AMemorixConfig.model_validate(payload)
733
+
734
+ @staticmethod
735
+ def _replace_config_file(path: Path, content: bytes) -> None:
736
+ temp_path = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp")
737
+ target_mode = stat.S_IMODE(path.stat().st_mode) if path.exists() else 0o600
738
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0)
739
+ try:
740
+ with os.fdopen(os.open(temp_path, flags, 0o600), "wb") as handle:
741
+ handle.write(content)
742
+ handle.flush()
743
+ os.fsync(handle.fileno())
744
+ os.chmod(temp_path, target_mode)
745
+ os.replace(temp_path, path)
746
+ finally:
747
+ if temp_path.exists():
748
+ temp_path.unlink()
749
+
750
+ async def _write_config_to_bot_config(
751
+ self,
752
+ config: Dict[str, Any],
753
+ *,
754
+ replace: bool = False,
755
+ ) -> Tuple[Path, Optional[Path]]:
756
+ async with self._config_update_lock:
757
+ return await self._write_config_to_bot_config_locked(config, replace=replace)
758
+
759
+ async def _write_config_to_bot_config_locked(
760
+ self,
761
+ config: Dict[str, Any],
762
+ *,
763
+ replace: bool = False,
764
+ ) -> Tuple[Path, Optional[Path]]:
765
+ path = self.get_config_path()
766
+ path.parent.mkdir(parents=True, exist_ok=True)
767
+ original_content = path.read_bytes() if path.exists() else None
768
+ if original_content is not None:
769
+ doc = tomlkit.loads(original_content.decode("utf-8"))
770
+ else:
771
+ doc = tomlkit.document()
772
+
773
+ bot_config_payload = self._runtime_dict_to_bot_config_dict(config)
774
+ current = doc.get("a_memorix")
775
+ if replace:
776
+ doc["a_memorix"] = bot_config_payload
777
+ elif isinstance(current, dict):
778
+ _update_toml_doc(current, bot_config_payload)
779
+ else:
780
+ doc["a_memorix"] = bot_config_payload
781
+
782
+ candidate = _to_builtin_data(doc.get("a_memorix", {}))
783
+ if not isinstance(candidate, dict):
784
+ raise TypeError("A_Memorix 配置必须是对象")
785
+ self._validate_bot_config_dict(candidate)
786
+
787
+ candidate_content = tomlkit.dumps(doc).encode("utf-8")
788
+ backup_path = _backup_config_file(path)
789
+ self._replace_config_file(path, candidate_content)
790
+
791
+ config_manager = _get_config_manager()
792
+ try:
793
+ persisted_content = await self._reload_stable_config(path, config_manager)
794
+ except Exception as exc:
795
+ restored_original = await self._recover_after_failed_reload(
796
+ path,
797
+ original_content,
798
+ candidate_content,
799
+ config_manager,
800
+ )
801
+ if restored_original:
802
+ raise RuntimeError("A_Memorix 配置重载失败,已恢复写入前的配置") from exc
803
+ raise RuntimeError("A_Memorix 配置重载失败,检测到其他写入,已保留并重新加载当前配置") from exc
804
+
805
+ persisted = self._read_a_memorix_config(persisted_content)
806
+ persisted_matches = persisted == bot_config_payload if replace else (
807
+ isinstance(persisted, dict) and self._contains_config_patch(persisted, bot_config_payload)
808
+ )
809
+ if not persisted_matches:
810
+ raise RuntimeError("A_Memorix 配置在保存期间被其他写入覆盖,已保留并重新加载当前配置")
811
+ return path, backup_path
812
+
813
+ @staticmethod
814
+ def _read_file_content(path: Path) -> Optional[bytes]:
815
+ try:
816
+ return path.read_bytes()
817
+ except FileNotFoundError:
818
+ return None
819
+
820
+ @staticmethod
821
+ def _read_a_memorix_config(content: Optional[bytes]) -> Optional[Dict[str, Any]]:
822
+ if content is None:
823
+ return None
824
+ try:
825
+ payload = _to_builtin_data(tomlkit.loads(content.decode("utf-8")).get("a_memorix", {}))
826
+ except (UnicodeDecodeError, tomlkit.exceptions.ParseError):
827
+ return None
828
+ return payload if isinstance(payload, dict) else None
829
+
830
+ async def _reload_config_manager(self, config_manager: Any) -> bool:
831
+ current_task = asyncio.current_task()
832
+ self._config_reload_callback_suppressed_task = current_task
833
+ try:
834
+ return bool(await config_manager.reload_config(changed_scopes=("bot",)))
835
+ finally:
836
+ if self._config_reload_callback_suppressed_task is current_task:
837
+ self._config_reload_callback_suppressed_task = None
838
+
839
+ async def _reload_stable_config(self, path: Path, config_manager: Any) -> Optional[bytes]:
840
+ for _ in range(3):
841
+ content_before_reload = self._read_file_content(path)
842
+ if not await self._reload_config_manager(config_manager):
843
+ raise RuntimeError("配置管理器未能加载配置")
844
+ content_after_reload = self._read_file_content(path)
845
+ if content_after_reload != content_before_reload:
846
+ continue
847
+ await self.reload()
848
+ content_after_runtime_reload = self._read_file_content(path)
849
+ if content_after_runtime_reload == content_after_reload:
850
+ return content_after_runtime_reload
851
+ raise RuntimeError("配置在重载期间持续发生变化")
852
+
853
+ @staticmethod
854
+ def _contains_config_patch(actual: Dict[str, Any], expected: Dict[str, Any]) -> bool:
855
+ for key, expected_value in expected.items():
856
+ if key not in actual:
857
+ return False
858
+ actual_value = actual[key]
859
+ if isinstance(expected_value, dict):
860
+ if not isinstance(actual_value, dict) or not AMemorixHostService._contains_config_patch(
861
+ actual_value,
862
+ expected_value,
863
+ ):
864
+ return False
865
+ elif actual_value != expected_value:
866
+ return False
867
+ return True
868
+
869
+ async def _recover_after_failed_reload(
870
+ self,
871
+ path: Path,
872
+ original_content: Optional[bytes],
873
+ candidate_content: bytes,
874
+ config_manager: Any,
875
+ ) -> bool:
876
+ current_content = self._read_file_content(path)
877
+ restored_original = current_content == candidate_content
878
+ if restored_original:
879
+ if original_content is None:
880
+ path.unlink(missing_ok=True)
881
+ else:
882
+ self._replace_config_file(path, original_content)
883
+ try:
884
+ await self._reload_stable_config(path, config_manager)
885
+ except Exception as exc:
886
+ if restored_original:
887
+ raise RuntimeError("已恢复原配置,但配置管理器或 A_Memorix 运行时仍无法重载") from exc
888
+ raise RuntimeError("检测到其他配置写入,已保留当前文件,但无法完成运行时重载") from exc
889
+ return restored_original
890
+
891
+ def register_config_reload_callback(self) -> None:
892
+ if self._reload_callback_registered:
893
+ return
894
+ # 运行时仅在启动时固化 a_memorix 配置节,其余配置节变更无需重启
895
+ _get_config_manager().register_reload_callback(self.on_config_reload, sections=("a_memorix",))
896
+ self._reload_callback_registered = True
897
+
898
+ async def on_config_reload(self, changed_scopes: Sequence[str] | None = None) -> None:
899
+ normalized = {str(scope or "").strip().lower() for scope in (changed_scopes or [])}
900
+ if normalized and "bot" not in normalized:
901
+ return
902
+ current_task = asyncio.current_task()
903
+ if current_task is not None and current_task is self._config_reload_callback_suppressed_task:
904
+ return
905
+ await self.reload()
906
+
907
+ @staticmethod
908
+ def _disabled_response(component_name: str) -> Dict[str, Any]:
909
+ reason = "a_memorix_disabled"
910
+ message = "A_Memorix 未启用,请在长期记忆配置中开启后再使用。"
911
+
912
+ if component_name == "search_memory":
913
+ return {
914
+ "success": True,
915
+ "disabled": True,
916
+ "reason": reason,
917
+ "summary": "",
918
+ "hits": [],
919
+ "filtered": False,
920
+ }
921
+
922
+ if component_name in {"ingest_summary", "ingest_text"}:
923
+ return {
924
+ "success": True,
925
+ "disabled": True,
926
+ "reason": reason,
927
+ "stored_ids": [],
928
+ "skipped_ids": [reason],
929
+ "detail": reason,
930
+ }
931
+
932
+ if component_name == "get_person_profile":
933
+ return {
934
+ "success": True,
935
+ "disabled": True,
936
+ "reason": reason,
937
+ "summary": "",
938
+ "traits": [],
939
+ "evidence": [],
940
+ }
941
+
942
+ if component_name == "memory_stats":
943
+ return {
944
+ "success": True,
945
+ "enabled": False,
946
+ "memory_enabled": False,
947
+ "disabled": True,
948
+ "reason": reason,
949
+ "message": message,
950
+ "paragraph_count": 0,
951
+ "relation_count": 0,
952
+ "episode_count": 0,
953
+ }
954
+
955
+ if component_name == "memory_runtime_admin":
956
+ return {
957
+ "success": True,
958
+ "enabled": False,
959
+ "memory_enabled": False,
960
+ "disabled": True,
961
+ "reason": reason,
962
+ "message": message,
963
+ "runtime_ready": False,
964
+ "retrieval_ready": False,
965
+ "degraded": False,
966
+ "retrieval_mode": "disabled",
967
+ "available_channels": [],
968
+ "unavailable_channels": [],
969
+ "vector_health": {"state": "disabled"},
970
+ "embedding_degraded": False,
971
+ "embedding_dimension": 0,
972
+ "auto_save": False,
973
+ "data_dir": "",
974
+ }
975
+
976
+ if component_name == "enqueue_feedback_task":
977
+ return {
978
+ "success": True,
979
+ "queued": False,
980
+ "disabled": True,
981
+ "reason": reason,
982
+ }
983
+
984
+ return {
985
+ "success": False,
986
+ "enabled": False,
987
+ "disabled": True,
988
+ "reason": reason,
989
+ "error": message,
990
+ }
991
+
992
+ async def _shutdown_locked(self) -> None:
993
+ task = self._startup_task
994
+ if task is not None and not task.done():
995
+ task.cancel()
996
+ try:
997
+ await task
998
+ except asyncio.CancelledError:
999
+ pass
1000
+ self._startup_task = None
1001
+
1002
+ if self._kernel is None:
1003
+ self._runtime_state = "stopped"
1004
+ self._startup_finished_at = time.time()
1005
+ set_runtime_kernel(None)
1006
+ return
1007
+ shutdown = getattr(self._kernel, "shutdown", None)
1008
+ if callable(shutdown):
1009
+ await shutdown()
1010
+ else:
1011
+ self._kernel.close()
1012
+ self._kernel = None
1013
+ self._runtime_state = "stopped"
1014
+ self._startup_finished_at = time.time()
1015
+ set_runtime_kernel(None)
1016
+
1017
+
1018
+ a_memorix_host_service = AMemorixHostService()