AstrBot 3.5.6__py3-none-any.whl → 4.7.0__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 (288) hide show
  1. astrbot/api/__init__.py +16 -4
  2. astrbot/api/all.py +2 -1
  3. astrbot/api/event/__init__.py +5 -6
  4. astrbot/api/event/filter/__init__.py +37 -34
  5. astrbot/api/platform/__init__.py +7 -8
  6. astrbot/api/provider/__init__.py +8 -7
  7. astrbot/api/star/__init__.py +3 -4
  8. astrbot/api/util/__init__.py +2 -2
  9. astrbot/cli/__init__.py +1 -0
  10. astrbot/cli/__main__.py +18 -197
  11. astrbot/cli/commands/__init__.py +6 -0
  12. astrbot/cli/commands/cmd_conf.py +209 -0
  13. astrbot/cli/commands/cmd_init.py +56 -0
  14. astrbot/cli/commands/cmd_plug.py +245 -0
  15. astrbot/cli/commands/cmd_run.py +62 -0
  16. astrbot/cli/utils/__init__.py +18 -0
  17. astrbot/cli/utils/basic.py +76 -0
  18. astrbot/cli/utils/plugin.py +246 -0
  19. astrbot/cli/utils/version_comparator.py +90 -0
  20. astrbot/core/__init__.py +17 -19
  21. astrbot/core/agent/agent.py +14 -0
  22. astrbot/core/agent/handoff.py +38 -0
  23. astrbot/core/agent/hooks.py +30 -0
  24. astrbot/core/agent/mcp_client.py +385 -0
  25. astrbot/core/agent/message.py +175 -0
  26. astrbot/core/agent/response.py +14 -0
  27. astrbot/core/agent/run_context.py +22 -0
  28. astrbot/core/agent/runners/__init__.py +3 -0
  29. astrbot/core/agent/runners/base.py +65 -0
  30. astrbot/core/agent/runners/coze/coze_agent_runner.py +367 -0
  31. astrbot/core/agent/runners/coze/coze_api_client.py +324 -0
  32. astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py +403 -0
  33. astrbot/core/agent/runners/dify/dify_agent_runner.py +336 -0
  34. astrbot/core/agent/runners/dify/dify_api_client.py +195 -0
  35. astrbot/core/agent/runners/tool_loop_agent_runner.py +400 -0
  36. astrbot/core/agent/tool.py +285 -0
  37. astrbot/core/agent/tool_executor.py +17 -0
  38. astrbot/core/astr_agent_context.py +19 -0
  39. astrbot/core/astr_agent_hooks.py +36 -0
  40. astrbot/core/astr_agent_run_util.py +80 -0
  41. astrbot/core/astr_agent_tool_exec.py +246 -0
  42. astrbot/core/astrbot_config_mgr.py +275 -0
  43. astrbot/core/config/__init__.py +2 -2
  44. astrbot/core/config/astrbot_config.py +60 -20
  45. astrbot/core/config/default.py +1972 -453
  46. astrbot/core/config/i18n_utils.py +110 -0
  47. astrbot/core/conversation_mgr.py +285 -75
  48. astrbot/core/core_lifecycle.py +167 -62
  49. astrbot/core/db/__init__.py +305 -102
  50. astrbot/core/db/migration/helper.py +69 -0
  51. astrbot/core/db/migration/migra_3_to_4.py +357 -0
  52. astrbot/core/db/migration/migra_45_to_46.py +44 -0
  53. astrbot/core/db/migration/migra_webchat_session.py +131 -0
  54. astrbot/core/db/migration/shared_preferences_v3.py +48 -0
  55. astrbot/core/db/migration/sqlite_v3.py +497 -0
  56. astrbot/core/db/po.py +259 -55
  57. astrbot/core/db/sqlite.py +773 -528
  58. astrbot/core/db/vec_db/base.py +73 -0
  59. astrbot/core/db/vec_db/faiss_impl/__init__.py +3 -0
  60. astrbot/core/db/vec_db/faiss_impl/document_storage.py +392 -0
  61. astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +93 -0
  62. astrbot/core/db/vec_db/faiss_impl/sqlite_init.sql +17 -0
  63. astrbot/core/db/vec_db/faiss_impl/vec_db.py +204 -0
  64. astrbot/core/event_bus.py +26 -22
  65. astrbot/core/exceptions.py +9 -0
  66. astrbot/core/file_token_service.py +98 -0
  67. astrbot/core/initial_loader.py +19 -10
  68. astrbot/core/knowledge_base/chunking/__init__.py +9 -0
  69. astrbot/core/knowledge_base/chunking/base.py +25 -0
  70. astrbot/core/knowledge_base/chunking/fixed_size.py +59 -0
  71. astrbot/core/knowledge_base/chunking/recursive.py +161 -0
  72. astrbot/core/knowledge_base/kb_db_sqlite.py +301 -0
  73. astrbot/core/knowledge_base/kb_helper.py +642 -0
  74. astrbot/core/knowledge_base/kb_mgr.py +330 -0
  75. astrbot/core/knowledge_base/models.py +120 -0
  76. astrbot/core/knowledge_base/parsers/__init__.py +13 -0
  77. astrbot/core/knowledge_base/parsers/base.py +51 -0
  78. astrbot/core/knowledge_base/parsers/markitdown_parser.py +26 -0
  79. astrbot/core/knowledge_base/parsers/pdf_parser.py +101 -0
  80. astrbot/core/knowledge_base/parsers/text_parser.py +42 -0
  81. astrbot/core/knowledge_base/parsers/url_parser.py +103 -0
  82. astrbot/core/knowledge_base/parsers/util.py +13 -0
  83. astrbot/core/knowledge_base/prompts.py +65 -0
  84. astrbot/core/knowledge_base/retrieval/__init__.py +14 -0
  85. astrbot/core/knowledge_base/retrieval/hit_stopwords.txt +767 -0
  86. astrbot/core/knowledge_base/retrieval/manager.py +276 -0
  87. astrbot/core/knowledge_base/retrieval/rank_fusion.py +142 -0
  88. astrbot/core/knowledge_base/retrieval/sparse_retriever.py +136 -0
  89. astrbot/core/log.py +21 -15
  90. astrbot/core/message/components.py +413 -287
  91. astrbot/core/message/message_event_result.py +35 -24
  92. astrbot/core/persona_mgr.py +192 -0
  93. astrbot/core/pipeline/__init__.py +14 -14
  94. astrbot/core/pipeline/content_safety_check/stage.py +13 -9
  95. astrbot/core/pipeline/content_safety_check/strategies/__init__.py +1 -2
  96. astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py +13 -14
  97. astrbot/core/pipeline/content_safety_check/strategies/keywords.py +2 -1
  98. astrbot/core/pipeline/content_safety_check/strategies/strategy.py +6 -6
  99. astrbot/core/pipeline/context.py +7 -1
  100. astrbot/core/pipeline/context_utils.py +107 -0
  101. astrbot/core/pipeline/preprocess_stage/stage.py +63 -36
  102. astrbot/core/pipeline/process_stage/method/agent_request.py +48 -0
  103. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +464 -0
  104. astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +202 -0
  105. astrbot/core/pipeline/process_stage/method/star_request.py +26 -32
  106. astrbot/core/pipeline/process_stage/stage.py +21 -15
  107. astrbot/core/pipeline/process_stage/utils.py +125 -0
  108. astrbot/core/pipeline/rate_limit_check/stage.py +34 -36
  109. astrbot/core/pipeline/respond/stage.py +142 -101
  110. astrbot/core/pipeline/result_decorate/stage.py +124 -57
  111. astrbot/core/pipeline/scheduler.py +21 -16
  112. astrbot/core/pipeline/session_status_check/stage.py +37 -0
  113. astrbot/core/pipeline/stage.py +11 -76
  114. astrbot/core/pipeline/waking_check/stage.py +69 -33
  115. astrbot/core/pipeline/whitelist_check/stage.py +10 -7
  116. astrbot/core/platform/__init__.py +6 -6
  117. astrbot/core/platform/astr_message_event.py +107 -129
  118. astrbot/core/platform/astrbot_message.py +32 -12
  119. astrbot/core/platform/manager.py +62 -18
  120. astrbot/core/platform/message_session.py +30 -0
  121. astrbot/core/platform/platform.py +16 -24
  122. astrbot/core/platform/platform_metadata.py +9 -4
  123. astrbot/core/platform/register.py +12 -7
  124. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +136 -60
  125. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +126 -46
  126. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +63 -31
  127. astrbot/core/platform/sources/dingtalk/dingtalk_event.py +30 -26
  128. astrbot/core/platform/sources/discord/client.py +129 -0
  129. astrbot/core/platform/sources/discord/components.py +139 -0
  130. astrbot/core/platform/sources/discord/discord_platform_adapter.py +473 -0
  131. astrbot/core/platform/sources/discord/discord_platform_event.py +313 -0
  132. astrbot/core/platform/sources/lark/lark_adapter.py +27 -18
  133. astrbot/core/platform/sources/lark/lark_event.py +39 -13
  134. astrbot/core/platform/sources/misskey/misskey_adapter.py +770 -0
  135. astrbot/core/platform/sources/misskey/misskey_api.py +964 -0
  136. astrbot/core/platform/sources/misskey/misskey_event.py +163 -0
  137. astrbot/core/platform/sources/misskey/misskey_utils.py +550 -0
  138. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +149 -33
  139. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +41 -26
  140. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -17
  141. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_event.py +3 -1
  142. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +14 -8
  143. astrbot/core/platform/sources/satori/satori_adapter.py +792 -0
  144. astrbot/core/platform/sources/satori/satori_event.py +432 -0
  145. astrbot/core/platform/sources/slack/client.py +164 -0
  146. astrbot/core/platform/sources/slack/slack_adapter.py +416 -0
  147. astrbot/core/platform/sources/slack/slack_event.py +253 -0
  148. astrbot/core/platform/sources/telegram/tg_adapter.py +100 -43
  149. astrbot/core/platform/sources/telegram/tg_event.py +136 -36
  150. astrbot/core/platform/sources/webchat/webchat_adapter.py +72 -22
  151. astrbot/core/platform/sources/webchat/webchat_event.py +46 -22
  152. astrbot/core/platform/sources/webchat/webchat_queue_mgr.py +35 -0
  153. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +926 -0
  154. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +178 -0
  155. astrbot/core/platform/sources/wechatpadpro/xml_data_parser.py +159 -0
  156. astrbot/core/platform/sources/wecom/wecom_adapter.py +169 -27
  157. astrbot/core/platform/sources/wecom/wecom_event.py +162 -77
  158. astrbot/core/platform/sources/wecom/wecom_kf.py +279 -0
  159. astrbot/core/platform/sources/wecom/wecom_kf_message.py +196 -0
  160. astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py +297 -0
  161. astrbot/core/platform/sources/wecom_ai_bot/__init__.py +15 -0
  162. astrbot/core/platform/sources/wecom_ai_bot/ierror.py +19 -0
  163. astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +472 -0
  164. astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py +417 -0
  165. astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +152 -0
  166. astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py +153 -0
  167. astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +168 -0
  168. astrbot/core/platform/sources/wecom_ai_bot/wecomai_utils.py +209 -0
  169. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +306 -0
  170. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +186 -0
  171. astrbot/core/platform_message_history_mgr.py +49 -0
  172. astrbot/core/provider/__init__.py +2 -3
  173. astrbot/core/provider/entites.py +8 -8
  174. astrbot/core/provider/entities.py +154 -98
  175. astrbot/core/provider/func_tool_manager.py +446 -458
  176. astrbot/core/provider/manager.py +345 -207
  177. astrbot/core/provider/provider.py +188 -73
  178. astrbot/core/provider/register.py +9 -7
  179. astrbot/core/provider/sources/anthropic_source.py +295 -115
  180. astrbot/core/provider/sources/azure_tts_source.py +224 -0
  181. astrbot/core/provider/sources/bailian_rerank_source.py +236 -0
  182. astrbot/core/provider/sources/dashscope_tts.py +138 -14
  183. astrbot/core/provider/sources/edge_tts_source.py +24 -19
  184. astrbot/core/provider/sources/fishaudio_tts_api_source.py +58 -13
  185. astrbot/core/provider/sources/gemini_embedding_source.py +61 -0
  186. astrbot/core/provider/sources/gemini_source.py +310 -132
  187. astrbot/core/provider/sources/gemini_tts_source.py +81 -0
  188. astrbot/core/provider/sources/groq_source.py +15 -0
  189. astrbot/core/provider/sources/gsv_selfhosted_source.py +151 -0
  190. astrbot/core/provider/sources/gsvi_tts_source.py +14 -7
  191. astrbot/core/provider/sources/minimax_tts_api_source.py +159 -0
  192. astrbot/core/provider/sources/openai_embedding_source.py +40 -0
  193. astrbot/core/provider/sources/openai_source.py +241 -145
  194. astrbot/core/provider/sources/openai_tts_api_source.py +18 -7
  195. astrbot/core/provider/sources/sensevoice_selfhosted_source.py +13 -11
  196. astrbot/core/provider/sources/vllm_rerank_source.py +71 -0
  197. astrbot/core/provider/sources/volcengine_tts.py +115 -0
  198. astrbot/core/provider/sources/whisper_api_source.py +18 -13
  199. astrbot/core/provider/sources/whisper_selfhosted_source.py +19 -12
  200. astrbot/core/provider/sources/xinference_rerank_source.py +116 -0
  201. astrbot/core/provider/sources/xinference_stt_provider.py +197 -0
  202. astrbot/core/provider/sources/zhipu_source.py +6 -73
  203. astrbot/core/star/__init__.py +43 -11
  204. astrbot/core/star/config.py +17 -18
  205. astrbot/core/star/context.py +362 -138
  206. astrbot/core/star/filter/__init__.py +4 -3
  207. astrbot/core/star/filter/command.py +111 -35
  208. astrbot/core/star/filter/command_group.py +46 -34
  209. astrbot/core/star/filter/custom_filter.py +6 -5
  210. astrbot/core/star/filter/event_message_type.py +4 -2
  211. astrbot/core/star/filter/permission.py +4 -2
  212. astrbot/core/star/filter/platform_adapter_type.py +45 -12
  213. astrbot/core/star/filter/regex.py +4 -2
  214. astrbot/core/star/register/__init__.py +19 -15
  215. astrbot/core/star/register/star.py +41 -13
  216. astrbot/core/star/register/star_handler.py +236 -86
  217. astrbot/core/star/session_llm_manager.py +280 -0
  218. astrbot/core/star/session_plugin_manager.py +170 -0
  219. astrbot/core/star/star.py +36 -43
  220. astrbot/core/star/star_handler.py +47 -85
  221. astrbot/core/star/star_manager.py +442 -260
  222. astrbot/core/star/star_tools.py +167 -45
  223. astrbot/core/star/updator.py +17 -20
  224. astrbot/core/umop_config_router.py +106 -0
  225. astrbot/core/updator.py +38 -13
  226. astrbot/core/utils/astrbot_path.py +39 -0
  227. astrbot/core/utils/command_parser.py +1 -1
  228. astrbot/core/utils/io.py +119 -60
  229. astrbot/core/utils/log_pipe.py +1 -1
  230. astrbot/core/utils/metrics.py +11 -10
  231. astrbot/core/utils/migra_helper.py +73 -0
  232. astrbot/core/utils/path_util.py +63 -62
  233. astrbot/core/utils/pip_installer.py +37 -15
  234. astrbot/core/utils/session_lock.py +29 -0
  235. astrbot/core/utils/session_waiter.py +19 -20
  236. astrbot/core/utils/shared_preferences.py +174 -34
  237. astrbot/core/utils/t2i/__init__.py +4 -1
  238. astrbot/core/utils/t2i/local_strategy.py +386 -238
  239. astrbot/core/utils/t2i/network_strategy.py +109 -49
  240. astrbot/core/utils/t2i/renderer.py +29 -14
  241. astrbot/core/utils/t2i/template/astrbot_powershell.html +184 -0
  242. astrbot/core/utils/t2i/template_manager.py +111 -0
  243. astrbot/core/utils/tencent_record_helper.py +115 -1
  244. astrbot/core/utils/version_comparator.py +10 -13
  245. astrbot/core/zip_updator.py +112 -65
  246. astrbot/dashboard/routes/__init__.py +20 -13
  247. astrbot/dashboard/routes/auth.py +20 -9
  248. astrbot/dashboard/routes/chat.py +297 -141
  249. astrbot/dashboard/routes/config.py +652 -55
  250. astrbot/dashboard/routes/conversation.py +107 -37
  251. astrbot/dashboard/routes/file.py +26 -0
  252. astrbot/dashboard/routes/knowledge_base.py +1244 -0
  253. astrbot/dashboard/routes/log.py +27 -2
  254. astrbot/dashboard/routes/persona.py +202 -0
  255. astrbot/dashboard/routes/plugin.py +197 -139
  256. astrbot/dashboard/routes/route.py +27 -7
  257. astrbot/dashboard/routes/session_management.py +354 -0
  258. astrbot/dashboard/routes/stat.py +85 -18
  259. astrbot/dashboard/routes/static_file.py +5 -2
  260. astrbot/dashboard/routes/t2i.py +233 -0
  261. astrbot/dashboard/routes/tools.py +184 -120
  262. astrbot/dashboard/routes/update.py +59 -36
  263. astrbot/dashboard/server.py +96 -36
  264. astrbot/dashboard/utils.py +165 -0
  265. astrbot-4.7.0.dist-info/METADATA +294 -0
  266. astrbot-4.7.0.dist-info/RECORD +274 -0
  267. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/WHEEL +1 -1
  268. astrbot/core/db/plugin/sqlite_impl.py +0 -112
  269. astrbot/core/db/sqlite_init.sql +0 -50
  270. astrbot/core/pipeline/platform_compatibility/stage.py +0 -56
  271. astrbot/core/pipeline/process_stage/method/llm_request.py +0 -606
  272. astrbot/core/platform/sources/gewechat/client.py +0 -806
  273. astrbot/core/platform/sources/gewechat/downloader.py +0 -55
  274. astrbot/core/platform/sources/gewechat/gewechat_event.py +0 -255
  275. astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py +0 -103
  276. astrbot/core/platform/sources/gewechat/xml_data_parser.py +0 -110
  277. astrbot/core/provider/sources/dashscope_source.py +0 -203
  278. astrbot/core/provider/sources/dify_source.py +0 -281
  279. astrbot/core/provider/sources/llmtuner_source.py +0 -132
  280. astrbot/core/rag/embedding/openai_source.py +0 -20
  281. astrbot/core/rag/knowledge_db_mgr.py +0 -94
  282. astrbot/core/rag/store/__init__.py +0 -9
  283. astrbot/core/rag/store/chroma_db.py +0 -42
  284. astrbot/core/utils/dify_api_client.py +0 -152
  285. astrbot-3.5.6.dist-info/METADATA +0 -249
  286. astrbot-3.5.6.dist-info/RECORD +0 -158
  287. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/entry_points.txt +0 -0
  288. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,13 +1,28 @@
1
- import uuid
1
+ import asyncio
2
2
  import json
3
3
  import os
4
- from .route import Route, Response, RouteContext
5
- from astrbot.core import web_chat_queue, web_chat_back_queue
6
- from quart import request, Response as QuartResponse, g, make_response
7
- from astrbot.core.db import BaseDatabase
8
- import asyncio
4
+ import uuid
5
+ from contextlib import asynccontextmanager
6
+
7
+ from quart import Response as QuartResponse
8
+ from quart import g, make_response, request
9
+
9
10
  from astrbot.core import logger
10
11
  from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
12
+ from astrbot.core.db import BaseDatabase
13
+ from astrbot.core.platform.sources.webchat.webchat_queue_mgr import webchat_queue_mgr
14
+ from astrbot.core.utils.astrbot_path import get_astrbot_data_path
15
+
16
+ from .route import Response, Route, RouteContext
17
+
18
+
19
+ @asynccontextmanager
20
+ async def track_conversation(convs: dict, conv_id: str):
21
+ convs[conv_id] = True
22
+ try:
23
+ yield
24
+ finally:
25
+ convs.pop(conv_id, None)
11
26
 
12
27
 
13
28
  class ChatRoute(Route):
@@ -20,38 +35,30 @@ class ChatRoute(Route):
20
35
  super().__init__(context)
21
36
  self.routes = {
22
37
  "/chat/send": ("POST", self.chat),
23
- "/chat/listen": ("GET", self.listener),
24
- "/chat/new_conversation": ("GET", self.new_conversation),
25
- "/chat/conversations": ("GET", self.get_conversations),
26
- "/chat/get_conversation": ("GET", self.get_conversation),
27
- "/chat/delete_conversation": ("GET", self.delete_conversation),
38
+ "/chat/new_session": ("GET", self.new_session),
39
+ "/chat/sessions": ("GET", self.get_sessions),
40
+ "/chat/get_session": ("GET", self.get_session),
41
+ "/chat/delete_session": ("GET", self.delete_webchat_session),
42
+ "/chat/update_session_display_name": (
43
+ "POST",
44
+ self.update_session_display_name,
45
+ ),
28
46
  "/chat/get_file": ("GET", self.get_file),
29
47
  "/chat/post_image": ("POST", self.post_image),
30
48
  "/chat/post_file": ("POST", self.post_file),
31
- "/chat/status": ("GET", self.status),
32
49
  }
33
- self.db = db
34
50
  self.core_lifecycle = core_lifecycle
35
51
  self.register_routes()
36
- self.imgs_dir = "data/webchat/imgs"
52
+ self.imgs_dir = os.path.join(get_astrbot_data_path(), "webchat", "imgs")
53
+ os.makedirs(self.imgs_dir, exist_ok=True)
37
54
 
38
55
  self.supported_imgs = ["jpg", "jpeg", "png", "gif", "webp"]
56
+ self.conv_mgr = core_lifecycle.conversation_manager
57
+ self.platform_history_mgr = core_lifecycle.platform_message_history_manager
58
+ self.db = db
59
+ self.umop_config_router = core_lifecycle.umop_config_router
39
60
 
40
- self.curr_user_cid = {}
41
- self.curr_chat_sse = {}
42
-
43
- async def status(self):
44
- has_llm_enabled = (
45
- self.core_lifecycle.provider_manager.curr_provider_inst is not None
46
- )
47
- has_stt_enabled = (
48
- self.core_lifecycle.provider_manager.curr_stt_provider_inst is not None
49
- )
50
- return (
51
- Response()
52
- .ok(data={"llm_enabled": has_llm_enabled, "stt_enabled": has_stt_enabled})
53
- .__dict__
54
- )
61
+ self.running_convs: dict[str, bool] = {}
55
62
 
56
63
  async def get_file(self):
57
64
  filename = request.args.get("filename")
@@ -59,16 +66,24 @@ class ChatRoute(Route):
59
66
  return Response().error("Missing key: filename").__dict__
60
67
 
61
68
  try:
62
- with open(os.path.join(self.imgs_dir, filename), "rb") as f:
63
- if filename.endswith(".wav"):
69
+ file_path = os.path.join(self.imgs_dir, os.path.basename(filename))
70
+ real_file_path = os.path.realpath(file_path)
71
+ real_imgs_dir = os.path.realpath(self.imgs_dir)
72
+
73
+ if not real_file_path.startswith(real_imgs_dir):
74
+ return Response().error("Invalid file path").__dict__
75
+
76
+ with open(real_file_path, "rb") as f:
77
+ filename_ext = os.path.splitext(filename)[1].lower()
78
+
79
+ if filename_ext == ".wav":
64
80
  return QuartResponse(f.read(), mimetype="audio/wav")
65
- elif filename.split(".")[-1] in self.supported_imgs:
81
+ if filename_ext[1:] in self.supported_imgs:
66
82
  return QuartResponse(f.read(), mimetype="image/jpeg")
67
- else:
68
- return QuartResponse(f.read())
83
+ return QuartResponse(f.read())
69
84
 
70
- except FileNotFoundError:
71
- return Response().error("File not found").__dict__
85
+ except (FileNotFoundError, OSError):
86
+ return Response().error("File access error").__dict__
72
87
 
73
88
  async def post_image(self):
74
89
  post_data = await request.files
@@ -88,8 +103,7 @@ class ChatRoute(Route):
88
103
  return Response().error("Missing key: file").__dict__
89
104
 
90
105
  file = post_data["file"]
91
- filename = f"{str(uuid.uuid4())}"
92
- print(file)
106
+ filename = f"{uuid.uuid4()!s}"
93
107
  # 通过文件格式判断文件类型
94
108
  if file.content_type.startswith("audio"):
95
109
  filename += ".wav"
@@ -106,112 +120,125 @@ class ChatRoute(Route):
106
120
  if "message" not in post_data and "image_url" not in post_data:
107
121
  return Response().error("Missing key: message or image_url").__dict__
108
122
 
109
- if "conversation_id" not in post_data:
110
- return Response().error("Missing key: conversation_id").__dict__
123
+ if "session_id" not in post_data and "conversation_id" not in post_data:
124
+ return (
125
+ Response().error("Missing key: session_id or conversation_id").__dict__
126
+ )
111
127
 
112
128
  message = post_data["message"]
113
- conversation_id = post_data["conversation_id"]
129
+ # conversation_id = post_data["conversation_id"]
130
+ session_id = post_data.get("session_id", post_data.get("conversation_id"))
114
131
  image_url = post_data.get("image_url")
115
132
  audio_url = post_data.get("audio_url")
133
+ selected_provider = post_data.get("selected_provider")
134
+ selected_model = post_data.get("selected_model")
135
+ enable_streaming = post_data.get("enable_streaming", True) # 默认为 True
136
+
116
137
  if not message and not image_url and not audio_url:
117
138
  return (
118
139
  Response()
119
140
  .error("Message and image_url and audio_url are empty")
120
141
  .__dict__
121
142
  )
122
- if not conversation_id:
123
- return Response().error("conversation_id is empty").__dict__
143
+ if not session_id:
144
+ return Response().error("session_id is empty").__dict__
124
145
 
125
- self.curr_user_cid[username] = conversation_id
146
+ # 追加用户消息
147
+ webchat_conv_id = session_id
126
148
 
127
- await web_chat_queue.put(
128
- (
129
- username,
130
- conversation_id,
131
- {
132
- "message": message,
133
- "image_url": image_url, # list
134
- "audio_url": audio_url,
135
- },
136
- )
137
- )
149
+ # 获取会话特定的队列
150
+ back_queue = webchat_queue_mgr.get_or_create_back_queue(webchat_conv_id)
138
151
 
139
- # 持久化
140
- conversation = self.db.get_conversation_by_user_id(username, conversation_id)
141
- try:
142
- history = json.loads(conversation.history)
143
- except BaseException as e:
144
- print(e)
145
- history = []
146
152
  new_his = {"type": "user", "message": message}
147
153
  if image_url:
148
154
  new_his["image_url"] = image_url
149
155
  if audio_url:
150
156
  new_his["audio_url"] = audio_url
151
- history.append(new_his)
152
- self.db.update_conversation(
153
- username, conversation_id, history=json.dumps(history)
157
+ await self.platform_history_mgr.insert(
158
+ platform_id="webchat",
159
+ user_id=webchat_conv_id,
160
+ content=new_his,
161
+ sender_id=username,
162
+ sender_name=username,
154
163
  )
155
164
 
156
- return Response().ok().__dict__
157
-
158
- async def listener(self):
159
- """一直保持长连接"""
160
-
161
- username = g.get("username", "guest")
162
-
163
- if username in self.curr_chat_sse:
164
- return Response().error("Already connected").__dict__
165
+ async def stream():
166
+ client_disconnected = False
165
167
 
166
- self.curr_chat_sse[username] = None
168
+ try:
169
+ async with track_conversation(self.running_convs, webchat_conv_id):
170
+ while True:
171
+ try:
172
+ result = await asyncio.wait_for(back_queue.get(), timeout=1)
173
+ except asyncio.TimeoutError:
174
+ continue
175
+ except asyncio.CancelledError:
176
+ logger.debug(f"[WebChat] 用户 {username} 断开聊天长连接。")
177
+ client_disconnected = True
178
+ except Exception as e:
179
+ logger.error(f"WebChat stream error: {e}")
180
+
181
+ if not result:
182
+ continue
183
+
184
+ result_text = result["data"]
185
+ type = result.get("type")
186
+ streaming = result.get("streaming", False)
167
187
 
168
- heartbeat = json.dumps({"type": "heartbeat", "data": "ping"})
188
+ try:
189
+ if not client_disconnected:
190
+ yield f"data: {json.dumps(result, ensure_ascii=False)}\n\n"
191
+ except Exception as e:
192
+ if not client_disconnected:
193
+ logger.debug(
194
+ f"[WebChat] 用户 {username} 断开聊天长连接。 {e}",
195
+ )
196
+ client_disconnected = True
169
197
 
170
- async def stream():
171
- try:
172
- yield f"data: {heartbeat}\n\n" # 心跳包
173
- while True:
174
- try:
175
- result = await asyncio.wait_for(
176
- web_chat_back_queue.get(), timeout=10
177
- ) # 设置超时时间为5秒
178
- except asyncio.TimeoutError:
179
- yield f"data: {heartbeat}\n\n" # 心跳包
180
- continue
181
-
182
- if not result:
183
- continue
184
-
185
- result_text = result["data"]
186
- type = result.get("type")
187
- cid = result.get("cid")
188
- streaming = result.get("streaming", False)
189
- if cid != self.curr_user_cid.get(username):
190
- # 丢弃
191
- continue
192
- yield f"data: {json.dumps(result, ensure_ascii=False)}\n\n"
193
- await asyncio.sleep(0.05)
194
-
195
- if streaming and type != "end":
196
- continue
197
-
198
- if result_text:
199
- conversation = self.db.get_conversation_by_user_id(
200
- username, cid
201
- )
202
198
  try:
203
- history = json.loads(conversation.history)
204
- except BaseException as e:
205
- print(e)
206
- history = []
207
- history.append({"type": "bot", "message": result_text})
208
- self.db.update_conversation(
209
- username, cid, history=json.dumps(history)
210
- )
211
- except BaseException as _:
212
- logger.debug(f"用户 {username} 断开聊天长连接。")
213
- self.curr_chat_sse.pop(username)
214
- return
199
+ if not client_disconnected:
200
+ await asyncio.sleep(0.05)
201
+ except asyncio.CancelledError:
202
+ logger.debug(f"[WebChat] 用户 {username} 断开聊天长连接。")
203
+ client_disconnected = True
204
+
205
+ if type == "end":
206
+ break
207
+ elif (
208
+ (streaming and type == "complete")
209
+ or not streaming
210
+ or type == "break"
211
+ ):
212
+ # 追加机器人消息
213
+ new_his = {"type": "bot", "message": result_text}
214
+ if "reasoning" in result:
215
+ new_his["reasoning"] = result["reasoning"]
216
+ await self.platform_history_mgr.insert(
217
+ platform_id="webchat",
218
+ user_id=webchat_conv_id,
219
+ content=new_his,
220
+ sender_id="bot",
221
+ sender_name="bot",
222
+ )
223
+ except BaseException as e:
224
+ logger.exception(f"WebChat stream unexpected error: {e}", exc_info=True)
225
+
226
+ # 将消息放入会话特定的队列
227
+ chat_queue = webchat_queue_mgr.get_or_create_queue(webchat_conv_id)
228
+ await chat_queue.put(
229
+ (
230
+ username,
231
+ webchat_conv_id,
232
+ {
233
+ "message": message,
234
+ "image_url": image_url, # list
235
+ "audio_url": audio_url,
236
+ "selected_provider": selected_provider,
237
+ "selected_model": selected_model,
238
+ "enable_streaming": enable_streaming,
239
+ },
240
+ ),
241
+ )
215
242
 
216
243
  response = await make_response(
217
244
  stream(),
@@ -222,37 +249,166 @@ class ChatRoute(Route):
222
249
  "Connection": "keep-alive",
223
250
  },
224
251
  )
225
- response.timeout = None
252
+ response.timeout = None # fix SSE auto disconnect issue
226
253
  return response
227
254
 
228
- async def delete_conversation(self):
255
+ async def delete_webchat_session(self):
256
+ """Delete a Platform session and all its related data."""
257
+ session_id = request.args.get("session_id")
258
+ if not session_id:
259
+ return Response().error("Missing key: session_id").__dict__
229
260
  username = g.get("username", "guest")
230
- conversation_id = request.args.get("conversation_id")
231
- if not conversation_id:
232
- return Response().error("Missing key: conversation_id").__dict__
233
261
 
234
- self.db.delete_conversation(username, conversation_id)
262
+ # 验证会话是否存在且属于当前用户
263
+ session = await self.db.get_platform_session_by_id(session_id)
264
+ if not session:
265
+ return Response().error(f"Session {session_id} not found").__dict__
266
+ if session.creator != username:
267
+ return Response().error("Permission denied").__dict__
268
+
269
+ # 删除该会话下的所有对话
270
+ message_type = "GroupMessage" if session.is_group else "FriendMessage"
271
+ unified_msg_origin = f"{session.platform_id}:{message_type}:{session.platform_id}!{username}!{session_id}"
272
+ await self.conv_mgr.delete_conversations_by_user_id(unified_msg_origin)
273
+
274
+ # 删除消息历史
275
+ await self.platform_history_mgr.delete(
276
+ platform_id=session.platform_id,
277
+ user_id=session_id,
278
+ offset_sec=99999999,
279
+ )
280
+
281
+ # 删除与会话关联的配置路由
282
+ try:
283
+ await self.umop_config_router.delete_route(unified_msg_origin)
284
+ except ValueError as exc:
285
+ logger.warning(
286
+ "Failed to delete UMO route %s during session cleanup: %s",
287
+ unified_msg_origin,
288
+ exc,
289
+ )
290
+
291
+ # 清理队列(仅对 webchat)
292
+ if session.platform_id == "webchat":
293
+ webchat_queue_mgr.remove_queues(session_id)
294
+
295
+ # 删除会话
296
+ await self.db.delete_platform_session(session_id)
297
+
235
298
  return Response().ok().__dict__
236
299
 
237
- async def new_conversation(self):
300
+ async def new_session(self):
301
+ """Create a new Platform session (default: webchat)."""
238
302
  username = g.get("username", "guest")
239
- conversation_id = str(uuid.uuid4())
240
- self.db.new_conversation(username, conversation_id)
241
- return Response().ok(data={"conversation_id": conversation_id}).__dict__
242
303
 
243
- async def get_conversations(self):
244
- username = g.get("username", "guest")
245
- conversations = self.db.get_conversations(username)
246
- return Response().ok(data=conversations).__dict__
304
+ # 获取可选的 platform_id 参数,默认为 webchat
305
+ platform_id = request.args.get("platform_id", "webchat")
306
+
307
+ # 创建新会话
308
+ session = await self.db.create_platform_session(
309
+ creator=username,
310
+ platform_id=platform_id,
311
+ is_group=0,
312
+ )
313
+
314
+ return (
315
+ Response()
316
+ .ok(
317
+ data={
318
+ "session_id": session.session_id,
319
+ "platform_id": session.platform_id,
320
+ }
321
+ )
322
+ .__dict__
323
+ )
247
324
 
248
- async def get_conversation(self):
325
+ async def get_sessions(self):
326
+ """Get all Platform sessions for the current user."""
249
327
  username = g.get("username", "guest")
250
- conversation_id = request.args.get("conversation_id")
251
- if not conversation_id:
252
- return Response().error("Missing key: conversation_id").__dict__
253
328
 
254
- conversation = self.db.get_conversation_by_user_id(username, conversation_id)
329
+ # 获取可选的 platform_id 参数
330
+ platform_id = request.args.get("platform_id")
331
+
332
+ sessions = await self.db.get_platform_sessions_by_creator(
333
+ creator=username,
334
+ platform_id=platform_id,
335
+ page=1,
336
+ page_size=100, # 暂时返回前100个
337
+ )
338
+
339
+ # 转换为字典格式,并添加额外信息
340
+ sessions_data = []
341
+ for session in sessions:
342
+ sessions_data.append(
343
+ {
344
+ "session_id": session.session_id,
345
+ "platform_id": session.platform_id,
346
+ "creator": session.creator,
347
+ "display_name": session.display_name,
348
+ "is_group": session.is_group,
349
+ "created_at": session.created_at.astimezone().isoformat(),
350
+ "updated_at": session.updated_at.astimezone().isoformat(),
351
+ }
352
+ )
353
+
354
+ return Response().ok(data=sessions_data).__dict__
355
+
356
+ async def get_session(self):
357
+ """Get session information and message history by session_id."""
358
+ session_id = request.args.get("session_id")
359
+ if not session_id:
360
+ return Response().error("Missing key: session_id").__dict__
361
+
362
+ # 获取会话信息以确定 platform_id
363
+ session = await self.db.get_platform_session_by_id(session_id)
364
+ platform_id = session.platform_id if session else "webchat"
365
+
366
+ # Get platform message history using session_id
367
+ history_ls = await self.platform_history_mgr.get(
368
+ platform_id=platform_id,
369
+ user_id=session_id,
370
+ page=1,
371
+ page_size=1000,
372
+ )
373
+
374
+ history_res = [history.model_dump() for history in history_ls]
375
+
376
+ return (
377
+ Response()
378
+ .ok(
379
+ data={
380
+ "history": history_res,
381
+ "is_running": self.running_convs.get(session_id, False),
382
+ },
383
+ )
384
+ .__dict__
385
+ )
386
+
387
+ async def update_session_display_name(self):
388
+ """Update a Platform session's display name."""
389
+ post_data = await request.json
390
+
391
+ session_id = post_data.get("session_id")
392
+ display_name = post_data.get("display_name")
393
+
394
+ if not session_id:
395
+ return Response().error("Missing key: session_id").__dict__
396
+ if display_name is None:
397
+ return Response().error("Missing key: display_name").__dict__
398
+
399
+ username = g.get("username", "guest")
255
400
 
256
- self.curr_user_cid[username] = conversation_id
401
+ # 验证会话是否存在且属于当前用户
402
+ session = await self.db.get_platform_session_by_id(session_id)
403
+ if not session:
404
+ return Response().error(f"Session {session_id} not found").__dict__
405
+ if session.creator != username:
406
+ return Response().error("Permission denied").__dict__
407
+
408
+ # 更新 display_name
409
+ await self.db.update_platform_session(
410
+ session_id=session_id,
411
+ display_name=display_name,
412
+ )
257
413
 
258
- return Response().ok(data=conversation).__dict__
414
+ return Response().ok().__dict__