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
@@ -0,0 +1,336 @@
1
+ import base64
2
+ import os
3
+ import sys
4
+ import typing as T
5
+
6
+ import astrbot.core.message.components as Comp
7
+ from astrbot.core import logger, sp
8
+ from astrbot.core.message.message_event_result import MessageChain
9
+ from astrbot.core.provider.entities import (
10
+ LLMResponse,
11
+ ProviderRequest,
12
+ )
13
+ from astrbot.core.utils.astrbot_path import get_astrbot_data_path
14
+ from astrbot.core.utils.io import download_file
15
+
16
+ from ...hooks import BaseAgentRunHooks
17
+ from ...response import AgentResponseData
18
+ from ...run_context import ContextWrapper, TContext
19
+ from ..base import AgentResponse, AgentState, BaseAgentRunner
20
+ from .dify_api_client import DifyAPIClient
21
+
22
+ if sys.version_info >= (3, 12):
23
+ from typing import override
24
+ else:
25
+ from typing_extensions import override
26
+
27
+
28
+ class DifyAgentRunner(BaseAgentRunner[TContext]):
29
+ """Dify Agent Runner"""
30
+
31
+ @override
32
+ async def reset(
33
+ self,
34
+ request: ProviderRequest,
35
+ run_context: ContextWrapper[TContext],
36
+ agent_hooks: BaseAgentRunHooks[TContext],
37
+ provider_config: dict,
38
+ **kwargs: T.Any,
39
+ ) -> None:
40
+ self.req = request
41
+ self.streaming = kwargs.get("streaming", False)
42
+ self.final_llm_resp = None
43
+ self._state = AgentState.IDLE
44
+ self.agent_hooks = agent_hooks
45
+ self.run_context = run_context
46
+
47
+ self.api_key = provider_config.get("dify_api_key", "")
48
+ self.api_base = provider_config.get("dify_api_base", "https://api.dify.ai/v1")
49
+ self.api_type = provider_config.get("dify_api_type", "chat")
50
+ self.workflow_output_key = provider_config.get(
51
+ "dify_workflow_output_key",
52
+ "astrbot_wf_output",
53
+ )
54
+ self.dify_query_input_key = provider_config.get(
55
+ "dify_query_input_key",
56
+ "astrbot_text_query",
57
+ )
58
+ self.variables: dict = provider_config.get("variables", {}) or {}
59
+ self.timeout = provider_config.get("timeout", 60)
60
+ if isinstance(self.timeout, str):
61
+ self.timeout = int(self.timeout)
62
+
63
+ self.api_client = DifyAPIClient(self.api_key, self.api_base)
64
+
65
+ @override
66
+ async def step(self):
67
+ """
68
+ 执行 Dify Agent 的一个步骤
69
+ """
70
+ if not self.req:
71
+ raise ValueError("Request is not set. Please call reset() first.")
72
+
73
+ if self._state == AgentState.IDLE:
74
+ try:
75
+ await self.agent_hooks.on_agent_begin(self.run_context)
76
+ except Exception as e:
77
+ logger.error(f"Error in on_agent_begin hook: {e}", exc_info=True)
78
+
79
+ # 开始处理,转换到运行状态
80
+ self._transition_state(AgentState.RUNNING)
81
+
82
+ try:
83
+ # 执行 Dify 请求并处理结果
84
+ async for response in self._execute_dify_request():
85
+ yield response
86
+ except Exception as e:
87
+ logger.error(f"Dify 请求失败:{str(e)}")
88
+ self._transition_state(AgentState.ERROR)
89
+ self.final_llm_resp = LLMResponse(
90
+ role="err", completion_text=f"Dify 请求失败:{str(e)}"
91
+ )
92
+ yield AgentResponse(
93
+ type="err",
94
+ data=AgentResponseData(
95
+ chain=MessageChain().message(f"Dify 请求失败:{str(e)}")
96
+ ),
97
+ )
98
+ finally:
99
+ await self.api_client.close()
100
+
101
+ @override
102
+ async def step_until_done(
103
+ self, max_step: int = 30
104
+ ) -> T.AsyncGenerator[AgentResponse, None]:
105
+ while not self.done():
106
+ async for resp in self.step():
107
+ yield resp
108
+
109
+ async def _execute_dify_request(self):
110
+ """执行 Dify 请求的核心逻辑"""
111
+ prompt = self.req.prompt or ""
112
+ session_id = self.req.session_id or "unknown"
113
+ image_urls = self.req.image_urls or []
114
+ system_prompt = self.req.system_prompt
115
+
116
+ conversation_id = await sp.get_async(
117
+ scope="umo",
118
+ scope_id=session_id,
119
+ key="dify_conversation_id",
120
+ default="",
121
+ )
122
+ result = ""
123
+
124
+ # 处理图片上传
125
+ files_payload = []
126
+ for image_url in image_urls:
127
+ # image_url is a base64 string
128
+ try:
129
+ image_data = base64.b64decode(image_url)
130
+ file_response = await self.api_client.file_upload(
131
+ file_data=image_data,
132
+ user=session_id,
133
+ mime_type="image/png",
134
+ file_name="image.png",
135
+ )
136
+ logger.debug(f"Dify 上传图片响应:{file_response}")
137
+ if "id" not in file_response:
138
+ logger.warning(
139
+ f"上传图片后得到未知的 Dify 响应:{file_response},图片将忽略。"
140
+ )
141
+ continue
142
+ files_payload.append(
143
+ {
144
+ "type": "image",
145
+ "transfer_method": "local_file",
146
+ "upload_file_id": file_response["id"],
147
+ }
148
+ )
149
+ except Exception as e:
150
+ logger.warning(f"上传图片失败:{e}")
151
+ continue
152
+
153
+ # 获得会话变量
154
+ payload_vars = self.variables.copy()
155
+ # 动态变量
156
+ session_var = await sp.get_async(
157
+ scope="umo",
158
+ scope_id=session_id,
159
+ key="session_variables",
160
+ default={},
161
+ )
162
+ payload_vars.update(session_var)
163
+ payload_vars["system_prompt"] = system_prompt
164
+
165
+ # 处理不同的 API 类型
166
+ match self.api_type:
167
+ case "chat" | "agent" | "chatflow":
168
+ if not prompt:
169
+ prompt = "请描述这张图片。"
170
+
171
+ async for chunk in self.api_client.chat_messages(
172
+ inputs={
173
+ **payload_vars,
174
+ },
175
+ query=prompt,
176
+ user=session_id,
177
+ conversation_id=conversation_id,
178
+ files=files_payload,
179
+ timeout=self.timeout,
180
+ ):
181
+ logger.debug(f"dify resp chunk: {chunk}")
182
+ if chunk["event"] == "message" or chunk["event"] == "agent_message":
183
+ result += chunk["answer"]
184
+ if not conversation_id:
185
+ await sp.put_async(
186
+ scope="umo",
187
+ scope_id=session_id,
188
+ key="dify_conversation_id",
189
+ value=chunk["conversation_id"],
190
+ )
191
+ conversation_id = chunk["conversation_id"]
192
+
193
+ # 如果是流式响应,发送增量数据
194
+ if self.streaming and chunk["answer"]:
195
+ yield AgentResponse(
196
+ type="streaming_delta",
197
+ data=AgentResponseData(
198
+ chain=MessageChain().message(chunk["answer"])
199
+ ),
200
+ )
201
+ elif chunk["event"] == "message_end":
202
+ logger.debug("Dify message end")
203
+ break
204
+ elif chunk["event"] == "error":
205
+ logger.error(f"Dify 出现错误:{chunk}")
206
+ raise Exception(
207
+ f"Dify 出现错误 status: {chunk['status']} message: {chunk['message']}"
208
+ )
209
+
210
+ case "workflow":
211
+ async for chunk in self.api_client.workflow_run(
212
+ inputs={
213
+ self.dify_query_input_key: prompt,
214
+ "astrbot_session_id": session_id,
215
+ **payload_vars,
216
+ },
217
+ user=session_id,
218
+ files=files_payload,
219
+ timeout=self.timeout,
220
+ ):
221
+ logger.debug(f"dify workflow resp chunk: {chunk}")
222
+ match chunk["event"]:
223
+ case "workflow_started":
224
+ logger.info(
225
+ f"Dify 工作流(ID: {chunk['workflow_run_id']})开始运行。"
226
+ )
227
+ case "node_finished":
228
+ logger.debug(
229
+ f"Dify 工作流节点(ID: {chunk['data']['node_id']} Title: {chunk['data'].get('title', '')})运行结束。"
230
+ )
231
+ case "text_chunk":
232
+ if self.streaming and chunk["data"]["text"]:
233
+ yield AgentResponse(
234
+ type="streaming_delta",
235
+ data=AgentResponseData(
236
+ chain=MessageChain().message(
237
+ chunk["data"]["text"]
238
+ )
239
+ ),
240
+ )
241
+ case "workflow_finished":
242
+ logger.info(
243
+ f"Dify 工作流(ID: {chunk['workflow_run_id']})运行结束"
244
+ )
245
+ logger.debug(f"Dify 工作流结果:{chunk}")
246
+ if chunk["data"]["error"]:
247
+ logger.error(
248
+ f"Dify 工作流出现错误:{chunk['data']['error']}"
249
+ )
250
+ raise Exception(
251
+ f"Dify 工作流出现错误:{chunk['data']['error']}"
252
+ )
253
+ if self.workflow_output_key not in chunk["data"]["outputs"]:
254
+ raise Exception(
255
+ f"Dify 工作流的输出不包含指定的键名:{self.workflow_output_key}"
256
+ )
257
+ result = chunk
258
+ case _:
259
+ raise Exception(f"未知的 Dify API 类型:{self.api_type}")
260
+
261
+ if not result:
262
+ logger.warning("Dify 请求结果为空,请查看 Debug 日志。")
263
+
264
+ # 解析结果
265
+ chain = await self.parse_dify_result(result)
266
+
267
+ # 创建最终响应
268
+ self.final_llm_resp = LLMResponse(role="assistant", result_chain=chain)
269
+ self._transition_state(AgentState.DONE)
270
+
271
+ try:
272
+ await self.agent_hooks.on_agent_done(self.run_context, self.final_llm_resp)
273
+ except Exception as e:
274
+ logger.error(f"Error in on_agent_done hook: {e}", exc_info=True)
275
+
276
+ # 返回最终结果
277
+ yield AgentResponse(
278
+ type="llm_result",
279
+ data=AgentResponseData(chain=chain),
280
+ )
281
+
282
+ async def parse_dify_result(self, chunk: dict | str) -> MessageChain:
283
+ """解析 Dify 的响应结果"""
284
+ if isinstance(chunk, str):
285
+ # Chat
286
+ return MessageChain(chain=[Comp.Plain(chunk)])
287
+
288
+ async def parse_file(item: dict):
289
+ match item["type"]:
290
+ case "image":
291
+ return Comp.Image(file=item["url"], url=item["url"])
292
+ case "audio":
293
+ # 仅支持 wav
294
+ temp_dir = os.path.join(get_astrbot_data_path(), "temp")
295
+ path = os.path.join(temp_dir, f"{item['filename']}.wav")
296
+ await download_file(item["url"], path)
297
+ return Comp.Image(file=item["url"], url=item["url"])
298
+ case "video":
299
+ return Comp.Video(file=item["url"])
300
+ case _:
301
+ return Comp.File(name=item["filename"], file=item["url"])
302
+
303
+ output = chunk["data"]["outputs"][self.workflow_output_key]
304
+ chains = []
305
+ if isinstance(output, str):
306
+ # 纯文本输出
307
+ chains.append(Comp.Plain(output))
308
+ elif isinstance(output, list):
309
+ # 主要适配 Dify 的 HTTP 请求结点的多模态输出
310
+ for item in output:
311
+ # handle Array[File]
312
+ if (
313
+ not isinstance(item, dict)
314
+ or item.get("dify_model_identity", "") != "__dify__file__"
315
+ ):
316
+ chains.append(Comp.Plain(str(output)))
317
+ break
318
+ else:
319
+ chains.append(Comp.Plain(str(output)))
320
+
321
+ # scan file
322
+ files = chunk["data"].get("files", [])
323
+ for item in files:
324
+ comp = await parse_file(item)
325
+ chains.append(comp)
326
+
327
+ return MessageChain(chain=chains)
328
+
329
+ @override
330
+ def done(self) -> bool:
331
+ """检查 Agent 是否已完成工作"""
332
+ return self._state in (AgentState.DONE, AgentState.ERROR)
333
+
334
+ @override
335
+ def get_final_llm_resp(self) -> LLMResponse | None:
336
+ return self.final_llm_resp
@@ -0,0 +1,195 @@
1
+ import codecs
2
+ import json
3
+ from collections.abc import AsyncGenerator
4
+ from typing import Any
5
+
6
+ from aiohttp import ClientResponse, ClientSession, FormData
7
+
8
+ from astrbot.core import logger
9
+
10
+
11
+ async def _stream_sse(resp: ClientResponse) -> AsyncGenerator[dict, None]:
12
+ decoder = codecs.getincrementaldecoder("utf-8")()
13
+ buffer = ""
14
+ async for chunk in resp.content.iter_chunked(8192):
15
+ buffer += decoder.decode(chunk)
16
+ while "\n\n" in buffer:
17
+ block, buffer = buffer.split("\n\n", 1)
18
+ if block.strip().startswith("data:"):
19
+ try:
20
+ yield json.loads(block[5:])
21
+ except json.JSONDecodeError:
22
+ logger.warning(f"Drop invalid dify json data: {block[5:]}")
23
+ continue
24
+ # flush any remaining text
25
+ buffer += decoder.decode(b"", final=True)
26
+ if buffer.strip().startswith("data:"):
27
+ try:
28
+ yield json.loads(buffer[5:])
29
+ except json.JSONDecodeError:
30
+ logger.warning(f"Drop invalid dify json data: {buffer[5:]}")
31
+
32
+
33
+ class DifyAPIClient:
34
+ def __init__(self, api_key: str, api_base: str = "https://api.dify.ai/v1"):
35
+ self.api_key = api_key
36
+ self.api_base = api_base
37
+ self.session = ClientSession(trust_env=True)
38
+ self.headers = {
39
+ "Authorization": f"Bearer {self.api_key}",
40
+ }
41
+
42
+ async def chat_messages(
43
+ self,
44
+ inputs: dict,
45
+ query: str,
46
+ user: str,
47
+ response_mode: str = "streaming",
48
+ conversation_id: str = "",
49
+ files: list[dict[str, Any]] | None = None,
50
+ timeout: float = 60,
51
+ ) -> AsyncGenerator[dict[str, Any], None]:
52
+ if files is None:
53
+ files = []
54
+ url = f"{self.api_base}/chat-messages"
55
+ payload = locals()
56
+ payload.pop("self")
57
+ payload.pop("timeout")
58
+ logger.info(f"chat_messages payload: {payload}")
59
+ async with self.session.post(
60
+ url,
61
+ json=payload,
62
+ headers=self.headers,
63
+ timeout=timeout,
64
+ ) as resp:
65
+ if resp.status != 200:
66
+ text = await resp.text()
67
+ raise Exception(
68
+ f"Dify /chat-messages 接口请求失败:{resp.status}. {text}",
69
+ )
70
+ async for event in _stream_sse(resp):
71
+ yield event
72
+
73
+ async def workflow_run(
74
+ self,
75
+ inputs: dict,
76
+ user: str,
77
+ response_mode: str = "streaming",
78
+ files: list[dict[str, Any]] | None = None,
79
+ timeout: float = 60,
80
+ ):
81
+ if files is None:
82
+ files = []
83
+ url = f"{self.api_base}/workflows/run"
84
+ payload = locals()
85
+ payload.pop("self")
86
+ payload.pop("timeout")
87
+ logger.info(f"workflow_run payload: {payload}")
88
+ async with self.session.post(
89
+ url,
90
+ json=payload,
91
+ headers=self.headers,
92
+ timeout=timeout,
93
+ ) as resp:
94
+ if resp.status != 200:
95
+ text = await resp.text()
96
+ raise Exception(
97
+ f"Dify /workflows/run 接口请求失败:{resp.status}. {text}",
98
+ )
99
+ async for event in _stream_sse(resp):
100
+ yield event
101
+
102
+ async def file_upload(
103
+ self,
104
+ user: str,
105
+ file_path: str | None = None,
106
+ file_data: bytes | None = None,
107
+ file_name: str | None = None,
108
+ mime_type: str | None = None,
109
+ ) -> dict[str, Any]:
110
+ """Upload a file to Dify. Must provide either file_path or file_data.
111
+
112
+ Args:
113
+ user: The user ID.
114
+ file_path: The path to the file to upload.
115
+ file_data: The file data in bytes.
116
+ file_name: Optional file name when using file_data.
117
+ Returns:
118
+ A dictionary containing the uploaded file information.
119
+ """
120
+ url = f"{self.api_base}/files/upload"
121
+
122
+ form = FormData()
123
+ form.add_field("user", user)
124
+
125
+ if file_data is not None:
126
+ # 使用 bytes 数据
127
+ form.add_field(
128
+ "file",
129
+ file_data,
130
+ filename=file_name or "uploaded_file",
131
+ content_type=mime_type or "application/octet-stream",
132
+ )
133
+ elif file_path is not None:
134
+ # 使用文件路径
135
+ import os
136
+
137
+ with open(file_path, "rb") as f:
138
+ file_content = f.read()
139
+ form.add_field(
140
+ "file",
141
+ file_content,
142
+ filename=os.path.basename(file_path),
143
+ content_type=mime_type or "application/octet-stream",
144
+ )
145
+ else:
146
+ raise ValueError("file_path 和 file_data 不能同时为 None")
147
+
148
+ async with self.session.post(
149
+ url,
150
+ data=form,
151
+ headers=self.headers, # 不包含 Content-Type,让 aiohttp 自动设置
152
+ ) as resp:
153
+ if resp.status != 200 and resp.status != 201:
154
+ text = await resp.text()
155
+ raise Exception(f"Dify 文件上传失败:{resp.status}. {text}")
156
+ return await resp.json() # {"id": "xxx", ...}
157
+
158
+ async def close(self):
159
+ await self.session.close()
160
+
161
+ async def get_chat_convs(self, user: str, limit: int = 20):
162
+ # conversations. GET
163
+ url = f"{self.api_base}/conversations"
164
+ payload = {
165
+ "user": user,
166
+ "limit": limit,
167
+ }
168
+ async with self.session.get(url, params=payload, headers=self.headers) as resp:
169
+ return await resp.json()
170
+
171
+ async def delete_chat_conv(self, user: str, conversation_id: str):
172
+ # conversation. DELETE
173
+ url = f"{self.api_base}/conversations/{conversation_id}"
174
+ payload = {
175
+ "user": user,
176
+ }
177
+ async with self.session.delete(url, json=payload, headers=self.headers) as resp:
178
+ return await resp.json()
179
+
180
+ async def rename(
181
+ self,
182
+ conversation_id: str,
183
+ name: str,
184
+ user: str,
185
+ auto_generate: bool = False,
186
+ ):
187
+ # /conversations/:conversation_id/name
188
+ url = f"{self.api_base}/conversations/{conversation_id}/name"
189
+ payload = {
190
+ "user": user,
191
+ "name": name,
192
+ "auto_generate": auto_generate,
193
+ }
194
+ async with self.session.post(url, json=payload, headers=self.headers) as resp:
195
+ return await resp.json()