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.
- astrbot/api/__init__.py +16 -4
- astrbot/api/all.py +2 -1
- astrbot/api/event/__init__.py +5 -6
- astrbot/api/event/filter/__init__.py +37 -34
- astrbot/api/platform/__init__.py +7 -8
- astrbot/api/provider/__init__.py +8 -7
- astrbot/api/star/__init__.py +3 -4
- astrbot/api/util/__init__.py +2 -2
- astrbot/cli/__init__.py +1 -0
- astrbot/cli/__main__.py +18 -197
- astrbot/cli/commands/__init__.py +6 -0
- astrbot/cli/commands/cmd_conf.py +209 -0
- astrbot/cli/commands/cmd_init.py +56 -0
- astrbot/cli/commands/cmd_plug.py +245 -0
- astrbot/cli/commands/cmd_run.py +62 -0
- astrbot/cli/utils/__init__.py +18 -0
- astrbot/cli/utils/basic.py +76 -0
- astrbot/cli/utils/plugin.py +246 -0
- astrbot/cli/utils/version_comparator.py +90 -0
- astrbot/core/__init__.py +17 -19
- astrbot/core/agent/agent.py +14 -0
- astrbot/core/agent/handoff.py +38 -0
- astrbot/core/agent/hooks.py +30 -0
- astrbot/core/agent/mcp_client.py +385 -0
- astrbot/core/agent/message.py +175 -0
- astrbot/core/agent/response.py +14 -0
- astrbot/core/agent/run_context.py +22 -0
- astrbot/core/agent/runners/__init__.py +3 -0
- astrbot/core/agent/runners/base.py +65 -0
- astrbot/core/agent/runners/coze/coze_agent_runner.py +367 -0
- astrbot/core/agent/runners/coze/coze_api_client.py +324 -0
- astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py +403 -0
- astrbot/core/agent/runners/dify/dify_agent_runner.py +336 -0
- astrbot/core/agent/runners/dify/dify_api_client.py +195 -0
- astrbot/core/agent/runners/tool_loop_agent_runner.py +400 -0
- astrbot/core/agent/tool.py +285 -0
- astrbot/core/agent/tool_executor.py +17 -0
- astrbot/core/astr_agent_context.py +19 -0
- astrbot/core/astr_agent_hooks.py +36 -0
- astrbot/core/astr_agent_run_util.py +80 -0
- astrbot/core/astr_agent_tool_exec.py +246 -0
- astrbot/core/astrbot_config_mgr.py +275 -0
- astrbot/core/config/__init__.py +2 -2
- astrbot/core/config/astrbot_config.py +60 -20
- astrbot/core/config/default.py +1972 -453
- astrbot/core/config/i18n_utils.py +110 -0
- astrbot/core/conversation_mgr.py +285 -75
- astrbot/core/core_lifecycle.py +167 -62
- astrbot/core/db/__init__.py +305 -102
- astrbot/core/db/migration/helper.py +69 -0
- astrbot/core/db/migration/migra_3_to_4.py +357 -0
- astrbot/core/db/migration/migra_45_to_46.py +44 -0
- astrbot/core/db/migration/migra_webchat_session.py +131 -0
- astrbot/core/db/migration/shared_preferences_v3.py +48 -0
- astrbot/core/db/migration/sqlite_v3.py +497 -0
- astrbot/core/db/po.py +259 -55
- astrbot/core/db/sqlite.py +773 -528
- astrbot/core/db/vec_db/base.py +73 -0
- astrbot/core/db/vec_db/faiss_impl/__init__.py +3 -0
- astrbot/core/db/vec_db/faiss_impl/document_storage.py +392 -0
- astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +93 -0
- astrbot/core/db/vec_db/faiss_impl/sqlite_init.sql +17 -0
- astrbot/core/db/vec_db/faiss_impl/vec_db.py +204 -0
- astrbot/core/event_bus.py +26 -22
- astrbot/core/exceptions.py +9 -0
- astrbot/core/file_token_service.py +98 -0
- astrbot/core/initial_loader.py +19 -10
- astrbot/core/knowledge_base/chunking/__init__.py +9 -0
- astrbot/core/knowledge_base/chunking/base.py +25 -0
- astrbot/core/knowledge_base/chunking/fixed_size.py +59 -0
- astrbot/core/knowledge_base/chunking/recursive.py +161 -0
- astrbot/core/knowledge_base/kb_db_sqlite.py +301 -0
- astrbot/core/knowledge_base/kb_helper.py +642 -0
- astrbot/core/knowledge_base/kb_mgr.py +330 -0
- astrbot/core/knowledge_base/models.py +120 -0
- astrbot/core/knowledge_base/parsers/__init__.py +13 -0
- astrbot/core/knowledge_base/parsers/base.py +51 -0
- astrbot/core/knowledge_base/parsers/markitdown_parser.py +26 -0
- astrbot/core/knowledge_base/parsers/pdf_parser.py +101 -0
- astrbot/core/knowledge_base/parsers/text_parser.py +42 -0
- astrbot/core/knowledge_base/parsers/url_parser.py +103 -0
- astrbot/core/knowledge_base/parsers/util.py +13 -0
- astrbot/core/knowledge_base/prompts.py +65 -0
- astrbot/core/knowledge_base/retrieval/__init__.py +14 -0
- astrbot/core/knowledge_base/retrieval/hit_stopwords.txt +767 -0
- astrbot/core/knowledge_base/retrieval/manager.py +276 -0
- astrbot/core/knowledge_base/retrieval/rank_fusion.py +142 -0
- astrbot/core/knowledge_base/retrieval/sparse_retriever.py +136 -0
- astrbot/core/log.py +21 -15
- astrbot/core/message/components.py +413 -287
- astrbot/core/message/message_event_result.py +35 -24
- astrbot/core/persona_mgr.py +192 -0
- astrbot/core/pipeline/__init__.py +14 -14
- astrbot/core/pipeline/content_safety_check/stage.py +13 -9
- astrbot/core/pipeline/content_safety_check/strategies/__init__.py +1 -2
- astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py +13 -14
- astrbot/core/pipeline/content_safety_check/strategies/keywords.py +2 -1
- astrbot/core/pipeline/content_safety_check/strategies/strategy.py +6 -6
- astrbot/core/pipeline/context.py +7 -1
- astrbot/core/pipeline/context_utils.py +107 -0
- astrbot/core/pipeline/preprocess_stage/stage.py +63 -36
- astrbot/core/pipeline/process_stage/method/agent_request.py +48 -0
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +464 -0
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +202 -0
- astrbot/core/pipeline/process_stage/method/star_request.py +26 -32
- astrbot/core/pipeline/process_stage/stage.py +21 -15
- astrbot/core/pipeline/process_stage/utils.py +125 -0
- astrbot/core/pipeline/rate_limit_check/stage.py +34 -36
- astrbot/core/pipeline/respond/stage.py +142 -101
- astrbot/core/pipeline/result_decorate/stage.py +124 -57
- astrbot/core/pipeline/scheduler.py +21 -16
- astrbot/core/pipeline/session_status_check/stage.py +37 -0
- astrbot/core/pipeline/stage.py +11 -76
- astrbot/core/pipeline/waking_check/stage.py +69 -33
- astrbot/core/pipeline/whitelist_check/stage.py +10 -7
- astrbot/core/platform/__init__.py +6 -6
- astrbot/core/platform/astr_message_event.py +107 -129
- astrbot/core/platform/astrbot_message.py +32 -12
- astrbot/core/platform/manager.py +62 -18
- astrbot/core/platform/message_session.py +30 -0
- astrbot/core/platform/platform.py +16 -24
- astrbot/core/platform/platform_metadata.py +9 -4
- astrbot/core/platform/register.py +12 -7
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +136 -60
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +126 -46
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +63 -31
- astrbot/core/platform/sources/dingtalk/dingtalk_event.py +30 -26
- astrbot/core/platform/sources/discord/client.py +129 -0
- astrbot/core/platform/sources/discord/components.py +139 -0
- astrbot/core/platform/sources/discord/discord_platform_adapter.py +473 -0
- astrbot/core/platform/sources/discord/discord_platform_event.py +313 -0
- astrbot/core/platform/sources/lark/lark_adapter.py +27 -18
- astrbot/core/platform/sources/lark/lark_event.py +39 -13
- astrbot/core/platform/sources/misskey/misskey_adapter.py +770 -0
- astrbot/core/platform/sources/misskey/misskey_api.py +964 -0
- astrbot/core/platform/sources/misskey/misskey_event.py +163 -0
- astrbot/core/platform/sources/misskey/misskey_utils.py +550 -0
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +149 -33
- astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +41 -26
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -17
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_event.py +3 -1
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +14 -8
- astrbot/core/platform/sources/satori/satori_adapter.py +792 -0
- astrbot/core/platform/sources/satori/satori_event.py +432 -0
- astrbot/core/platform/sources/slack/client.py +164 -0
- astrbot/core/platform/sources/slack/slack_adapter.py +416 -0
- astrbot/core/platform/sources/slack/slack_event.py +253 -0
- astrbot/core/platform/sources/telegram/tg_adapter.py +100 -43
- astrbot/core/platform/sources/telegram/tg_event.py +136 -36
- astrbot/core/platform/sources/webchat/webchat_adapter.py +72 -22
- astrbot/core/platform/sources/webchat/webchat_event.py +46 -22
- astrbot/core/platform/sources/webchat/webchat_queue_mgr.py +35 -0
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +926 -0
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +178 -0
- astrbot/core/platform/sources/wechatpadpro/xml_data_parser.py +159 -0
- astrbot/core/platform/sources/wecom/wecom_adapter.py +169 -27
- astrbot/core/platform/sources/wecom/wecom_event.py +162 -77
- astrbot/core/platform/sources/wecom/wecom_kf.py +279 -0
- astrbot/core/platform/sources/wecom/wecom_kf_message.py +196 -0
- astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py +297 -0
- astrbot/core/platform/sources/wecom_ai_bot/__init__.py +15 -0
- astrbot/core/platform/sources/wecom_ai_bot/ierror.py +19 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +472 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py +417 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +152 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py +153 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +168 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_utils.py +209 -0
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +306 -0
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +186 -0
- astrbot/core/platform_message_history_mgr.py +49 -0
- astrbot/core/provider/__init__.py +2 -3
- astrbot/core/provider/entites.py +8 -8
- astrbot/core/provider/entities.py +154 -98
- astrbot/core/provider/func_tool_manager.py +446 -458
- astrbot/core/provider/manager.py +345 -207
- astrbot/core/provider/provider.py +188 -73
- astrbot/core/provider/register.py +9 -7
- astrbot/core/provider/sources/anthropic_source.py +295 -115
- astrbot/core/provider/sources/azure_tts_source.py +224 -0
- astrbot/core/provider/sources/bailian_rerank_source.py +236 -0
- astrbot/core/provider/sources/dashscope_tts.py +138 -14
- astrbot/core/provider/sources/edge_tts_source.py +24 -19
- astrbot/core/provider/sources/fishaudio_tts_api_source.py +58 -13
- astrbot/core/provider/sources/gemini_embedding_source.py +61 -0
- astrbot/core/provider/sources/gemini_source.py +310 -132
- astrbot/core/provider/sources/gemini_tts_source.py +81 -0
- astrbot/core/provider/sources/groq_source.py +15 -0
- astrbot/core/provider/sources/gsv_selfhosted_source.py +151 -0
- astrbot/core/provider/sources/gsvi_tts_source.py +14 -7
- astrbot/core/provider/sources/minimax_tts_api_source.py +159 -0
- astrbot/core/provider/sources/openai_embedding_source.py +40 -0
- astrbot/core/provider/sources/openai_source.py +241 -145
- astrbot/core/provider/sources/openai_tts_api_source.py +18 -7
- astrbot/core/provider/sources/sensevoice_selfhosted_source.py +13 -11
- astrbot/core/provider/sources/vllm_rerank_source.py +71 -0
- astrbot/core/provider/sources/volcengine_tts.py +115 -0
- astrbot/core/provider/sources/whisper_api_source.py +18 -13
- astrbot/core/provider/sources/whisper_selfhosted_source.py +19 -12
- astrbot/core/provider/sources/xinference_rerank_source.py +116 -0
- astrbot/core/provider/sources/xinference_stt_provider.py +197 -0
- astrbot/core/provider/sources/zhipu_source.py +6 -73
- astrbot/core/star/__init__.py +43 -11
- astrbot/core/star/config.py +17 -18
- astrbot/core/star/context.py +362 -138
- astrbot/core/star/filter/__init__.py +4 -3
- astrbot/core/star/filter/command.py +111 -35
- astrbot/core/star/filter/command_group.py +46 -34
- astrbot/core/star/filter/custom_filter.py +6 -5
- astrbot/core/star/filter/event_message_type.py +4 -2
- astrbot/core/star/filter/permission.py +4 -2
- astrbot/core/star/filter/platform_adapter_type.py +45 -12
- astrbot/core/star/filter/regex.py +4 -2
- astrbot/core/star/register/__init__.py +19 -15
- astrbot/core/star/register/star.py +41 -13
- astrbot/core/star/register/star_handler.py +236 -86
- astrbot/core/star/session_llm_manager.py +280 -0
- astrbot/core/star/session_plugin_manager.py +170 -0
- astrbot/core/star/star.py +36 -43
- astrbot/core/star/star_handler.py +47 -85
- astrbot/core/star/star_manager.py +442 -260
- astrbot/core/star/star_tools.py +167 -45
- astrbot/core/star/updator.py +17 -20
- astrbot/core/umop_config_router.py +106 -0
- astrbot/core/updator.py +38 -13
- astrbot/core/utils/astrbot_path.py +39 -0
- astrbot/core/utils/command_parser.py +1 -1
- astrbot/core/utils/io.py +119 -60
- astrbot/core/utils/log_pipe.py +1 -1
- astrbot/core/utils/metrics.py +11 -10
- astrbot/core/utils/migra_helper.py +73 -0
- astrbot/core/utils/path_util.py +63 -62
- astrbot/core/utils/pip_installer.py +37 -15
- astrbot/core/utils/session_lock.py +29 -0
- astrbot/core/utils/session_waiter.py +19 -20
- astrbot/core/utils/shared_preferences.py +174 -34
- astrbot/core/utils/t2i/__init__.py +4 -1
- astrbot/core/utils/t2i/local_strategy.py +386 -238
- astrbot/core/utils/t2i/network_strategy.py +109 -49
- astrbot/core/utils/t2i/renderer.py +29 -14
- astrbot/core/utils/t2i/template/astrbot_powershell.html +184 -0
- astrbot/core/utils/t2i/template_manager.py +111 -0
- astrbot/core/utils/tencent_record_helper.py +115 -1
- astrbot/core/utils/version_comparator.py +10 -13
- astrbot/core/zip_updator.py +112 -65
- astrbot/dashboard/routes/__init__.py +20 -13
- astrbot/dashboard/routes/auth.py +20 -9
- astrbot/dashboard/routes/chat.py +297 -141
- astrbot/dashboard/routes/config.py +652 -55
- astrbot/dashboard/routes/conversation.py +107 -37
- astrbot/dashboard/routes/file.py +26 -0
- astrbot/dashboard/routes/knowledge_base.py +1244 -0
- astrbot/dashboard/routes/log.py +27 -2
- astrbot/dashboard/routes/persona.py +202 -0
- astrbot/dashboard/routes/plugin.py +197 -139
- astrbot/dashboard/routes/route.py +27 -7
- astrbot/dashboard/routes/session_management.py +354 -0
- astrbot/dashboard/routes/stat.py +85 -18
- astrbot/dashboard/routes/static_file.py +5 -2
- astrbot/dashboard/routes/t2i.py +233 -0
- astrbot/dashboard/routes/tools.py +184 -120
- astrbot/dashboard/routes/update.py +59 -36
- astrbot/dashboard/server.py +96 -36
- astrbot/dashboard/utils.py +165 -0
- astrbot-4.7.0.dist-info/METADATA +294 -0
- astrbot-4.7.0.dist-info/RECORD +274 -0
- {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/WHEEL +1 -1
- astrbot/core/db/plugin/sqlite_impl.py +0 -112
- astrbot/core/db/sqlite_init.sql +0 -50
- astrbot/core/pipeline/platform_compatibility/stage.py +0 -56
- astrbot/core/pipeline/process_stage/method/llm_request.py +0 -606
- astrbot/core/platform/sources/gewechat/client.py +0 -806
- astrbot/core/platform/sources/gewechat/downloader.py +0 -55
- astrbot/core/platform/sources/gewechat/gewechat_event.py +0 -255
- astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py +0 -103
- astrbot/core/platform/sources/gewechat/xml_data_parser.py +0 -110
- astrbot/core/provider/sources/dashscope_source.py +0 -203
- astrbot/core/provider/sources/dify_source.py +0 -281
- astrbot/core/provider/sources/llmtuner_source.py +0 -132
- astrbot/core/rag/embedding/openai_source.py +0 -20
- astrbot/core/rag/knowledge_db_mgr.py +0 -94
- astrbot/core/rag/store/__init__.py +0 -9
- astrbot/core/rag/store/chroma_db.py +0 -42
- astrbot/core/utils/dify_api_client.py +0 -152
- astrbot-3.5.6.dist-info/METADATA +0 -249
- astrbot-3.5.6.dist-info/RECORD +0 -158
- {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/entry_points.txt +0 -0
- {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
"""企业微信智能机器人平台适配器
|
|
2
|
+
基于企业微信智能机器人 API 的消息平台适配器,支持 HTTP 回调
|
|
3
|
+
参考webchat_adapter.py的队列机制,实现异步消息处理和流式响应
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import base64
|
|
8
|
+
import hashlib
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
from collections.abc import Awaitable, Callable
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from astrbot.api import logger
|
|
15
|
+
from astrbot.api.event import MessageChain
|
|
16
|
+
from astrbot.api.message_components import At, Image, Plain
|
|
17
|
+
from astrbot.api.platform import (
|
|
18
|
+
AstrBotMessage,
|
|
19
|
+
MessageMember,
|
|
20
|
+
MessageType,
|
|
21
|
+
Platform,
|
|
22
|
+
PlatformMetadata,
|
|
23
|
+
)
|
|
24
|
+
from astrbot.core.platform.astr_message_event import MessageSesion
|
|
25
|
+
|
|
26
|
+
from ...register import register_platform_adapter
|
|
27
|
+
from .wecomai_api import (
|
|
28
|
+
WecomAIBotAPIClient,
|
|
29
|
+
WecomAIBotMessageParser,
|
|
30
|
+
WecomAIBotStreamMessageBuilder,
|
|
31
|
+
)
|
|
32
|
+
from .wecomai_event import WecomAIBotMessageEvent
|
|
33
|
+
from .wecomai_queue_mgr import WecomAIQueueMgr
|
|
34
|
+
from .wecomai_server import WecomAIBotServer
|
|
35
|
+
from .wecomai_utils import (
|
|
36
|
+
WecomAIBotConstants,
|
|
37
|
+
format_session_id,
|
|
38
|
+
generate_random_string,
|
|
39
|
+
process_encrypted_image,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class WecomAIQueueListener:
|
|
44
|
+
"""企业微信智能机器人队列监听器,参考webchat的QueueListener设计"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
queue_mgr: WecomAIQueueMgr,
|
|
49
|
+
callback: Callable[[dict], Awaitable[None]],
|
|
50
|
+
) -> None:
|
|
51
|
+
self.queue_mgr = queue_mgr
|
|
52
|
+
self.callback = callback
|
|
53
|
+
self.running_tasks = set()
|
|
54
|
+
|
|
55
|
+
async def listen_to_queue(self, session_id: str):
|
|
56
|
+
"""监听特定会话的队列"""
|
|
57
|
+
queue = self.queue_mgr.get_or_create_queue(session_id)
|
|
58
|
+
while True:
|
|
59
|
+
try:
|
|
60
|
+
data = await queue.get()
|
|
61
|
+
await self.callback(data)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.error(f"处理会话 {session_id} 消息时发生错误: {e}")
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
async def run(self):
|
|
67
|
+
"""监控新会话队列并启动监听器"""
|
|
68
|
+
monitored_sessions = set()
|
|
69
|
+
|
|
70
|
+
while True:
|
|
71
|
+
# 检查新会话
|
|
72
|
+
current_sessions = set(self.queue_mgr.queues.keys())
|
|
73
|
+
new_sessions = current_sessions - monitored_sessions
|
|
74
|
+
|
|
75
|
+
# 为新会话启动监听器
|
|
76
|
+
for session_id in new_sessions:
|
|
77
|
+
task = asyncio.create_task(self.listen_to_queue(session_id))
|
|
78
|
+
self.running_tasks.add(task)
|
|
79
|
+
task.add_done_callback(self.running_tasks.discard)
|
|
80
|
+
monitored_sessions.add(session_id)
|
|
81
|
+
logger.debug(f"[WecomAI] 为会话启动监听器: {session_id}")
|
|
82
|
+
|
|
83
|
+
# 清理已不存在的会话
|
|
84
|
+
removed_sessions = monitored_sessions - current_sessions
|
|
85
|
+
monitored_sessions -= removed_sessions
|
|
86
|
+
|
|
87
|
+
# 清理过期的待处理响应
|
|
88
|
+
self.queue_mgr.cleanup_expired_responses()
|
|
89
|
+
|
|
90
|
+
await asyncio.sleep(1) # 每秒检查一次新会话
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@register_platform_adapter(
|
|
94
|
+
"wecom_ai_bot",
|
|
95
|
+
"企业微信智能机器人适配器,支持 HTTP 回调接收消息",
|
|
96
|
+
)
|
|
97
|
+
class WecomAIBotAdapter(Platform):
|
|
98
|
+
"""企业微信智能机器人适配器"""
|
|
99
|
+
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
platform_config: dict,
|
|
103
|
+
platform_settings: dict,
|
|
104
|
+
event_queue: asyncio.Queue,
|
|
105
|
+
) -> None:
|
|
106
|
+
super().__init__(event_queue)
|
|
107
|
+
|
|
108
|
+
self.config = platform_config
|
|
109
|
+
self.settings = platform_settings
|
|
110
|
+
|
|
111
|
+
# 初始化配置参数
|
|
112
|
+
self.token = self.config["token"]
|
|
113
|
+
self.encoding_aes_key = self.config["encoding_aes_key"]
|
|
114
|
+
self.port = int(self.config["port"])
|
|
115
|
+
self.host = self.config.get("callback_server_host", "0.0.0.0")
|
|
116
|
+
self.bot_name = self.config.get("wecom_ai_bot_name", "")
|
|
117
|
+
self.initial_respond_text = self.config.get(
|
|
118
|
+
"wecomaibot_init_respond_text",
|
|
119
|
+
"💭 思考中...",
|
|
120
|
+
)
|
|
121
|
+
self.friend_message_welcome_text = self.config.get(
|
|
122
|
+
"wecomaibot_friend_message_welcome_text",
|
|
123
|
+
"",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# 平台元数据
|
|
127
|
+
self.metadata = PlatformMetadata(
|
|
128
|
+
name="wecom_ai_bot",
|
|
129
|
+
description="企业微信智能机器人适配器,支持 HTTP 回调接收消息",
|
|
130
|
+
id=self.config.get("id", "wecom_ai_bot"),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# 初始化 API 客户端
|
|
134
|
+
self.api_client = WecomAIBotAPIClient(self.token, self.encoding_aes_key)
|
|
135
|
+
|
|
136
|
+
# 初始化 HTTP 服务器
|
|
137
|
+
self.server = WecomAIBotServer(
|
|
138
|
+
host=self.host,
|
|
139
|
+
port=self.port,
|
|
140
|
+
api_client=self.api_client,
|
|
141
|
+
message_handler=self._process_message,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# 事件循环和关闭信号
|
|
145
|
+
self.shutdown_event = asyncio.Event()
|
|
146
|
+
|
|
147
|
+
# 队列管理器
|
|
148
|
+
self.queue_mgr = WecomAIQueueMgr()
|
|
149
|
+
|
|
150
|
+
# 队列监听器
|
|
151
|
+
self.queue_listener = WecomAIQueueListener(
|
|
152
|
+
self.queue_mgr,
|
|
153
|
+
self._handle_queued_message,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
async def _handle_queued_message(self, data: dict):
|
|
157
|
+
"""处理队列中的消息,类似webchat的callback"""
|
|
158
|
+
try:
|
|
159
|
+
abm = await self.convert_message(data)
|
|
160
|
+
await self.handle_msg(abm)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logger.error(f"处理队列消息时发生异常: {e}")
|
|
163
|
+
|
|
164
|
+
async def _process_message(
|
|
165
|
+
self,
|
|
166
|
+
message_data: dict[str, Any],
|
|
167
|
+
callback_params: dict[str, str],
|
|
168
|
+
) -> str | None:
|
|
169
|
+
"""处理接收到的消息
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
message_data: 解密后的消息数据
|
|
173
|
+
callback_params: 回调参数 (nonce, timestamp)
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
加密后的响应消息,无需响应时返回 None
|
|
177
|
+
|
|
178
|
+
"""
|
|
179
|
+
msgtype = message_data.get("msgtype")
|
|
180
|
+
if not msgtype:
|
|
181
|
+
logger.warning(f"消息类型未知,忽略: {message_data}")
|
|
182
|
+
return None
|
|
183
|
+
session_id = self._extract_session_id(message_data)
|
|
184
|
+
if msgtype in ("text", "image", "mixed"):
|
|
185
|
+
# user sent a text / image / mixed message
|
|
186
|
+
try:
|
|
187
|
+
# create a brand-new unique stream_id for this message session
|
|
188
|
+
stream_id = f"{session_id}_{generate_random_string(10)}"
|
|
189
|
+
await self._enqueue_message(
|
|
190
|
+
message_data,
|
|
191
|
+
callback_params,
|
|
192
|
+
stream_id,
|
|
193
|
+
session_id,
|
|
194
|
+
)
|
|
195
|
+
self.queue_mgr.set_pending_response(stream_id, callback_params)
|
|
196
|
+
|
|
197
|
+
resp = WecomAIBotStreamMessageBuilder.make_text_stream(
|
|
198
|
+
stream_id,
|
|
199
|
+
self.initial_respond_text,
|
|
200
|
+
False,
|
|
201
|
+
)
|
|
202
|
+
return await self.api_client.encrypt_message(
|
|
203
|
+
resp,
|
|
204
|
+
callback_params["nonce"],
|
|
205
|
+
callback_params["timestamp"],
|
|
206
|
+
)
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.error("处理消息时发生异常: %s", e)
|
|
209
|
+
return None
|
|
210
|
+
elif msgtype == "stream":
|
|
211
|
+
# wechat server is requesting for updates of a stream
|
|
212
|
+
stream_id = message_data["stream"]["id"]
|
|
213
|
+
if not self.queue_mgr.has_back_queue(stream_id):
|
|
214
|
+
logger.error(f"Cannot find back queue for stream_id: {stream_id}")
|
|
215
|
+
|
|
216
|
+
# 返回结束标志,告诉微信服务器流已结束
|
|
217
|
+
end_message = WecomAIBotStreamMessageBuilder.make_text_stream(
|
|
218
|
+
stream_id,
|
|
219
|
+
"",
|
|
220
|
+
True,
|
|
221
|
+
)
|
|
222
|
+
resp = await self.api_client.encrypt_message(
|
|
223
|
+
end_message,
|
|
224
|
+
callback_params["nonce"],
|
|
225
|
+
callback_params["timestamp"],
|
|
226
|
+
)
|
|
227
|
+
return resp
|
|
228
|
+
queue = self.queue_mgr.get_or_create_back_queue(stream_id)
|
|
229
|
+
if queue.empty():
|
|
230
|
+
logger.debug(
|
|
231
|
+
f"No new messages in back queue for stream_id: {stream_id}",
|
|
232
|
+
)
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
# aggregate all delta chains in the back queue
|
|
236
|
+
latest_plain_content = ""
|
|
237
|
+
image_base64 = []
|
|
238
|
+
finish = False
|
|
239
|
+
while not queue.empty():
|
|
240
|
+
msg = await queue.get()
|
|
241
|
+
if msg["type"] == "plain":
|
|
242
|
+
latest_plain_content = msg["data"] or ""
|
|
243
|
+
elif msg["type"] == "image":
|
|
244
|
+
image_base64.append(msg["image_data"])
|
|
245
|
+
elif msg["type"] == "end":
|
|
246
|
+
# stream end
|
|
247
|
+
finish = True
|
|
248
|
+
self.queue_mgr.remove_queues(stream_id)
|
|
249
|
+
break
|
|
250
|
+
|
|
251
|
+
logger.debug(
|
|
252
|
+
f"Aggregated content: {latest_plain_content}, image: {len(image_base64)}, finish: {finish}",
|
|
253
|
+
)
|
|
254
|
+
if latest_plain_content or image_base64:
|
|
255
|
+
msg_items = []
|
|
256
|
+
if finish and image_base64:
|
|
257
|
+
for img_b64 in image_base64:
|
|
258
|
+
# get md5 of image
|
|
259
|
+
img_data = base64.b64decode(img_b64)
|
|
260
|
+
img_md5 = hashlib.md5(img_data).hexdigest()
|
|
261
|
+
msg_items.append(
|
|
262
|
+
{
|
|
263
|
+
"msgtype": WecomAIBotConstants.MSG_TYPE_IMAGE,
|
|
264
|
+
"image": {"base64": img_b64, "md5": img_md5},
|
|
265
|
+
},
|
|
266
|
+
)
|
|
267
|
+
image_base64 = []
|
|
268
|
+
|
|
269
|
+
plain_message = WecomAIBotStreamMessageBuilder.make_mixed_stream(
|
|
270
|
+
stream_id,
|
|
271
|
+
latest_plain_content,
|
|
272
|
+
msg_items,
|
|
273
|
+
finish,
|
|
274
|
+
)
|
|
275
|
+
encrypted_message = await self.api_client.encrypt_message(
|
|
276
|
+
plain_message,
|
|
277
|
+
callback_params["nonce"],
|
|
278
|
+
callback_params["timestamp"],
|
|
279
|
+
)
|
|
280
|
+
if encrypted_message:
|
|
281
|
+
logger.debug(
|
|
282
|
+
f"Stream message sent successfully, stream_id: {stream_id}",
|
|
283
|
+
)
|
|
284
|
+
else:
|
|
285
|
+
logger.error("消息加密失败")
|
|
286
|
+
return encrypted_message
|
|
287
|
+
return None
|
|
288
|
+
elif msgtype == "event":
|
|
289
|
+
event = message_data.get("event")
|
|
290
|
+
if event == "enter_chat" and self.friend_message_welcome_text:
|
|
291
|
+
# 用户进入会话,发送欢迎消息
|
|
292
|
+
try:
|
|
293
|
+
resp = WecomAIBotStreamMessageBuilder.make_text(
|
|
294
|
+
self.friend_message_welcome_text,
|
|
295
|
+
)
|
|
296
|
+
return await self.api_client.encrypt_message(
|
|
297
|
+
resp,
|
|
298
|
+
callback_params["nonce"],
|
|
299
|
+
callback_params["timestamp"],
|
|
300
|
+
)
|
|
301
|
+
except Exception as e:
|
|
302
|
+
logger.error("处理欢迎消息时发生异常: %s", e)
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
def _extract_session_id(self, message_data: dict[str, Any]) -> str:
|
|
306
|
+
"""从消息数据中提取会话ID"""
|
|
307
|
+
user_id = message_data.get("from", {}).get("userid", "default_user")
|
|
308
|
+
return format_session_id("wecomai", user_id)
|
|
309
|
+
|
|
310
|
+
async def _enqueue_message(
|
|
311
|
+
self,
|
|
312
|
+
message_data: dict[str, Any],
|
|
313
|
+
callback_params: dict[str, str],
|
|
314
|
+
stream_id: str,
|
|
315
|
+
session_id: str,
|
|
316
|
+
):
|
|
317
|
+
"""将消息放入队列进行异步处理"""
|
|
318
|
+
input_queue = self.queue_mgr.get_or_create_queue(stream_id)
|
|
319
|
+
_ = self.queue_mgr.get_or_create_back_queue(stream_id)
|
|
320
|
+
message_payload = {
|
|
321
|
+
"message_data": message_data,
|
|
322
|
+
"callback_params": callback_params,
|
|
323
|
+
"session_id": session_id,
|
|
324
|
+
"stream_id": stream_id,
|
|
325
|
+
}
|
|
326
|
+
await input_queue.put(message_payload)
|
|
327
|
+
logger.debug(f"[WecomAI] 消息已入队: {stream_id}")
|
|
328
|
+
|
|
329
|
+
async def convert_message(self, payload: dict) -> AstrBotMessage:
|
|
330
|
+
"""转换队列中的消息数据为AstrBotMessage,类似webchat的convert_message"""
|
|
331
|
+
message_data = payload["message_data"]
|
|
332
|
+
session_id = payload["session_id"]
|
|
333
|
+
# callback_params = payload["callback_params"] # 保留但暂时不使用
|
|
334
|
+
|
|
335
|
+
# 解析消息内容
|
|
336
|
+
msgtype = message_data.get("msgtype")
|
|
337
|
+
content = ""
|
|
338
|
+
image_base64 = []
|
|
339
|
+
|
|
340
|
+
_img_url_to_process = []
|
|
341
|
+
msg_items = []
|
|
342
|
+
|
|
343
|
+
if msgtype == WecomAIBotConstants.MSG_TYPE_TEXT:
|
|
344
|
+
content = WecomAIBotMessageParser.parse_text_message(message_data)
|
|
345
|
+
elif msgtype == WecomAIBotConstants.MSG_TYPE_IMAGE:
|
|
346
|
+
_img_url_to_process.append(
|
|
347
|
+
WecomAIBotMessageParser.parse_image_message(message_data),
|
|
348
|
+
)
|
|
349
|
+
elif msgtype == WecomAIBotConstants.MSG_TYPE_MIXED:
|
|
350
|
+
# 提取混合消息中的文本内容
|
|
351
|
+
msg_items = WecomAIBotMessageParser.parse_mixed_message(message_data)
|
|
352
|
+
text_parts = []
|
|
353
|
+
for item in msg_items or []:
|
|
354
|
+
if item.get("msgtype") == WecomAIBotConstants.MSG_TYPE_TEXT:
|
|
355
|
+
text_content = item.get("text", {}).get("content", "")
|
|
356
|
+
if text_content:
|
|
357
|
+
text_parts.append(text_content)
|
|
358
|
+
elif item.get("msgtype") == WecomAIBotConstants.MSG_TYPE_IMAGE:
|
|
359
|
+
image_url = item.get("image", {}).get("url", "")
|
|
360
|
+
if image_url:
|
|
361
|
+
_img_url_to_process.append(image_url)
|
|
362
|
+
content = " ".join(text_parts) if text_parts else ""
|
|
363
|
+
else:
|
|
364
|
+
content = f"[{msgtype}消息]"
|
|
365
|
+
|
|
366
|
+
# 并行处理图片下载和解密
|
|
367
|
+
if _img_url_to_process:
|
|
368
|
+
tasks = [
|
|
369
|
+
process_encrypted_image(url, self.encoding_aes_key)
|
|
370
|
+
for url in _img_url_to_process
|
|
371
|
+
]
|
|
372
|
+
results = await asyncio.gather(*tasks)
|
|
373
|
+
for success, result in results:
|
|
374
|
+
if success:
|
|
375
|
+
image_base64.append(result)
|
|
376
|
+
else:
|
|
377
|
+
logger.error(f"处理加密图片失败: {result}")
|
|
378
|
+
|
|
379
|
+
# 构建 AstrBotMessage
|
|
380
|
+
abm = AstrBotMessage()
|
|
381
|
+
abm.self_id = self.bot_name
|
|
382
|
+
abm.message_str = content or "[未知消息]"
|
|
383
|
+
abm.message_id = str(uuid.uuid4())
|
|
384
|
+
abm.timestamp = int(time.time())
|
|
385
|
+
abm.raw_message = payload
|
|
386
|
+
|
|
387
|
+
# 发送者信息
|
|
388
|
+
abm.sender = MessageMember(
|
|
389
|
+
user_id=message_data.get("from", {}).get("userid", "unknown"),
|
|
390
|
+
nickname=message_data.get("from", {}).get("userid", "unknown"),
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# 消息类型
|
|
394
|
+
abm.type = (
|
|
395
|
+
MessageType.GROUP_MESSAGE
|
|
396
|
+
if message_data.get("chattype") == "group"
|
|
397
|
+
else MessageType.FRIEND_MESSAGE
|
|
398
|
+
)
|
|
399
|
+
abm.session_id = session_id
|
|
400
|
+
|
|
401
|
+
# 消息内容
|
|
402
|
+
abm.message = []
|
|
403
|
+
|
|
404
|
+
# 处理 At
|
|
405
|
+
if self.bot_name and f"@{self.bot_name}" in abm.message_str:
|
|
406
|
+
abm.message_str = abm.message_str.replace(f"@{self.bot_name}", "").strip()
|
|
407
|
+
abm.message.append(At(qq=self.bot_name, name=self.bot_name))
|
|
408
|
+
abm.message.append(Plain(abm.message_str))
|
|
409
|
+
if image_base64:
|
|
410
|
+
for img_b64 in image_base64:
|
|
411
|
+
abm.message.append(Image.fromBase64(img_b64))
|
|
412
|
+
|
|
413
|
+
logger.debug(f"WecomAIAdapter: {abm.message}")
|
|
414
|
+
return abm
|
|
415
|
+
|
|
416
|
+
async def send_by_session(
|
|
417
|
+
self,
|
|
418
|
+
session: MessageSesion,
|
|
419
|
+
message_chain: MessageChain,
|
|
420
|
+
):
|
|
421
|
+
"""通过会话发送消息"""
|
|
422
|
+
# 企业微信智能机器人主要通过回调响应,这里记录日志
|
|
423
|
+
logger.info("会话发送消息: %s -> %s", session.session_id, message_chain)
|
|
424
|
+
await super().send_by_session(session, message_chain)
|
|
425
|
+
|
|
426
|
+
def run(self) -> Awaitable[Any]:
|
|
427
|
+
"""运行适配器,同时启动HTTP服务器和队列监听器"""
|
|
428
|
+
logger.info("启动企业微信智能机器人适配器,监听 %s:%d", self.host, self.port)
|
|
429
|
+
|
|
430
|
+
async def run_both():
|
|
431
|
+
# 同时运行HTTP服务器和队列监听器
|
|
432
|
+
await asyncio.gather(
|
|
433
|
+
self.server.start_server(),
|
|
434
|
+
self.queue_listener.run(),
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
return run_both()
|
|
438
|
+
|
|
439
|
+
async def terminate(self):
|
|
440
|
+
"""终止适配器"""
|
|
441
|
+
logger.info("企业微信智能机器人适配器正在关闭...")
|
|
442
|
+
self.shutdown_event.set()
|
|
443
|
+
await self.server.shutdown()
|
|
444
|
+
|
|
445
|
+
def meta(self) -> PlatformMetadata:
|
|
446
|
+
"""获取平台元数据"""
|
|
447
|
+
return self.metadata
|
|
448
|
+
|
|
449
|
+
async def handle_msg(self, message: AstrBotMessage):
|
|
450
|
+
"""处理消息,创建消息事件并提交到事件队列"""
|
|
451
|
+
try:
|
|
452
|
+
message_event = WecomAIBotMessageEvent(
|
|
453
|
+
message_str=message.message_str,
|
|
454
|
+
message_obj=message,
|
|
455
|
+
platform_meta=self.meta(),
|
|
456
|
+
session_id=message.session_id,
|
|
457
|
+
api_client=self.api_client,
|
|
458
|
+
queue_mgr=self.queue_mgr,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
self.commit_event(message_event)
|
|
462
|
+
|
|
463
|
+
except Exception as e:
|
|
464
|
+
logger.error("处理消息时发生异常: %s", e)
|
|
465
|
+
|
|
466
|
+
def get_client(self) -> WecomAIBotAPIClient:
|
|
467
|
+
"""获取 API 客户端"""
|
|
468
|
+
return self.api_client
|
|
469
|
+
|
|
470
|
+
def get_server(self) -> WecomAIBotServer:
|
|
471
|
+
"""获取 HTTP 服务器实例"""
|
|
472
|
+
return self.server
|