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,1483 @@
1
+ """
2
+ 发送服务模块。
3
+
4
+ 统一封装内部模块的出站消息发送逻辑:
5
+
6
+ 1. 内部模块统一调用本模块。
7
+ 2. send service 只负责构造和预处理消息。
8
+ 3. 具体走插件链还是 legacy 旧链,由 Platform IO 内部统一决策。
9
+ """
10
+
11
+ from copy import deepcopy
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ import asyncio
15
+ import base64
16
+ import hashlib
17
+ import time
18
+ from datetime import datetime
19
+
20
+ from src.chat.message_receive.chat_manager import BotChatSession
21
+ from src.chat.message_receive.chat_manager import chat_manager as _chat_manager
22
+ from src.chat.message_receive.message import SessionMessage
23
+ from src.chat.utils.utils import calculate_typing_time, get_bot_account
24
+ from src.common.data_models.mai_message_data_model import GroupInfo, MaiMessage, MessageInfo, UserInfo
25
+ from src.common.data_models.message_component_data_model import (
26
+ AtComponent,
27
+ DictComponent,
28
+ EmojiComponent,
29
+ FileComponent,
30
+ ForwardNodeComponent,
31
+ ImageComponent,
32
+ MessageSequence,
33
+ ReplyComponent,
34
+ StandardMessageComponents,
35
+ TextComponent,
36
+ VoiceComponent,
37
+ )
38
+ from src.common.logger import get_logger
39
+ from src.common.utils.utils_message import MessageUtils
40
+ from src.config.config import global_config
41
+ from src.platform_io import DeliveryBatch, DriverKind, get_platform_io_manager
42
+ from src.platform_io.route_key_factory import RouteKeyFactory
43
+ from src.plugin_runtime.hook_payloads import deserialize_session_message, serialize_session_message
44
+ from src.plugin_runtime.hook_schema_utils import build_object_schema
45
+ from src.plugin_runtime.host.hook_dispatcher import HookDispatchResult
46
+ from src.plugin_runtime.host.hook_spec_registry import HookSpec, HookSpecRegistry
47
+
48
+ logger = get_logger("send_service")
49
+
50
+
51
+ def register_send_service_hook_specs(registry: HookSpecRegistry) -> List[HookSpec]:
52
+ """注册发送服务内置 Hook 规格。
53
+
54
+ Args:
55
+ registry: 目标 Hook 规格注册中心。
56
+
57
+ Returns:
58
+ List[HookSpec]: 实际注册的 Hook 规格列表。
59
+ """
60
+
61
+ return registry.register_hook_specs(
62
+ [
63
+ HookSpec(
64
+ name="send_service.after_build_message",
65
+ description="在出站 SessionMessage 构建完成后触发,可改写消息体或取消发送。",
66
+ parameters_schema=build_object_schema(
67
+ {
68
+ "message": {
69
+ "type": "object",
70
+ "description": "待发送消息的序列化 SessionMessage。",
71
+ },
72
+ "stream_id": {
73
+ "type": "string",
74
+ "description": "目标会话 ID。",
75
+ },
76
+ "processed_plain_text": {
77
+ "type": "string",
78
+ "description": "可选的预处理纯文本内容。",
79
+ },
80
+ "typing": {
81
+ "type": "boolean",
82
+ "description": "是否模拟打字。",
83
+ },
84
+ "set_reply": {
85
+ "type": "boolean",
86
+ "description": "是否附带引用回复。",
87
+ },
88
+ "storage_message": {
89
+ "type": "boolean",
90
+ "description": "发送成功后是否写库。",
91
+ },
92
+ "show_log": {
93
+ "type": "boolean",
94
+ "description": "是否输出发送日志。",
95
+ },
96
+ },
97
+ required=[
98
+ "message",
99
+ "stream_id",
100
+ "processed_plain_text",
101
+ "typing",
102
+ "set_reply",
103
+ "storage_message",
104
+ "show_log",
105
+ ],
106
+ ),
107
+ default_timeout_ms=5000,
108
+ allow_abort=True,
109
+ allow_kwargs_mutation=True,
110
+ ),
111
+ HookSpec(
112
+ name="send_service.before_send",
113
+ description="在真正调用 Platform IO 发送前触发,可改写消息或取消本次发送。",
114
+ parameters_schema=build_object_schema(
115
+ {
116
+ "message": {
117
+ "type": "object",
118
+ "description": "待发送消息的序列化 SessionMessage。",
119
+ },
120
+ "typing": {
121
+ "type": "boolean",
122
+ "description": "是否模拟打字。",
123
+ },
124
+ "set_reply": {
125
+ "type": "boolean",
126
+ "description": "是否附带引用回复。",
127
+ },
128
+ "reply_message_id": {
129
+ "type": "string",
130
+ "description": "被引用消息 ID。",
131
+ },
132
+ "storage_message": {
133
+ "type": "boolean",
134
+ "description": "发送成功后是否写库。",
135
+ },
136
+ "show_log": {
137
+ "type": "boolean",
138
+ "description": "是否输出发送日志。",
139
+ },
140
+ },
141
+ required=["message", "typing", "set_reply", "storage_message", "show_log"],
142
+ ),
143
+ default_timeout_ms=5000,
144
+ allow_abort=True,
145
+ allow_kwargs_mutation=True,
146
+ ),
147
+ HookSpec(
148
+ name="send_service.after_send",
149
+ description="在发送流程结束后触发,用于观察最终发送结果。",
150
+ parameters_schema=build_object_schema(
151
+ {
152
+ "message": {
153
+ "type": "object",
154
+ "description": "本次发送消息的序列化 SessionMessage。",
155
+ },
156
+ "sent": {
157
+ "type": "boolean",
158
+ "description": "本次发送是否成功。",
159
+ },
160
+ "typing": {
161
+ "type": "boolean",
162
+ "description": "是否模拟打字。",
163
+ },
164
+ "set_reply": {
165
+ "type": "boolean",
166
+ "description": "是否附带引用回复。",
167
+ },
168
+ "reply_message_id": {
169
+ "type": "string",
170
+ "description": "被引用消息 ID。",
171
+ },
172
+ "storage_message": {
173
+ "type": "boolean",
174
+ "description": "发送成功后是否写库。",
175
+ },
176
+ "show_log": {
177
+ "type": "boolean",
178
+ "description": "是否输出发送日志。",
179
+ },
180
+ },
181
+ required=["message", "sent", "typing", "set_reply", "storage_message", "show_log"],
182
+ ),
183
+ default_timeout_ms=5000,
184
+ allow_abort=False,
185
+ allow_kwargs_mutation=False,
186
+ ),
187
+ ]
188
+ )
189
+
190
+
191
+ def _get_runtime_manager() -> Any:
192
+ """获取插件运行时管理器。
193
+
194
+ Returns:
195
+ Any: 插件运行时管理器单例。
196
+ """
197
+
198
+ from src.plugin_runtime.integration import get_plugin_runtime_manager
199
+
200
+ return get_plugin_runtime_manager()
201
+
202
+
203
+ def _coerce_bool(value: Any, default: bool) -> bool:
204
+ """将任意值安全转换为布尔值。
205
+
206
+ Args:
207
+ value: 待转换的值。
208
+ default: 当值为空时使用的默认值。
209
+
210
+ Returns:
211
+ bool: 转换后的布尔值。
212
+ """
213
+
214
+ if value is None:
215
+ return default
216
+ return bool(value)
217
+
218
+
219
+ async def _invoke_send_hook(
220
+ hook_name: str,
221
+ message: SessionMessage,
222
+ **kwargs: Any,
223
+ ) -> tuple[HookDispatchResult, SessionMessage]:
224
+ """触发携带出站消息的命名 Hook。
225
+
226
+ Args:
227
+ hook_name: 目标 Hook 名称。
228
+ message: 当前待发送消息。
229
+ **kwargs: 需要附带的额外参数。
230
+
231
+ Returns:
232
+ tuple[HookDispatchResult, SessionMessage]: Hook 聚合结果以及可能被改写后的消息对象。
233
+ """
234
+
235
+ hook_result = await _get_runtime_manager().invoke_hook(
236
+ hook_name,
237
+ message=serialize_session_message(message),
238
+ **kwargs,
239
+ )
240
+ mutated_message = message
241
+ raw_message = hook_result.kwargs.get("message")
242
+ if raw_message is not None:
243
+ try:
244
+ mutated_message = deserialize_session_message(raw_message)
245
+ except Exception as exc:
246
+ logger.warning(f"Hook {hook_name} 返回的 message 无法反序列化,已忽略: {exc}")
247
+ return hook_result, mutated_message
248
+
249
+
250
+ def _inherit_platform_io_route_metadata(target_stream: BotChatSession) -> Dict[str, object]:
251
+ """从目标会话继承 Platform IO 路由元数据。
252
+
253
+ Args:
254
+ target_stream: 当前消息要发送到的会话对象。
255
+
256
+ Returns:
257
+ Dict[str, object]: 可安全透传到出站消息 ``additional_config`` 中的
258
+ 路由辅助字段。
259
+ """
260
+ inherited_metadata: Dict[str, object] = {}
261
+
262
+ context_message = target_stream.context.message if target_stream.context else None
263
+ if context_message is not None:
264
+ additional_config = context_message.message_info.additional_config
265
+ if isinstance(additional_config, dict):
266
+ for key in (*RouteKeyFactory.ACCOUNT_ID_KEYS, *RouteKeyFactory.SCOPE_KEYS):
267
+ value = additional_config.get(key)
268
+ if value is None:
269
+ continue
270
+ normalized_value = str(value).strip()
271
+ if normalized_value:
272
+ inherited_metadata[key] = value
273
+
274
+ # 账号和作用域通过出站消息正式字段继承,不再写入 additional_config。
275
+ # 暂时保留旧上下文元数据的透传以兼容尚未迁移的适配器,下个版本移除。
276
+
277
+ if target_stream.group_id and (normalized_group_id := str(target_stream.group_id).strip()):
278
+ inherited_metadata["platform_io_target_group_id"] = normalized_group_id
279
+
280
+ if target_stream.user_id and (normalized_user_id := str(target_stream.user_id).strip()):
281
+ inherited_metadata["platform_io_target_user_id"] = normalized_user_id
282
+
283
+ return inherited_metadata
284
+
285
+
286
+ def _build_binary_component_from_base64(component_type: str, raw_data: str) -> StandardMessageComponents:
287
+ """根据 Base64 数据构造二进制消息组件。
288
+
289
+ Args:
290
+ component_type: 组件类型名称。
291
+ raw_data: Base64 编码后的二进制数据。
292
+
293
+ Returns:
294
+ StandardMessageComponents: 转换后的内部消息组件。
295
+
296
+ Raises:
297
+ ValueError: 当组件类型不受支持时抛出。
298
+ """
299
+ binary_data = base64.b64decode(raw_data)
300
+ binary_hash = hashlib.sha256(binary_data).hexdigest()
301
+
302
+ if component_type == "image":
303
+ return ImageComponent(binary_hash=binary_hash, binary_data=binary_data)
304
+ if component_type == "emoji":
305
+ return EmojiComponent(binary_hash=binary_hash, binary_data=binary_data)
306
+ if component_type == "voice":
307
+ return VoiceComponent(binary_hash=binary_hash, binary_data=binary_data)
308
+ raise ValueError(f"不支持的二进制组件类型: {component_type}")
309
+
310
+
311
+ def _build_message_sequence_from_custom_message(
312
+ message_type: str,
313
+ content: str | Dict[str, Any],
314
+ ) -> MessageSequence:
315
+ """根据自定义消息类型构造内部消息组件序列。
316
+
317
+ Args:
318
+ message_type: 自定义消息类型。
319
+ content: 自定义消息内容。
320
+
321
+ Returns:
322
+ MessageSequence: 转换后的消息组件序列。
323
+ """
324
+ normalized_type = message_type.strip().lower()
325
+
326
+ if normalized_type == "text":
327
+ return MessageSequence(components=[TextComponent(text=str(content))])
328
+
329
+ if normalized_type in {"image", "emoji", "voice"}:
330
+ return MessageSequence(components=[_build_binary_component_from_base64(normalized_type, str(content))])
331
+
332
+ if normalized_type == "at":
333
+ return MessageSequence(components=[AtComponent(target_user_id=str(content))])
334
+
335
+ if normalized_type == "reply":
336
+ return MessageSequence(components=[ReplyComponent(target_message_id=str(content))])
337
+
338
+ if normalized_type == "file" and isinstance(content, dict):
339
+ return MessageSequence(components=[FileComponent.from_payload(deepcopy(content))])
340
+
341
+ if normalized_type == "dict" and isinstance(content, dict):
342
+ return MessageSequence(components=[DictComponent(data=deepcopy(content))])
343
+
344
+ return MessageSequence(
345
+ components=[
346
+ DictComponent(
347
+ data={
348
+ "type": normalized_type,
349
+ "data": deepcopy(content),
350
+ }
351
+ )
352
+ ]
353
+ )
354
+
355
+
356
+ def _clone_message_sequence(message_sequence: MessageSequence) -> MessageSequence:
357
+ """复制消息组件序列,避免原对象被发送流程修改。
358
+
359
+ Args:
360
+ message_sequence: 原始消息组件序列。
361
+
362
+ Returns:
363
+ MessageSequence: 深拷贝后的消息组件序列。
364
+ """
365
+ return deepcopy(message_sequence)
366
+
367
+
368
+ def _detect_outbound_message_flags(message_sequence: MessageSequence) -> Dict[str, bool]:
369
+ """根据消息组件序列推断出站消息标记。
370
+
371
+ Args:
372
+ message_sequence: 待发送的消息组件序列。
373
+
374
+ Returns:
375
+ Dict[str, bool]: 包含 ``is_emoji``、``is_picture``、``is_command`` 的标记字典。
376
+ """
377
+ if len(message_sequence.components) != 1:
378
+ return {
379
+ "is_emoji": False,
380
+ "is_picture": False,
381
+ "is_command": False,
382
+ }
383
+
384
+ component = message_sequence.components[0]
385
+ is_command = False
386
+ if isinstance(component, DictComponent) and isinstance(component.data, dict):
387
+ is_command = str(component.data.get("type") or "").strip().lower() == "command"
388
+
389
+ return {
390
+ "is_emoji": isinstance(component, EmojiComponent),
391
+ "is_picture": isinstance(component, ImageComponent),
392
+ "is_command": is_command,
393
+ }
394
+
395
+
396
+ def _describe_message_sequence(message_sequence: MessageSequence) -> str:
397
+ """生成消息组件序列的简短描述文本。
398
+
399
+ Args:
400
+ message_sequence: 待描述的消息组件序列。
401
+
402
+ Returns:
403
+ str: 适用于日志的简短类型描述。
404
+ """
405
+ if len(message_sequence.components) != 1:
406
+ return "message_sequence"
407
+
408
+ component = message_sequence.components[0]
409
+ if isinstance(component, DictComponent) and isinstance(component.data, dict):
410
+ custom_type = str(component.data.get("type") or "").strip()
411
+ return custom_type or "dict"
412
+
413
+ if isinstance(component, TextComponent):
414
+ return component.format_name
415
+
416
+ if isinstance(component, ImageComponent):
417
+ return component.format_name
418
+
419
+ if isinstance(component, EmojiComponent):
420
+ return component.format_name
421
+
422
+ if isinstance(component, VoiceComponent):
423
+ return component.format_name
424
+
425
+ if isinstance(component, FileComponent):
426
+ return component.format_name
427
+
428
+ if isinstance(component, AtComponent):
429
+ return component.format_name
430
+
431
+ if isinstance(component, ReplyComponent):
432
+ return component.format_name
433
+
434
+ if isinstance(component, ForwardNodeComponent):
435
+ return component.format_name
436
+
437
+ return "unknown"
438
+
439
+
440
+ def _build_processed_plain_text(message: SessionMessage) -> str:
441
+ """为出站消息构造轻量纯文本摘要。
442
+
443
+ Args:
444
+ message: 待发送的内部消息对象。
445
+
446
+ Returns:
447
+ str: 适用于日志与打字时长估算的纯文本摘要。
448
+ """
449
+ processed_parts: List[str] = []
450
+ for component in message.raw_message.components:
451
+ if isinstance(component, TextComponent):
452
+ processed_parts.append(component.text)
453
+ continue
454
+
455
+ if isinstance(component, ImageComponent):
456
+ processed_parts.append(component.content.strip() or "[图片]")
457
+ continue
458
+
459
+ if isinstance(component, EmojiComponent):
460
+ processed_parts.append(component.content.strip() or "[表情]")
461
+ continue
462
+
463
+ if isinstance(component, VoiceComponent):
464
+ processed_parts.append(component.content.strip() or "[语音]")
465
+ continue
466
+
467
+ if isinstance(component, FileComponent):
468
+ processed_parts.append(component.to_plain_text())
469
+ continue
470
+
471
+ if isinstance(component, AtComponent):
472
+ at_target = component.target_user_cardname or component.target_user_nickname or component.target_user_id
473
+ processed_parts.append(f"@{at_target}")
474
+ continue
475
+
476
+ if isinstance(component, ReplyComponent):
477
+ processed_parts.append(component.target_message_content or "[回复消息]")
478
+ continue
479
+
480
+ if isinstance(component, DictComponent):
481
+ raw_type = component.data.get("type") if isinstance(component.data, dict) else None
482
+ if isinstance(raw_type, str) and raw_type.strip():
483
+ processed_parts.append(f"[{raw_type.strip()}消息]")
484
+ else:
485
+ processed_parts.append("[自定义消息]")
486
+ continue
487
+
488
+ return " ".join(part for part in processed_parts if part)
489
+
490
+
491
+ def _build_outbound_log_preview(message: SessionMessage, max_length: int = 160) -> str:
492
+ """构造出站消息的日志预览文本。
493
+
494
+ Args:
495
+ message: 待发送的内部消息对象。
496
+ max_length: 预览文本最大长度。
497
+
498
+ Returns:
499
+ str: 适用于日志展示的消息摘要。
500
+ """
501
+ preview_text = (message.processed_plain_text or "").strip()
502
+ if not preview_text:
503
+ preview_text = f"[{_describe_message_sequence(message.raw_message)}]"
504
+
505
+ normalized_preview = " ".join(preview_text.split())
506
+ if len(normalized_preview) <= max_length:
507
+ return normalized_preview
508
+ return f"{normalized_preview[:max_length]}..."
509
+
510
+
511
+ def _build_outbound_session_message(
512
+ message_sequence: MessageSequence,
513
+ stream_id: str,
514
+ processed_plain_text: str = "",
515
+ reply_message: Optional[MaiMessage] = None,
516
+ selected_expressions: Optional[List[int]] = None,
517
+ ) -> Optional[SessionMessage]:
518
+ """根据目标会话构建待发送的内部消息对象。
519
+
520
+ Args:
521
+ message_sequence: 待发送的消息组件序列。
522
+ stream_id: 目标会话 ID。
523
+ processed_plain_text: 可选的预处理纯文本内容。
524
+ reply_message: 被回复的锚点消息。
525
+ selected_expressions: 可选的表情候选索引列表。
526
+
527
+ Returns:
528
+ Optional[SessionMessage]: 构建成功时返回内部消息对象;若目标会话或
529
+ 机器人账号不存在,则返回 ``None``。
530
+ """
531
+ target_stream = _chat_manager.get_session_by_session_id(stream_id)
532
+ if target_stream is None:
533
+ logger.error(f"[SendService] 未找到聊天流: {stream_id}")
534
+ return None
535
+
536
+ bot_user_id = get_bot_account(target_stream.platform, target_stream.account_id)
537
+ if not bot_user_id:
538
+ logger.error(f"[SendService] 平台 {target_stream.platform} 未配置机器人账号,无法发送消息")
539
+ return None
540
+
541
+ current_time = time.time()
542
+ message_id = f"send_api_{int(current_time * 1000)}"
543
+ anchor_message = reply_message.deepcopy() if reply_message is not None else None
544
+
545
+ group_info: Optional[GroupInfo] = None
546
+ if target_stream.group_id:
547
+ group_name = str(target_stream.group_name or "").strip()
548
+ if (
549
+ not group_name
550
+ and target_stream.context
551
+ and target_stream.context.message
552
+ and target_stream.context.message.message_info.group_info
553
+ ):
554
+ group_name = target_stream.context.message.message_info.group_info.group_name
555
+ if group_name:
556
+ group_info = GroupInfo(
557
+ group_id=target_stream.group_id,
558
+ group_name=group_name,
559
+ )
560
+
561
+ additional_config: Dict[str, object] = _inherit_platform_io_route_metadata(target_stream)
562
+ if selected_expressions is not None:
563
+ additional_config["selected_expressions"] = selected_expressions
564
+
565
+ outbound_message = SessionMessage(
566
+ message_id=message_id,
567
+ timestamp=datetime.fromtimestamp(current_time),
568
+ platform=target_stream.platform,
569
+ )
570
+ outbound_message.account_id = bot_user_id
571
+ outbound_message.scope = target_stream.scope
572
+ outbound_message.message_info = MessageInfo(
573
+ user_info=UserInfo(
574
+ user_id=bot_user_id,
575
+ user_nickname=global_config.bot.nickname,
576
+ ),
577
+ group_info=group_info,
578
+ additional_config=additional_config,
579
+ )
580
+ outbound_message.raw_message = _clone_message_sequence(message_sequence)
581
+ outbound_message.session_id = target_stream.session_id
582
+ outbound_message.processed_plain_text = processed_plain_text.strip() or _build_processed_plain_text(
583
+ outbound_message
584
+ )
585
+ outbound_message.reply_to = anchor_message.message_id if anchor_message is not None else None
586
+ message_flags = _detect_outbound_message_flags(outbound_message.raw_message)
587
+ outbound_message.is_emoji = message_flags["is_emoji"]
588
+ outbound_message.is_picture = message_flags["is_picture"]
589
+ outbound_message.is_command = message_flags["is_command"]
590
+ outbound_message.initialized = True
591
+ return outbound_message
592
+
593
+
594
+ def _build_reply_component(reply_message_id: str, reply_message: Optional[MaiMessage]) -> ReplyComponent:
595
+ """根据被引用消息构造包含预览信息的回复组件。"""
596
+ if reply_message is None or reply_message.message_id != reply_message_id:
597
+ return ReplyComponent(target_message_id=reply_message_id)
598
+
599
+ user_info = reply_message.message_info.user_info
600
+ return ReplyComponent(
601
+ target_message_id=reply_message_id,
602
+ target_message_content=reply_message.processed_plain_text or "",
603
+ target_message_sender_id=user_info.user_id,
604
+ target_message_sender_nickname=user_info.user_nickname,
605
+ target_message_sender_cardname=user_info.user_cardname,
606
+ )
607
+
608
+
609
+ def _fill_reply_component(component: ReplyComponent, reply_message: Optional[MaiMessage]) -> None:
610
+ """补齐已存在回复组件中的目标消息预览信息。"""
611
+ if reply_message is None or reply_message.message_id != component.target_message_id:
612
+ return
613
+
614
+ user_info = reply_message.message_info.user_info
615
+ if not component.target_message_content:
616
+ component.target_message_content = reply_message.processed_plain_text or ""
617
+ if not component.target_message_sender_id:
618
+ component.target_message_sender_id = user_info.user_id
619
+ if not component.target_message_sender_nickname:
620
+ component.target_message_sender_nickname = user_info.user_nickname
621
+ if not component.target_message_sender_cardname:
622
+ component.target_message_sender_cardname = user_info.user_cardname
623
+
624
+
625
+ def _ensure_reply_component(
626
+ message: SessionMessage,
627
+ reply_message_id: str,
628
+ reply_message: Optional[MaiMessage] = None,
629
+ ) -> None:
630
+ """为消息补充回复组件。
631
+
632
+ Args:
633
+ message: 待发送的内部消息对象。
634
+ reply_message_id: 被引用消息的 ID。
635
+ """
636
+ if message.raw_message.components:
637
+ first_component = message.raw_message.components[0]
638
+ if isinstance(first_component, ReplyComponent) and first_component.target_message_id == reply_message_id:
639
+ _fill_reply_component(first_component, reply_message)
640
+ return
641
+
642
+ message.raw_message.components.insert(0, _build_reply_component(reply_message_id, reply_message))
643
+
644
+
645
+ async def _prepare_message_for_platform_io(
646
+ message: SessionMessage,
647
+ *,
648
+ typing: bool,
649
+ set_reply: bool,
650
+ reply_message_id: Optional[str],
651
+ reply_message: Optional[MaiMessage] = None,
652
+ ) -> None:
653
+ """为 Platform IO 发送链预处理消息。
654
+
655
+ Args:
656
+ message: 待发送的内部消息对象。
657
+ typing: 是否模拟打字等待。
658
+ set_reply: 是否构建引用回复组件。
659
+ reply_message_id: 被引用消息的 ID。
660
+
661
+ Raises:
662
+ ValueError: 当要求设置引用回复但缺少 ``reply_message_id`` 时抛出。
663
+ """
664
+ if set_reply:
665
+ if not reply_message_id:
666
+ raise ValueError("set_reply=True 时必须提供 reply_message_id")
667
+ _ensure_reply_component(message, reply_message_id, reply_message)
668
+
669
+ if set_reply or not message.processed_plain_text:
670
+ message.processed_plain_text = _build_processed_plain_text(message)
671
+ if typing:
672
+ # 纯图片分段与表情包一样使用固定等待,避免按图片描述长度模拟打字。
673
+ components = message.raw_message.components
674
+ is_image = any(isinstance(component, ImageComponent) for component in components) and not any(
675
+ isinstance(component, TextComponent) and component.text.strip() for component in components
676
+ )
677
+ typing_time = calculate_typing_time(
678
+ input_string=message.processed_plain_text or "",
679
+ is_emoji=message.is_emoji,
680
+ is_image=is_image,
681
+ )
682
+ await asyncio.sleep(typing_time)
683
+
684
+
685
+ async def _store_sent_message(message: SessionMessage) -> None:
686
+ """将已成功发送的消息写入数据库。
687
+
688
+ Args:
689
+ message: 已成功发送的内部消息对象。
690
+ """
691
+ await MessageUtils.store_message_to_db_async(message)
692
+
693
+
694
+ async def _apply_successful_delivery_receipt(message: SessionMessage, delivery_batch: DeliveryBatch) -> None:
695
+ """将成功回执中的平台消息 ID 回填到内部消息。
696
+
697
+ Args:
698
+ message: 已发送成功的内部消息对象。
699
+ delivery_batch: Platform IO 返回的批量回执。
700
+ """
701
+ message.platform_message_id = None
702
+ if not delivery_batch.sent_receipts:
703
+ return
704
+
705
+ original_message_id = str(message.message_id or "").strip()
706
+ external_message_id = str(delivery_batch.sent_receipts[0].external_message_id or "").strip()
707
+ if not external_message_id:
708
+ return
709
+
710
+ message.platform_message_id = external_message_id
711
+ if external_message_id == original_message_id:
712
+ return
713
+
714
+ message.message_id = external_message_id
715
+
716
+
717
+ async def _dispatch_adapter_callbacks(delivery_batch: DeliveryBatch) -> None:
718
+ """分发适配器随成功回执返回的自定义回调。
719
+
720
+ Args:
721
+ delivery_batch: Platform IO 返回的批量回执。
722
+ """
723
+ try:
724
+ from src.common.message_server import api as message_server_api
725
+
726
+ global_api = getattr(message_server_api, "global_api", None)
727
+ custom_handlers = getattr(global_api, "_custom_message_handlers", None)
728
+ if not isinstance(custom_handlers, dict):
729
+ return
730
+
731
+ for receipt in delivery_batch.sent_receipts:
732
+ raw_callbacks = receipt.metadata.get("adapter_callbacks")
733
+ if not isinstance(raw_callbacks, list):
734
+ continue
735
+
736
+ for raw_callback in raw_callbacks:
737
+ if not isinstance(raw_callback, dict):
738
+ continue
739
+
740
+ callback_name = str(raw_callback.get("name") or "").strip()
741
+ payload = raw_callback.get("payload")
742
+ if not callback_name or not isinstance(payload, dict):
743
+ continue
744
+
745
+ handler = custom_handlers.get(callback_name)
746
+ if handler is None:
747
+ continue
748
+
749
+ await handler(payload)
750
+ except Exception as exc:
751
+ logger.warning(f"[SendService] 分发适配器回调失败: {exc}")
752
+
753
+
754
+ async def _notify_memory_automation_on_message_sent(message: SessionMessage) -> None:
755
+ """在发送成功后通知长期记忆自动化服务。
756
+
757
+ Args:
758
+ message: 已成功发送的内部消息对象。
759
+ """
760
+ try:
761
+ from src.services.memory_flow_service import memory_automation_service
762
+
763
+ await memory_automation_service.on_message_sent(message)
764
+ except Exception as exc:
765
+ session_id = message.session_id or "unknown-session"
766
+ logger.warning(f"[{session_id}] 长期记忆人物事实写回注册失败: {exc}")
767
+
768
+
769
+ def _record_sent_emoji_usage(message: SessionMessage) -> None:
770
+ """在消息真实发送成功后记录其中表情包的使用次数。"""
771
+
772
+ emoji_hashes = [
773
+ component.binary_hash.strip()
774
+ for component in message.raw_message.components
775
+ if isinstance(component, EmojiComponent) and component.binary_hash.strip()
776
+ ]
777
+ if not emoji_hashes:
778
+ return
779
+
780
+ try:
781
+ from src.emoji_system.emoji_manager import emoji_manager
782
+
783
+ for emoji_hash in emoji_hashes:
784
+ emoji_manager.update_emoji_usage_by_hash(emoji_hash, log_missing=False)
785
+ except Exception as exc:
786
+ logger.warning(f"[SendService] 记录表情包使用次数失败: {exc}")
787
+
788
+
789
+ def _sync_sent_message_to_maisaka_history(
790
+ message: SessionMessage,
791
+ *,
792
+ source_kind: str,
793
+ ) -> None:
794
+ """将已发送成功的消息同步到当前会话对应的 Maisaka 历史。"""
795
+
796
+ session_id = str(message.session_id or "").strip()
797
+ if not session_id:
798
+ return
799
+
800
+ try:
801
+ from src.chat.heart_flow.heartflow_manager import heartflow_manager
802
+
803
+ runtime = heartflow_manager.heartflow_chat_list.get(session_id)
804
+ if runtime is None:
805
+ return
806
+ runtime.append_sent_message_to_chat_history(message, source_kind=source_kind)
807
+ except Exception as exc:
808
+ logger.warning(f"[SendService] 同步消息到 Maisaka 历史失败: session_id={session_id} error={exc}")
809
+
810
+
811
+ def _log_platform_io_failures(delivery_batch: DeliveryBatch) -> None:
812
+ """输出 Platform IO 批量发送失败详情。
813
+
814
+ Args:
815
+ delivery_batch: Platform IO 返回的批量回执。
816
+ """
817
+ failed_details = (
818
+ "; ".join(
819
+ f"driver={receipt.driver_id} status={receipt.status} error={receipt.error}"
820
+ for receipt in delivery_batch.failed_receipts
821
+ )
822
+ or "未命中任何发送路由"
823
+ )
824
+ logger.warning(f"[SendService] Platform IO 发送失败: platform={delivery_batch.route_key.platform} {failed_details}")
825
+
826
+
827
+ async def _send_via_platform_io(
828
+ message: SessionMessage,
829
+ *,
830
+ typing: bool,
831
+ set_reply: bool,
832
+ reply_message_id: Optional[str],
833
+ reply_message: Optional[MaiMessage] = None,
834
+ storage_message: bool,
835
+ show_log: bool,
836
+ ) -> Optional[SessionMessage]:
837
+ """通过 Platform IO 发送消息。
838
+
839
+ Args:
840
+ message: 待发送的内部消息对象。
841
+ typing: 是否模拟打字等待。
842
+ set_reply: 是否设置引用回复。
843
+ reply_message_id: 被引用消息的 ID。
844
+ storage_message: 发送成功后是否写入数据库。
845
+ show_log: 是否输出发送成功日志。
846
+
847
+ Returns:
848
+ bool: 发送成功时返回 ``True``。
849
+ """
850
+ before_send_result, message = await _invoke_send_hook(
851
+ "send_service.before_send",
852
+ message,
853
+ typing=typing,
854
+ set_reply=set_reply,
855
+ reply_message_id=reply_message_id,
856
+ storage_message=storage_message,
857
+ show_log=show_log,
858
+ )
859
+ if before_send_result.aborted:
860
+ logger.info(f"[SendService] 消息 {message.message_id} 在发送前被 Hook 中止")
861
+ return None
862
+
863
+ before_kwargs = before_send_result.kwargs
864
+ typing = _coerce_bool(before_kwargs.get("typing"), typing)
865
+ set_reply = _coerce_bool(before_kwargs.get("set_reply"), set_reply)
866
+ storage_message = _coerce_bool(before_kwargs.get("storage_message"), storage_message)
867
+ show_log = _coerce_bool(before_kwargs.get("show_log"), show_log)
868
+ raw_reply_message_id = before_kwargs.get("reply_message_id", reply_message_id)
869
+ reply_message_id = None if raw_reply_message_id in {None, ""} else str(raw_reply_message_id)
870
+
871
+ platform_io_manager = get_platform_io_manager()
872
+ try:
873
+ await platform_io_manager.ensure_send_pipeline_ready()
874
+ except Exception as exc:
875
+ logger.error(f"[SendService] 准备 Platform IO 发送管线失败: {exc}", exc_info=True)
876
+ return None
877
+
878
+ try:
879
+ route_key = platform_io_manager.build_route_key_from_message(message)
880
+ except Exception as exc:
881
+ logger.warning(f"[SendService] 根据消息构造 Platform IO 路由键失败: {exc}")
882
+ return None
883
+
884
+ try:
885
+ await _prepare_message_for_platform_io(
886
+ message,
887
+ typing=typing,
888
+ set_reply=set_reply,
889
+ reply_message_id=reply_message_id,
890
+ reply_message=reply_message,
891
+ )
892
+ delivery_batch = await platform_io_manager.send_message(
893
+ message,
894
+ route_key,
895
+ metadata={"show_log": False},
896
+ )
897
+ except Exception as exc:
898
+ logger.error(f"[SendService] Platform IO 发送异常: {exc}", exc_info=True)
899
+ return None
900
+
901
+ sent = bool(delivery_batch.has_success)
902
+ if sent:
903
+ await _apply_successful_delivery_receipt(message, delivery_batch)
904
+ await _dispatch_adapter_callbacks(delivery_batch)
905
+ await _invoke_send_hook(
906
+ "send_service.after_send",
907
+ message,
908
+ sent=sent,
909
+ typing=typing,
910
+ set_reply=set_reply,
911
+ reply_message_id=reply_message_id,
912
+ storage_message=storage_message,
913
+ show_log=show_log,
914
+ )
915
+
916
+ if delivery_batch.has_success:
917
+ _record_sent_emoji_usage(message)
918
+ if storage_message:
919
+ await _store_sent_message(message)
920
+ await _notify_memory_automation_on_message_sent(message)
921
+ should_log_delivery = show_log and any(
922
+ receipt.driver_kind != DriverKind.LOCAL
923
+ for receipt in delivery_batch.sent_receipts
924
+ )
925
+ if should_log_delivery:
926
+ successful_driver_ids = [receipt.driver_id or "unknown" for receipt in delivery_batch.sent_receipts]
927
+ logger.debug(
928
+ f"投递成功 平台={route_key.platform} 驱动={', '.join(successful_driver_ids)}"
929
+ )
930
+ # 网关驱动 ID 形如 gateway:插件ID:网关名,INFO 只展示网关名。
931
+ driver_names = ",".join(
932
+ driver_id.rsplit(":", 1)[-1] if driver_id.startswith("gateway:") else driver_id
933
+ for driver_id in successful_driver_ids
934
+ )
935
+ group_info = message.message_info.group_info
936
+ user_info = message.message_info.user_info
937
+ session_name = _chat_manager.get_session_name(message.session_id)
938
+ if not session_name:
939
+ session_name = (
940
+ group_info.group_name or group_info.group_id
941
+ if group_info is not None
942
+ else f"{user_info.user_nickname or user_info.user_id}的私聊"
943
+ )
944
+ logger.info(
945
+ f"[{route_key.account_id or ''}:{route_key.platform}:{driver_names}] "
946
+ f"[{session_name}] {global_config.bot.nickname}: {_build_outbound_log_preview(message)}"
947
+ )
948
+ return message
949
+
950
+ _log_platform_io_failures(delivery_batch)
951
+ return None
952
+
953
+
954
+ async def send_session_message_with_message(
955
+ message: SessionMessage,
956
+ *,
957
+ typing: bool = False,
958
+ set_reply: bool = False,
959
+ reply_message_id: Optional[str] = None,
960
+ reply_message: Optional[MaiMessage] = None,
961
+ storage_message: bool = True,
962
+ show_log: bool = True,
963
+ sync_to_maisaka_history: bool = False,
964
+ maisaka_source_kind: str = "outbound_send",
965
+ ) -> Optional[SessionMessage]:
966
+ """统一发送一条内部消息,并返回最终发送成功的消息对象。"""
967
+ if not message.message_id:
968
+ logger.error("[SendService] 消息缺少 message_id,无法发送")
969
+ raise ValueError("消息缺少 message_id,无法发送")
970
+
971
+ sent_message = await _send_via_platform_io(
972
+ message,
973
+ typing=typing,
974
+ set_reply=set_reply,
975
+ reply_message_id=reply_message_id,
976
+ reply_message=reply_message,
977
+ storage_message=storage_message,
978
+ show_log=show_log,
979
+ )
980
+ if sent_message is not None and sync_to_maisaka_history:
981
+ _sync_sent_message_to_maisaka_history(
982
+ sent_message,
983
+ source_kind=str(maisaka_source_kind or "outbound_send"),
984
+ )
985
+ return sent_message
986
+
987
+
988
+ async def send_session_message(
989
+ message: SessionMessage,
990
+ *,
991
+ typing: bool = False,
992
+ set_reply: bool = False,
993
+ reply_message_id: Optional[str] = None,
994
+ reply_message: Optional[MaiMessage] = None,
995
+ storage_message: bool = True,
996
+ show_log: bool = True,
997
+ sync_to_maisaka_history: bool = False,
998
+ maisaka_source_kind: str = "outbound_send",
999
+ ) -> bool:
1000
+ """统一发送一条内部消息。
1001
+
1002
+ 该方法是内部模块的统一发送入口:
1003
+
1004
+ 1. 构造并维护内部消息对象。
1005
+ 2. 由 Platform IO 统一决定走插件链还是 legacy 旧链。
1006
+ 3. send service 不再自行判断底层发送路径。
1007
+
1008
+ Args:
1009
+ message: 待发送的内部消息对象。
1010
+ typing: 是否模拟打字等待。
1011
+ set_reply: 是否设置引用回复。
1012
+ reply_message_id: 被引用消息的 ID。
1013
+ storage_message: 发送成功后是否写入数据库。
1014
+ show_log: 是否输出发送日志。
1015
+
1016
+ Returns:
1017
+ bool: 发送成功时返回 ``True``,否则返回 ``False``。
1018
+ """
1019
+ if not message.message_id:
1020
+ logger.error("[SendService] 消息缺少 message_id,无法发送")
1021
+ raise ValueError("消息缺少 message_id,无法发送")
1022
+
1023
+ return (
1024
+ await send_session_message_with_message(
1025
+ message,
1026
+ typing=typing,
1027
+ set_reply=set_reply,
1028
+ reply_message_id=reply_message_id,
1029
+ reply_message=reply_message,
1030
+ storage_message=storage_message,
1031
+ show_log=show_log,
1032
+ sync_to_maisaka_history=sync_to_maisaka_history,
1033
+ maisaka_source_kind=maisaka_source_kind,
1034
+ )
1035
+ is not None
1036
+ )
1037
+
1038
+
1039
+ async def _send_to_target(
1040
+ message_sequence: MessageSequence,
1041
+ stream_id: str,
1042
+ processed_plain_text: str = "",
1043
+ typing: bool = False,
1044
+ set_reply: bool = False,
1045
+ reply_message: Optional[MaiMessage] = None,
1046
+ storage_message: bool = True,
1047
+ show_log: bool = True,
1048
+ selected_expressions: Optional[List[int]] = None,
1049
+ sync_to_maisaka_history: bool = False,
1050
+ maisaka_source_kind: str = "outbound_send",
1051
+ ) -> bool:
1052
+ """向指定目标构建并发送消息,并返回是否发送成功。"""
1053
+ return (
1054
+ await _send_to_target_with_message(
1055
+ message_sequence=message_sequence,
1056
+ stream_id=stream_id,
1057
+ processed_plain_text=processed_plain_text,
1058
+ typing=typing,
1059
+ set_reply=set_reply,
1060
+ reply_message=reply_message,
1061
+ storage_message=storage_message,
1062
+ show_log=show_log,
1063
+ selected_expressions=selected_expressions,
1064
+ sync_to_maisaka_history=sync_to_maisaka_history,
1065
+ maisaka_source_kind=maisaka_source_kind,
1066
+ )
1067
+ is not None
1068
+ )
1069
+
1070
+
1071
+ async def _send_to_target_with_message(
1072
+ message_sequence: MessageSequence,
1073
+ stream_id: str,
1074
+ processed_plain_text: str = "",
1075
+ typing: bool = False,
1076
+ set_reply: bool = False,
1077
+ reply_message: Optional[MaiMessage] = None,
1078
+ storage_message: bool = True,
1079
+ show_log: bool = True,
1080
+ selected_expressions: Optional[List[int]] = None,
1081
+ sync_to_maisaka_history: bool = False,
1082
+ maisaka_source_kind: str = "outbound_send",
1083
+ ) -> Optional[SessionMessage]:
1084
+ """向指定目标构建并发送消息。
1085
+
1086
+ Args:
1087
+ message_sequence: 待发送的消息组件序列。
1088
+ stream_id: 目标会话 ID。
1089
+ processed_plain_text: 可选的预处理纯文本内容。
1090
+ typing: 是否显示输入中状态。
1091
+ set_reply: 是否在发送时附带引用回复。
1092
+ reply_message: 被回复的消息对象。
1093
+ storage_message: 是否将发送结果写入消息存储。
1094
+ show_log: 是否输出发送日志。
1095
+ selected_expressions: 可选的表情候选索引列表。
1096
+
1097
+ Returns:
1098
+ bool: 发送成功返回 ``True``,否则返回 ``False``。
1099
+ """
1100
+ try:
1101
+ if set_reply and reply_message is None:
1102
+ logger.warning("[SendService] 使用引用回复,但未提供回复消息")
1103
+ return None
1104
+
1105
+ if show_log:
1106
+ logger.debug(f"[SendService] 发送{_describe_message_sequence(message_sequence)}消息到 {stream_id}")
1107
+
1108
+ outbound_message = _build_outbound_session_message(
1109
+ message_sequence=message_sequence,
1110
+ stream_id=stream_id,
1111
+ processed_plain_text=processed_plain_text,
1112
+ reply_message=reply_message,
1113
+ selected_expressions=selected_expressions,
1114
+ )
1115
+ if outbound_message is None:
1116
+ return None
1117
+
1118
+ after_build_result, outbound_message = await _invoke_send_hook(
1119
+ "send_service.after_build_message",
1120
+ outbound_message,
1121
+ stream_id=stream_id,
1122
+ processed_plain_text=processed_plain_text,
1123
+ typing=typing,
1124
+ set_reply=set_reply,
1125
+ storage_message=storage_message,
1126
+ show_log=show_log,
1127
+ )
1128
+ if after_build_result.aborted:
1129
+ logger.info(f"[SendService] 消息 {outbound_message.message_id} 在构建后被 Hook 中止")
1130
+ return None
1131
+
1132
+ after_build_kwargs = after_build_result.kwargs
1133
+ typing = _coerce_bool(after_build_kwargs.get("typing"), typing)
1134
+ set_reply = _coerce_bool(after_build_kwargs.get("set_reply"), set_reply)
1135
+ storage_message = _coerce_bool(after_build_kwargs.get("storage_message"), storage_message)
1136
+ show_log = _coerce_bool(after_build_kwargs.get("show_log"), show_log)
1137
+
1138
+ sent_message = await send_session_message_with_message(
1139
+ outbound_message,
1140
+ typing=typing,
1141
+ set_reply=set_reply,
1142
+ reply_message_id=reply_message.message_id if reply_message is not None else None,
1143
+ reply_message=reply_message,
1144
+ storage_message=storage_message,
1145
+ show_log=show_log,
1146
+ sync_to_maisaka_history=sync_to_maisaka_history,
1147
+ maisaka_source_kind=maisaka_source_kind,
1148
+ )
1149
+ if sent_message is not None:
1150
+ logger.debug(f"[SendService] 成功发送消息到 {stream_id}")
1151
+ return sent_message
1152
+
1153
+ logger.error("[SendService] 发送消息失败")
1154
+ return None
1155
+ except Exception as exc:
1156
+ logger.exception(f"[SendService] 发送消息时出错: {exc}")
1157
+ return None
1158
+
1159
+
1160
+ async def text_to_stream_with_message(
1161
+ text: str,
1162
+ stream_id: str,
1163
+ typing: bool = False,
1164
+ set_reply: bool = False,
1165
+ reply_message: Optional[MaiMessage] = None,
1166
+ storage_message: bool = True,
1167
+ selected_expressions: Optional[List[int]] = None,
1168
+ sync_to_maisaka_history: bool = False,
1169
+ maisaka_source_kind: str = "outbound_send",
1170
+ ) -> Optional[SessionMessage]:
1171
+ """向指定流发送文本消息,并返回发送成功后的消息对象。"""
1172
+ return await _send_to_target_with_message(
1173
+ message_sequence=MessageSequence(components=[TextComponent(text=text)]),
1174
+ stream_id=stream_id,
1175
+ typing=typing,
1176
+ set_reply=set_reply,
1177
+ reply_message=reply_message,
1178
+ storage_message=storage_message,
1179
+ selected_expressions=selected_expressions,
1180
+ sync_to_maisaka_history=sync_to_maisaka_history,
1181
+ maisaka_source_kind=maisaka_source_kind,
1182
+ )
1183
+
1184
+
1185
+ async def text_to_stream(
1186
+ text: str,
1187
+ stream_id: str,
1188
+ typing: bool = False,
1189
+ set_reply: bool = False,
1190
+ reply_message: Optional[MaiMessage] = None,
1191
+ storage_message: bool = True,
1192
+ selected_expressions: Optional[List[int]] = None,
1193
+ sync_to_maisaka_history: bool = False,
1194
+ maisaka_source_kind: str = "outbound_send",
1195
+ ) -> bool:
1196
+ """向指定流发送文本消息。
1197
+
1198
+ Args:
1199
+ text: 要发送的文本内容。
1200
+ stream_id: 目标会话 ID。
1201
+ typing: 是否显示输入中状态。
1202
+ set_reply: 是否附带引用回复。
1203
+ reply_message: 被回复的消息对象。
1204
+ storage_message: 是否在发送成功后写入数据库。
1205
+ selected_expressions: 可选的表情候选索引列表。
1206
+
1207
+ Returns:
1208
+ bool: 发送成功时返回 ``True``。
1209
+ """
1210
+ return (
1211
+ await text_to_stream_with_message(
1212
+ text=text,
1213
+ stream_id=stream_id,
1214
+ typing=typing,
1215
+ set_reply=set_reply,
1216
+ reply_message=reply_message,
1217
+ storage_message=storage_message,
1218
+ selected_expressions=selected_expressions,
1219
+ sync_to_maisaka_history=sync_to_maisaka_history,
1220
+ maisaka_source_kind=maisaka_source_kind,
1221
+ )
1222
+ is not None
1223
+ )
1224
+
1225
+
1226
+ async def emoji_to_stream_with_message(
1227
+ emoji_base64: str,
1228
+ stream_id: str,
1229
+ storage_message: bool = True,
1230
+ set_reply: bool = False,
1231
+ reply_message: Optional[MaiMessage] = None,
1232
+ sync_to_maisaka_history: bool = False,
1233
+ maisaka_source_kind: str = "outbound_send",
1234
+ ) -> Optional[SessionMessage]:
1235
+ """向指定流发送表情消息,并返回发送成功后的消息对象。"""
1236
+ return await _send_to_target_with_message(
1237
+ message_sequence=_build_message_sequence_from_custom_message("emoji", emoji_base64),
1238
+ stream_id=stream_id,
1239
+ typing=False,
1240
+ storage_message=storage_message,
1241
+ set_reply=set_reply,
1242
+ reply_message=reply_message,
1243
+ sync_to_maisaka_history=sync_to_maisaka_history,
1244
+ maisaka_source_kind=maisaka_source_kind,
1245
+ )
1246
+
1247
+
1248
+ async def emoji_to_stream(
1249
+ emoji_base64: str,
1250
+ stream_id: str,
1251
+ storage_message: bool = True,
1252
+ set_reply: bool = False,
1253
+ reply_message: Optional[MaiMessage] = None,
1254
+ sync_to_maisaka_history: bool = False,
1255
+ maisaka_source_kind: str = "outbound_send",
1256
+ ) -> bool:
1257
+ """向指定流发送表情消息。
1258
+
1259
+ Args:
1260
+ emoji_base64: 表情图片的 Base64 内容。
1261
+ stream_id: 目标会话 ID。
1262
+ storage_message: 是否在发送成功后写入数据库。
1263
+ set_reply: 是否附带引用回复。
1264
+ reply_message: 被回复的消息对象。
1265
+
1266
+ Returns:
1267
+ bool: 发送成功时返回 ``True``。
1268
+ """
1269
+ return (
1270
+ await emoji_to_stream_with_message(
1271
+ emoji_base64=emoji_base64,
1272
+ stream_id=stream_id,
1273
+ storage_message=storage_message,
1274
+ set_reply=set_reply,
1275
+ reply_message=reply_message,
1276
+ sync_to_maisaka_history=sync_to_maisaka_history,
1277
+ maisaka_source_kind=maisaka_source_kind,
1278
+ )
1279
+ is not None
1280
+ )
1281
+
1282
+
1283
+ async def image_to_stream_with_message(
1284
+ image_base64: str,
1285
+ stream_id: str,
1286
+ storage_message: bool = True,
1287
+ set_reply: bool = False,
1288
+ reply_message: Optional[MaiMessage] = None,
1289
+ sync_to_maisaka_history: bool = False,
1290
+ maisaka_source_kind: str = "outbound_send",
1291
+ ) -> Optional[SessionMessage]:
1292
+ """向指定流发送图片消息,并返回发送成功后的消息对象。"""
1293
+ return await _send_to_target_with_message(
1294
+ message_sequence=_build_message_sequence_from_custom_message("image", image_base64),
1295
+ stream_id=stream_id,
1296
+ typing=False,
1297
+ storage_message=storage_message,
1298
+ set_reply=set_reply,
1299
+ reply_message=reply_message,
1300
+ sync_to_maisaka_history=sync_to_maisaka_history,
1301
+ maisaka_source_kind=maisaka_source_kind,
1302
+ )
1303
+
1304
+
1305
+ async def image_to_stream(
1306
+ image_base64: str,
1307
+ stream_id: str,
1308
+ storage_message: bool = True,
1309
+ set_reply: bool = False,
1310
+ reply_message: Optional[MaiMessage] = None,
1311
+ sync_to_maisaka_history: bool = False,
1312
+ maisaka_source_kind: str = "outbound_send",
1313
+ ) -> bool:
1314
+ """向指定流发送图片消息。
1315
+
1316
+ Args:
1317
+ image_base64: 图片的 Base64 内容。
1318
+ stream_id: 目标会话 ID。
1319
+ storage_message: 是否在发送成功后写入数据库。
1320
+ set_reply: 是否附带引用回复。
1321
+ reply_message: 被回复的消息对象。
1322
+
1323
+ Returns:
1324
+ bool: 发送成功时返回 ``True``。
1325
+ """
1326
+ return (
1327
+ await image_to_stream_with_message(
1328
+ image_base64=image_base64,
1329
+ stream_id=stream_id,
1330
+ storage_message=storage_message,
1331
+ set_reply=set_reply,
1332
+ reply_message=reply_message,
1333
+ sync_to_maisaka_history=sync_to_maisaka_history,
1334
+ maisaka_source_kind=maisaka_source_kind,
1335
+ )
1336
+ is not None
1337
+ )
1338
+
1339
+
1340
+ async def custom_to_stream_with_message(
1341
+ message_type: str,
1342
+ content: str | Dict[str, Any],
1343
+ stream_id: str,
1344
+ processed_plain_text: str = "",
1345
+ typing: bool = False,
1346
+ reply_message: Optional[MaiMessage] = None,
1347
+ set_reply: bool = False,
1348
+ storage_message: bool = True,
1349
+ show_log: bool = True,
1350
+ sync_to_maisaka_history: bool = False,
1351
+ maisaka_source_kind: str = "outbound_send",
1352
+ ) -> Optional[SessionMessage]:
1353
+ """向指定流发送自定义类型消息,并返回发送成功后的消息对象。"""
1354
+ return await _send_to_target_with_message(
1355
+ message_sequence=_build_message_sequence_from_custom_message(message_type, content),
1356
+ stream_id=stream_id,
1357
+ processed_plain_text=processed_plain_text,
1358
+ typing=typing,
1359
+ reply_message=reply_message,
1360
+ set_reply=set_reply,
1361
+ storage_message=storage_message,
1362
+ show_log=show_log,
1363
+ sync_to_maisaka_history=sync_to_maisaka_history,
1364
+ maisaka_source_kind=maisaka_source_kind,
1365
+ )
1366
+
1367
+
1368
+ async def custom_to_stream(
1369
+ message_type: str,
1370
+ content: str | Dict[str, Any],
1371
+ stream_id: str,
1372
+ processed_plain_text: str = "",
1373
+ typing: bool = False,
1374
+ reply_message: Optional[MaiMessage] = None,
1375
+ set_reply: bool = False,
1376
+ storage_message: bool = True,
1377
+ show_log: bool = True,
1378
+ sync_to_maisaka_history: bool = False,
1379
+ maisaka_source_kind: str = "outbound_send",
1380
+ ) -> bool:
1381
+ """向指定流发送自定义类型消息。
1382
+
1383
+ Args:
1384
+ message_type: 自定义消息类型。
1385
+ content: 自定义消息内容。
1386
+ stream_id: 目标会话 ID。
1387
+ processed_plain_text: 可选的预处理纯文本内容。
1388
+ typing: 是否显示输入中状态。
1389
+ reply_message: 被回复的消息对象。
1390
+ set_reply: 是否附带引用回复。
1391
+ storage_message: 是否在发送成功后写入数据库。
1392
+ show_log: 是否输出发送日志。
1393
+
1394
+ Returns:
1395
+ bool: 发送成功时返回 ``True``。
1396
+ """
1397
+ return (
1398
+ await custom_to_stream_with_message(
1399
+ message_type=message_type,
1400
+ content=content,
1401
+ stream_id=stream_id,
1402
+ processed_plain_text=processed_plain_text,
1403
+ typing=typing,
1404
+ reply_message=reply_message,
1405
+ set_reply=set_reply,
1406
+ storage_message=storage_message,
1407
+ show_log=show_log,
1408
+ sync_to_maisaka_history=sync_to_maisaka_history,
1409
+ maisaka_source_kind=maisaka_source_kind,
1410
+ )
1411
+ is not None
1412
+ )
1413
+
1414
+
1415
+ async def custom_reply_set_to_stream_with_message(
1416
+ reply_set: MessageSequence,
1417
+ stream_id: str,
1418
+ processed_plain_text: str = "",
1419
+ typing: bool = False,
1420
+ reply_message: Optional[MaiMessage] = None,
1421
+ set_reply: bool = False,
1422
+ storage_message: bool = True,
1423
+ show_log: bool = True,
1424
+ sync_to_maisaka_history: bool = False,
1425
+ maisaka_source_kind: str = "outbound_send",
1426
+ ) -> Optional[SessionMessage]:
1427
+ """向指定流发送消息组件序列,并返回发送成功后的消息对象。"""
1428
+ return await _send_to_target_with_message(
1429
+ message_sequence=reply_set,
1430
+ stream_id=stream_id,
1431
+ processed_plain_text=processed_plain_text,
1432
+ typing=typing,
1433
+ reply_message=reply_message,
1434
+ set_reply=set_reply,
1435
+ storage_message=storage_message,
1436
+ show_log=show_log,
1437
+ sync_to_maisaka_history=sync_to_maisaka_history,
1438
+ maisaka_source_kind=maisaka_source_kind,
1439
+ )
1440
+
1441
+
1442
+ async def custom_reply_set_to_stream(
1443
+ reply_set: MessageSequence,
1444
+ stream_id: str,
1445
+ processed_plain_text: str = "",
1446
+ typing: bool = False,
1447
+ reply_message: Optional[MaiMessage] = None,
1448
+ set_reply: bool = False,
1449
+ storage_message: bool = True,
1450
+ show_log: bool = True,
1451
+ sync_to_maisaka_history: bool = False,
1452
+ maisaka_source_kind: str = "outbound_send",
1453
+ ) -> bool:
1454
+ """向指定流发送消息组件序列。
1455
+
1456
+ Args:
1457
+ reply_set: 待发送的消息组件序列。
1458
+ stream_id: 目标会话 ID。
1459
+ processed_plain_text: 可选的预处理纯文本内容。
1460
+ typing: 是否显示输入中状态。
1461
+ reply_message: 被回复的消息对象。
1462
+ set_reply: 是否附带引用回复。
1463
+ storage_message: 是否在发送成功后写入数据库。
1464
+ show_log: 是否输出发送日志。
1465
+
1466
+ Returns:
1467
+ bool: 发送成功时返回 ``True``。
1468
+ """
1469
+ return (
1470
+ await custom_reply_set_to_stream_with_message(
1471
+ reply_set=reply_set,
1472
+ stream_id=stream_id,
1473
+ processed_plain_text=processed_plain_text,
1474
+ typing=typing,
1475
+ reply_message=reply_message,
1476
+ set_reply=set_reply,
1477
+ storage_message=storage_message,
1478
+ show_log=show_log,
1479
+ sync_to_maisaka_history=sync_to_maisaka_history,
1480
+ maisaka_source_kind=maisaka_source_kind,
1481
+ )
1482
+ is not None
1483
+ )