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,367 @@
1
+ import base64
2
+ import json
3
+ import sys
4
+ import typing as T
5
+
6
+ import astrbot.core.message.components as Comp
7
+ from astrbot import logger
8
+ from astrbot.core import sp
9
+ from astrbot.core.message.message_event_result import MessageChain
10
+ from astrbot.core.provider.entities import (
11
+ LLMResponse,
12
+ ProviderRequest,
13
+ )
14
+
15
+ from ...hooks import BaseAgentRunHooks
16
+ from ...response import AgentResponseData
17
+ from ...run_context import ContextWrapper, TContext
18
+ from ..base import AgentResponse, AgentState, BaseAgentRunner
19
+ from .coze_api_client import CozeAPIClient
20
+
21
+ if sys.version_info >= (3, 12):
22
+ from typing import override
23
+ else:
24
+ from typing_extensions import override
25
+
26
+
27
+ class CozeAgentRunner(BaseAgentRunner[TContext]):
28
+ """Coze Agent Runner"""
29
+
30
+ @override
31
+ async def reset(
32
+ self,
33
+ request: ProviderRequest,
34
+ run_context: ContextWrapper[TContext],
35
+ agent_hooks: BaseAgentRunHooks[TContext],
36
+ provider_config: dict,
37
+ **kwargs: T.Any,
38
+ ) -> None:
39
+ self.req = request
40
+ self.streaming = kwargs.get("streaming", False)
41
+ self.final_llm_resp = None
42
+ self._state = AgentState.IDLE
43
+ self.agent_hooks = agent_hooks
44
+ self.run_context = run_context
45
+
46
+ self.api_key = provider_config.get("coze_api_key", "")
47
+ if not self.api_key:
48
+ raise Exception("Coze API Key 不能为空。")
49
+ self.bot_id = provider_config.get("bot_id", "")
50
+ if not self.bot_id:
51
+ raise Exception("Coze Bot ID 不能为空。")
52
+ self.api_base: str = provider_config.get("coze_api_base", "https://api.coze.cn")
53
+
54
+ if not isinstance(self.api_base, str) or not self.api_base.startswith(
55
+ ("http://", "https://"),
56
+ ):
57
+ raise Exception(
58
+ "Coze API Base URL 格式不正确,必须以 http:// 或 https:// 开头。",
59
+ )
60
+
61
+ self.timeout = provider_config.get("timeout", 120)
62
+ if isinstance(self.timeout, str):
63
+ self.timeout = int(self.timeout)
64
+ self.auto_save_history = provider_config.get("auto_save_history", True)
65
+
66
+ # 创建 API 客户端
67
+ self.api_client = CozeAPIClient(api_key=self.api_key, api_base=self.api_base)
68
+
69
+ # 会话相关缓存
70
+ self.file_id_cache: dict[str, dict[str, str]] = {}
71
+
72
+ @override
73
+ async def step(self):
74
+ """
75
+ 执行 Coze Agent 的一个步骤
76
+ """
77
+ if not self.req:
78
+ raise ValueError("Request is not set. Please call reset() first.")
79
+
80
+ if self._state == AgentState.IDLE:
81
+ try:
82
+ await self.agent_hooks.on_agent_begin(self.run_context)
83
+ except Exception as e:
84
+ logger.error(f"Error in on_agent_begin hook: {e}", exc_info=True)
85
+
86
+ # 开始处理,转换到运行状态
87
+ self._transition_state(AgentState.RUNNING)
88
+
89
+ try:
90
+ # 执行 Coze 请求并处理结果
91
+ async for response in self._execute_coze_request():
92
+ yield response
93
+ except Exception as e:
94
+ logger.error(f"Coze 请求失败:{str(e)}")
95
+ self._transition_state(AgentState.ERROR)
96
+ self.final_llm_resp = LLMResponse(
97
+ role="err", completion_text=f"Coze 请求失败:{str(e)}"
98
+ )
99
+ yield AgentResponse(
100
+ type="err",
101
+ data=AgentResponseData(
102
+ chain=MessageChain().message(f"Coze 请求失败:{str(e)}")
103
+ ),
104
+ )
105
+ finally:
106
+ await self.api_client.close()
107
+
108
+ @override
109
+ async def step_until_done(
110
+ self, max_step: int = 30
111
+ ) -> T.AsyncGenerator[AgentResponse, None]:
112
+ while not self.done():
113
+ async for resp in self.step():
114
+ yield resp
115
+
116
+ async def _execute_coze_request(self):
117
+ """执行 Coze 请求的核心逻辑"""
118
+ prompt = self.req.prompt or ""
119
+ session_id = self.req.session_id or "unknown"
120
+ image_urls = self.req.image_urls or []
121
+ contexts = self.req.contexts or []
122
+ system_prompt = self.req.system_prompt
123
+
124
+ # 用户ID参数
125
+ user_id = session_id
126
+
127
+ # 获取或创建会话ID
128
+ conversation_id = await sp.get_async(
129
+ scope="umo",
130
+ scope_id=user_id,
131
+ key="coze_conversation_id",
132
+ default="",
133
+ )
134
+
135
+ # 构建消息
136
+ additional_messages = []
137
+
138
+ if system_prompt:
139
+ if not self.auto_save_history or not conversation_id:
140
+ additional_messages.append(
141
+ {
142
+ "role": "system",
143
+ "content": system_prompt,
144
+ "content_type": "text",
145
+ },
146
+ )
147
+
148
+ # 处理历史上下文
149
+ if not self.auto_save_history and contexts:
150
+ for ctx in contexts:
151
+ if isinstance(ctx, dict) and "role" in ctx and "content" in ctx:
152
+ # 处理上下文中的图片
153
+ content = ctx["content"]
154
+ if isinstance(content, list):
155
+ # 多模态内容,需要处理图片
156
+ processed_content = []
157
+ for item in content:
158
+ if isinstance(item, dict):
159
+ if item.get("type") == "text":
160
+ processed_content.append(item)
161
+ elif item.get("type") == "image_url":
162
+ # 处理图片上传
163
+ try:
164
+ image_data = item.get("image_url", {})
165
+ url = image_data.get("url", "")
166
+ if url:
167
+ file_id = (
168
+ await self._download_and_upload_image(
169
+ url, session_id
170
+ )
171
+ )
172
+ processed_content.append(
173
+ {
174
+ "type": "file",
175
+ "file_id": file_id,
176
+ "file_url": url,
177
+ }
178
+ )
179
+ except Exception as e:
180
+ logger.warning(f"处理上下文图片失败: {e}")
181
+ continue
182
+
183
+ if processed_content:
184
+ additional_messages.append(
185
+ {
186
+ "role": ctx["role"],
187
+ "content": processed_content,
188
+ "content_type": "object_string",
189
+ }
190
+ )
191
+ else:
192
+ # 纯文本内容
193
+ additional_messages.append(
194
+ {
195
+ "role": ctx["role"],
196
+ "content": content,
197
+ "content_type": "text",
198
+ }
199
+ )
200
+
201
+ # 构建当前消息
202
+ if prompt or image_urls:
203
+ if image_urls:
204
+ # 多模态
205
+ object_string_content = []
206
+ if prompt:
207
+ object_string_content.append({"type": "text", "text": prompt})
208
+
209
+ for url in image_urls:
210
+ # the url is a base64 string
211
+ try:
212
+ image_data = base64.b64decode(url)
213
+ file_id = await self.api_client.upload_file(image_data)
214
+ object_string_content.append(
215
+ {
216
+ "type": "image",
217
+ "file_id": file_id,
218
+ }
219
+ )
220
+ except Exception as e:
221
+ logger.warning(f"处理图片失败 {url}: {e}")
222
+ continue
223
+
224
+ if object_string_content:
225
+ content = json.dumps(object_string_content, ensure_ascii=False)
226
+ additional_messages.append(
227
+ {
228
+ "role": "user",
229
+ "content": content,
230
+ "content_type": "object_string",
231
+ }
232
+ )
233
+ elif prompt:
234
+ # 纯文本
235
+ additional_messages.append(
236
+ {
237
+ "role": "user",
238
+ "content": prompt,
239
+ "content_type": "text",
240
+ },
241
+ )
242
+
243
+ # 执行 Coze API 请求
244
+ accumulated_content = ""
245
+ message_started = False
246
+
247
+ async for chunk in self.api_client.chat_messages(
248
+ bot_id=self.bot_id,
249
+ user_id=user_id,
250
+ additional_messages=additional_messages,
251
+ conversation_id=conversation_id,
252
+ auto_save_history=self.auto_save_history,
253
+ stream=True,
254
+ timeout=self.timeout,
255
+ ):
256
+ event_type = chunk.get("event")
257
+ data = chunk.get("data", {})
258
+
259
+ if event_type == "conversation.chat.created":
260
+ if isinstance(data, dict) and "conversation_id" in data:
261
+ await sp.put_async(
262
+ scope="umo",
263
+ scope_id=user_id,
264
+ key="coze_conversation_id",
265
+ value=data["conversation_id"],
266
+ )
267
+
268
+ if event_type == "conversation.message.delta":
269
+ # 增量消息
270
+ content = data.get("content", "")
271
+ if not content and "delta" in data:
272
+ content = data["delta"].get("content", "")
273
+ if not content and "text" in data:
274
+ content = data.get("text", "")
275
+
276
+ if content:
277
+ accumulated_content += content
278
+ message_started = True
279
+
280
+ # 如果是流式响应,发送增量数据
281
+ if self.streaming:
282
+ yield AgentResponse(
283
+ type="streaming_delta",
284
+ data=AgentResponseData(
285
+ chain=MessageChain().message(content)
286
+ ),
287
+ )
288
+
289
+ elif event_type == "conversation.message.completed":
290
+ # 消息完成
291
+ logger.debug("Coze message completed")
292
+ message_started = True
293
+
294
+ elif event_type == "conversation.chat.completed":
295
+ # 对话完成
296
+ logger.debug("Coze chat completed")
297
+ break
298
+
299
+ elif event_type == "error":
300
+ # 错误处理
301
+ error_msg = data.get("msg", "未知错误")
302
+ error_code = data.get("code", "UNKNOWN")
303
+ logger.error(f"Coze 出现错误: {error_code} - {error_msg}")
304
+ raise Exception(f"Coze 出现错误: {error_code} - {error_msg}")
305
+
306
+ if not message_started and not accumulated_content:
307
+ logger.warning("Coze 未返回任何内容")
308
+ accumulated_content = ""
309
+
310
+ # 创建最终响应
311
+ chain = MessageChain(chain=[Comp.Plain(accumulated_content)])
312
+ self.final_llm_resp = LLMResponse(role="assistant", result_chain=chain)
313
+ self._transition_state(AgentState.DONE)
314
+
315
+ try:
316
+ await self.agent_hooks.on_agent_done(self.run_context, self.final_llm_resp)
317
+ except Exception as e:
318
+ logger.error(f"Error in on_agent_done hook: {e}", exc_info=True)
319
+
320
+ # 返回最终结果
321
+ yield AgentResponse(
322
+ type="llm_result",
323
+ data=AgentResponseData(chain=chain),
324
+ )
325
+
326
+ async def _download_and_upload_image(
327
+ self,
328
+ image_url: str,
329
+ session_id: str | None = None,
330
+ ) -> str:
331
+ """下载图片并上传到 Coze,返回 file_id"""
332
+ import hashlib
333
+
334
+ # 计算哈希实现缓存
335
+ cache_key = hashlib.md5(image_url.encode("utf-8")).hexdigest()
336
+
337
+ if session_id:
338
+ if session_id not in self.file_id_cache:
339
+ self.file_id_cache[session_id] = {}
340
+
341
+ if cache_key in self.file_id_cache[session_id]:
342
+ file_id = self.file_id_cache[session_id][cache_key]
343
+ logger.debug(f"[Coze] 使用缓存的 file_id: {file_id}")
344
+ return file_id
345
+
346
+ try:
347
+ image_data = await self.api_client.download_image(image_url)
348
+ file_id = await self.api_client.upload_file(image_data)
349
+
350
+ if session_id:
351
+ self.file_id_cache[session_id][cache_key] = file_id
352
+ logger.debug(f"[Coze] 图片上传成功并缓存,file_id: {file_id}")
353
+
354
+ return file_id
355
+
356
+ except Exception as e:
357
+ logger.error(f"处理图片失败 {image_url}: {e!s}")
358
+ raise Exception(f"处理图片失败: {e!s}")
359
+
360
+ @override
361
+ def done(self) -> bool:
362
+ """检查 Agent 是否已完成工作"""
363
+ return self._state in (AgentState.DONE, AgentState.ERROR)
364
+
365
+ @override
366
+ def get_final_llm_resp(self) -> LLMResponse | None:
367
+ return self.final_llm_resp
@@ -0,0 +1,324 @@
1
+ import asyncio
2
+ import io
3
+ import json
4
+ from collections.abc import AsyncGenerator
5
+ from typing import Any
6
+
7
+ import aiohttp
8
+
9
+ from astrbot.core import logger
10
+
11
+
12
+ class CozeAPIClient:
13
+ def __init__(self, api_key: str, api_base: str = "https://api.coze.cn"):
14
+ self.api_key = api_key
15
+ self.api_base = api_base
16
+ self.session = None
17
+
18
+ async def _ensure_session(self):
19
+ """确保HTTP session存在"""
20
+ if self.session is None:
21
+ connector = aiohttp.TCPConnector(
22
+ ssl=False if self.api_base.startswith("http://") else True,
23
+ limit=100,
24
+ limit_per_host=30,
25
+ keepalive_timeout=30,
26
+ enable_cleanup_closed=True,
27
+ )
28
+ timeout = aiohttp.ClientTimeout(
29
+ total=120, # 默认超时时间
30
+ connect=30,
31
+ sock_read=120,
32
+ )
33
+ headers = {
34
+ "Authorization": f"Bearer {self.api_key}",
35
+ "Accept": "text/event-stream",
36
+ }
37
+ self.session = aiohttp.ClientSession(
38
+ headers=headers,
39
+ timeout=timeout,
40
+ connector=connector,
41
+ )
42
+ return self.session
43
+
44
+ async def upload_file(
45
+ self,
46
+ file_data: bytes,
47
+ ) -> str:
48
+ """上传文件到 Coze 并返回 file_id
49
+
50
+ Args:
51
+ file_data (bytes): 文件的二进制数据
52
+ Returns:
53
+ str: 上传成功后返回的 file_id
54
+
55
+ """
56
+ session = await self._ensure_session()
57
+ url = f"{self.api_base}/v1/files/upload"
58
+
59
+ try:
60
+ file_io = io.BytesIO(file_data)
61
+ async with session.post(
62
+ url,
63
+ data={
64
+ "file": file_io,
65
+ },
66
+ timeout=aiohttp.ClientTimeout(total=60),
67
+ ) as response:
68
+ if response.status == 401:
69
+ raise Exception("Coze API 认证失败,请检查 API Key 是否正确")
70
+
71
+ response_text = await response.text()
72
+ logger.debug(
73
+ f"文件上传响应状态: {response.status}, 内容: {response_text}",
74
+ )
75
+
76
+ if response.status != 200:
77
+ raise Exception(
78
+ f"文件上传失败,状态码: {response.status}, 响应: {response_text}",
79
+ )
80
+
81
+ try:
82
+ result = await response.json()
83
+ except json.JSONDecodeError:
84
+ raise Exception(f"文件上传响应解析失败: {response_text}")
85
+
86
+ if result.get("code") != 0:
87
+ raise Exception(f"文件上传失败: {result.get('msg', '未知错误')}")
88
+
89
+ file_id = result["data"]["id"]
90
+ logger.debug(f"[Coze] 图片上传成功,file_id: {file_id}")
91
+ return file_id
92
+
93
+ except asyncio.TimeoutError:
94
+ logger.error("文件上传超时")
95
+ raise Exception("文件上传超时")
96
+ except Exception as e:
97
+ logger.error(f"文件上传失败: {e!s}")
98
+ raise Exception(f"文件上传失败: {e!s}")
99
+
100
+ async def download_image(self, image_url: str) -> bytes:
101
+ """下载图片并返回字节数据
102
+
103
+ Args:
104
+ image_url (str): 图片的URL
105
+ Returns:
106
+ bytes: 图片的二进制数据
107
+
108
+ """
109
+ session = await self._ensure_session()
110
+
111
+ try:
112
+ async with session.get(image_url) as response:
113
+ if response.status != 200:
114
+ raise Exception(f"下载图片失败,状态码: {response.status}")
115
+
116
+ image_data = await response.read()
117
+ return image_data
118
+
119
+ except Exception as e:
120
+ logger.error(f"下载图片失败 {image_url}: {e!s}")
121
+ raise Exception(f"下载图片失败: {e!s}")
122
+
123
+ async def chat_messages(
124
+ self,
125
+ bot_id: str,
126
+ user_id: str,
127
+ additional_messages: list[dict] | None = None,
128
+ conversation_id: str | None = None,
129
+ auto_save_history: bool = True,
130
+ stream: bool = True,
131
+ timeout: float = 120,
132
+ ) -> AsyncGenerator[dict[str, Any], None]:
133
+ """发送聊天消息并返回流式响应
134
+
135
+ Args:
136
+ bot_id: Bot ID
137
+ user_id: 用户ID
138
+ additional_messages: 额外消息列表
139
+ conversation_id: 会话ID
140
+ auto_save_history: 是否自动保存历史
141
+ stream: 是否流式响应
142
+ timeout: 超时时间
143
+
144
+ """
145
+ session = await self._ensure_session()
146
+ url = f"{self.api_base}/v3/chat"
147
+
148
+ payload = {
149
+ "bot_id": bot_id,
150
+ "user_id": user_id,
151
+ "stream": stream,
152
+ "auto_save_history": auto_save_history,
153
+ }
154
+
155
+ if additional_messages:
156
+ payload["additional_messages"] = additional_messages
157
+
158
+ params = {}
159
+ if conversation_id:
160
+ params["conversation_id"] = conversation_id
161
+
162
+ logger.debug(f"Coze chat_messages payload: {payload}, params: {params}")
163
+
164
+ try:
165
+ async with session.post(
166
+ url,
167
+ json=payload,
168
+ params=params,
169
+ timeout=aiohttp.ClientTimeout(total=timeout),
170
+ ) as response:
171
+ if response.status == 401:
172
+ raise Exception("Coze API 认证失败,请检查 API Key 是否正确")
173
+
174
+ if response.status != 200:
175
+ raise Exception(f"Coze API 流式请求失败,状态码: {response.status}")
176
+
177
+ # SSE
178
+ buffer = ""
179
+ event_type = None
180
+ event_data = None
181
+
182
+ async for chunk in response.content:
183
+ if chunk:
184
+ buffer += chunk.decode("utf-8", errors="ignore")
185
+ lines = buffer.split("\n")
186
+ buffer = lines[-1]
187
+
188
+ for line in lines[:-1]:
189
+ line = line.strip()
190
+
191
+ if not line:
192
+ if event_type and event_data:
193
+ yield {"event": event_type, "data": event_data}
194
+ event_type = None
195
+ event_data = None
196
+ elif line.startswith("event:"):
197
+ event_type = line[6:].strip()
198
+ elif line.startswith("data:"):
199
+ data_str = line[5:].strip()
200
+ if data_str and data_str != "[DONE]":
201
+ try:
202
+ event_data = json.loads(data_str)
203
+ except json.JSONDecodeError:
204
+ event_data = {"content": data_str}
205
+
206
+ except asyncio.TimeoutError:
207
+ raise Exception(f"Coze API 流式请求超时 ({timeout}秒)")
208
+ except Exception as e:
209
+ raise Exception(f"Coze API 流式请求失败: {e!s}")
210
+
211
+ async def clear_context(self, conversation_id: str):
212
+ """清空会话上下文
213
+
214
+ Args:
215
+ conversation_id: 会话ID
216
+ Returns:
217
+ dict: API响应结果
218
+
219
+ """
220
+ session = await self._ensure_session()
221
+ url = f"{self.api_base}/v3/conversation/message/clear_context"
222
+ payload = {"conversation_id": conversation_id}
223
+
224
+ try:
225
+ async with session.post(url, json=payload) as response:
226
+ response_text = await response.text()
227
+
228
+ if response.status == 401:
229
+ raise Exception("Coze API 认证失败,请检查 API Key 是否正确")
230
+
231
+ if response.status != 200:
232
+ raise Exception(f"Coze API 请求失败,状态码: {response.status}")
233
+
234
+ try:
235
+ return json.loads(response_text)
236
+ except json.JSONDecodeError:
237
+ raise Exception("Coze API 返回非JSON格式")
238
+
239
+ except asyncio.TimeoutError:
240
+ raise Exception("Coze API 请求超时")
241
+ except aiohttp.ClientError as e:
242
+ raise Exception(f"Coze API 请求失败: {e!s}")
243
+
244
+ async def get_message_list(
245
+ self,
246
+ conversation_id: str,
247
+ order: str = "desc",
248
+ limit: int = 10,
249
+ offset: int = 0,
250
+ ):
251
+ """获取消息列表
252
+
253
+ Args:
254
+ conversation_id: 会话ID
255
+ order: 排序方式 (asc/desc)
256
+ limit: 限制数量
257
+ offset: 偏移量
258
+ Returns:
259
+ dict: API响应结果
260
+
261
+ """
262
+ session = await self._ensure_session()
263
+ url = f"{self.api_base}/v3/conversation/message/list"
264
+ params = {
265
+ "conversation_id": conversation_id,
266
+ "order": order,
267
+ "limit": limit,
268
+ "offset": offset,
269
+ }
270
+
271
+ try:
272
+ async with session.get(url, params=params) as response:
273
+ response.raise_for_status()
274
+ return await response.json()
275
+
276
+ except Exception as e:
277
+ logger.error(f"获取Coze消息列表失败: {e!s}")
278
+ raise Exception(f"获取Coze消息列表失败: {e!s}")
279
+
280
+ async def close(self):
281
+ """关闭会话"""
282
+ if self.session:
283
+ await self.session.close()
284
+ self.session = None
285
+
286
+
287
+ if __name__ == "__main__":
288
+ import asyncio
289
+ import os
290
+
291
+ async def test_coze_api_client():
292
+ api_key = os.getenv("COZE_API_KEY", "")
293
+ bot_id = os.getenv("COZE_BOT_ID", "")
294
+ client = CozeAPIClient(api_key=api_key)
295
+
296
+ try:
297
+ with open("README.md", "rb") as f:
298
+ file_data = f.read()
299
+ file_id = await client.upload_file(file_data)
300
+ print(f"Uploaded file_id: {file_id}")
301
+ async for event in client.chat_messages(
302
+ bot_id=bot_id,
303
+ user_id="test_user",
304
+ additional_messages=[
305
+ {
306
+ "role": "user",
307
+ "content": json.dumps(
308
+ [
309
+ {"type": "text", "text": "这是什么"},
310
+ {"type": "file", "file_id": file_id},
311
+ ],
312
+ ensure_ascii=False,
313
+ ),
314
+ "content_type": "object_string",
315
+ },
316
+ ],
317
+ stream=True,
318
+ ):
319
+ print(f"Event: {event}")
320
+
321
+ finally:
322
+ await client.close()
323
+
324
+ asyncio.run(test_coze_api_client())