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,178 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import base64
|
|
3
|
+
import io
|
|
4
|
+
from collections.abc import AsyncGenerator
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import aiohttp
|
|
8
|
+
from PIL import Image as PILImage # 使用别名避免冲突
|
|
9
|
+
|
|
10
|
+
from astrbot import logger
|
|
11
|
+
from astrbot.core.message.components import (
|
|
12
|
+
Image,
|
|
13
|
+
Plain,
|
|
14
|
+
Record,
|
|
15
|
+
WechatEmoji,
|
|
16
|
+
) # Import Image
|
|
17
|
+
from astrbot.core.message.message_event_result import MessageChain
|
|
18
|
+
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
|
19
|
+
from astrbot.core.platform.astrbot_message import AstrBotMessage, MessageType
|
|
20
|
+
from astrbot.core.platform.platform_metadata import PlatformMetadata
|
|
21
|
+
from astrbot.core.utils.tencent_record_helper import audio_to_tencent_silk_base64
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from .wechatpadpro_adapter import WeChatPadProAdapter
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class WeChatPadProMessageEvent(AstrMessageEvent):
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
message_str: str,
|
|
31
|
+
message_obj: AstrBotMessage,
|
|
32
|
+
platform_meta: PlatformMetadata,
|
|
33
|
+
session_id: str,
|
|
34
|
+
adapter: "WeChatPadProAdapter", # 传递适配器实例
|
|
35
|
+
):
|
|
36
|
+
super().__init__(message_str, message_obj, platform_meta, session_id)
|
|
37
|
+
self.message_obj = message_obj # Save the full message object
|
|
38
|
+
self.adapter = adapter # Save the adapter instance
|
|
39
|
+
|
|
40
|
+
async def send(self, message: MessageChain):
|
|
41
|
+
async with aiohttp.ClientSession() as session:
|
|
42
|
+
for comp in message.chain:
|
|
43
|
+
await asyncio.sleep(1)
|
|
44
|
+
if isinstance(comp, Plain):
|
|
45
|
+
await self._send_text(session, comp.text)
|
|
46
|
+
elif isinstance(comp, Image):
|
|
47
|
+
await self._send_image(session, comp)
|
|
48
|
+
elif isinstance(comp, WechatEmoji):
|
|
49
|
+
await self._send_emoji(session, comp)
|
|
50
|
+
elif isinstance(comp, Record):
|
|
51
|
+
await self._send_voice(session, comp)
|
|
52
|
+
await super().send(message)
|
|
53
|
+
|
|
54
|
+
async def send_streaming(
|
|
55
|
+
self, generator: AsyncGenerator[MessageChain, None], use_fallback: bool = False
|
|
56
|
+
):
|
|
57
|
+
buffer = None
|
|
58
|
+
async for chain in generator:
|
|
59
|
+
if not buffer:
|
|
60
|
+
buffer = chain
|
|
61
|
+
else:
|
|
62
|
+
buffer.chain.extend(chain.chain)
|
|
63
|
+
if not buffer:
|
|
64
|
+
return None
|
|
65
|
+
buffer.squash_plain()
|
|
66
|
+
await self.send(buffer)
|
|
67
|
+
return await super().send_streaming(generator, use_fallback)
|
|
68
|
+
|
|
69
|
+
async def _send_image(self, session: aiohttp.ClientSession, comp: Image):
|
|
70
|
+
b64 = await comp.convert_to_base64()
|
|
71
|
+
raw = self._validate_base64(b64)
|
|
72
|
+
b64c = self._compress_image(raw)
|
|
73
|
+
payload = {
|
|
74
|
+
"MsgItem": [
|
|
75
|
+
{"ImageContent": b64c, "MsgType": 3, "ToUserName": self.session_id},
|
|
76
|
+
],
|
|
77
|
+
}
|
|
78
|
+
url = f"{self.adapter.base_url}/message/SendImageNewMessage"
|
|
79
|
+
await self._post(session, url, payload)
|
|
80
|
+
|
|
81
|
+
async def _send_text(self, session: aiohttp.ClientSession, text: str):
|
|
82
|
+
if (
|
|
83
|
+
self.message_obj.type == MessageType.GROUP_MESSAGE # 确保是群聊消息
|
|
84
|
+
and self.adapter.settings.get(
|
|
85
|
+
"reply_with_mention",
|
|
86
|
+
False,
|
|
87
|
+
) # 检查适配器设置是否启用 reply_with_mention
|
|
88
|
+
and self.message_obj.sender # 确保有发送者信息
|
|
89
|
+
and (
|
|
90
|
+
self.message_obj.sender.user_id or self.message_obj.sender.nickname
|
|
91
|
+
) # 确保发送者有 ID 或昵称
|
|
92
|
+
):
|
|
93
|
+
# 优先使用 nickname,如果没有则使用 user_id
|
|
94
|
+
mention_text = (
|
|
95
|
+
self.message_obj.sender.nickname or self.message_obj.sender.user_id
|
|
96
|
+
)
|
|
97
|
+
message_text = f"@{mention_text} {text}"
|
|
98
|
+
# logger.info(f"已添加 @ 信息: {message_text}")
|
|
99
|
+
else:
|
|
100
|
+
message_text = text
|
|
101
|
+
if self.get_group_id() and "#" in self.session_id:
|
|
102
|
+
session_id = self.session_id.split("#")[0]
|
|
103
|
+
else:
|
|
104
|
+
session_id = self.session_id
|
|
105
|
+
payload = {
|
|
106
|
+
"MsgItem": [
|
|
107
|
+
{
|
|
108
|
+
"MsgType": 1,
|
|
109
|
+
"TextContent": message_text,
|
|
110
|
+
"ToUserName": session_id,
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
}
|
|
114
|
+
url = f"{self.adapter.base_url}/message/SendTextMessage"
|
|
115
|
+
await self._post(session, url, payload)
|
|
116
|
+
|
|
117
|
+
async def _send_emoji(self, session: aiohttp.ClientSession, comp: WechatEmoji):
|
|
118
|
+
payload = {
|
|
119
|
+
"EmojiList": [
|
|
120
|
+
{
|
|
121
|
+
"EmojiMd5": comp.md5,
|
|
122
|
+
"EmojiSize": comp.md5_len,
|
|
123
|
+
"ToUserName": self.session_id,
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
}
|
|
127
|
+
url = f"{self.adapter.base_url}/message/SendEmojiMessage"
|
|
128
|
+
await self._post(session, url, payload)
|
|
129
|
+
|
|
130
|
+
async def _send_voice(self, session: aiohttp.ClientSession, comp: Record):
|
|
131
|
+
record_path = await comp.convert_to_file_path()
|
|
132
|
+
# 默认已经存在 data/temp 中
|
|
133
|
+
b64, duration = await audio_to_tencent_silk_base64(record_path)
|
|
134
|
+
payload = {
|
|
135
|
+
"ToUserName": self.session_id,
|
|
136
|
+
"VoiceData": b64,
|
|
137
|
+
"VoiceFormat": 4,
|
|
138
|
+
"VoiceSecond": duration,
|
|
139
|
+
}
|
|
140
|
+
url = f"{self.adapter.base_url}/message/SendVoice"
|
|
141
|
+
await self._post(session, url, payload)
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def _validate_base64(b64: str) -> bytes:
|
|
145
|
+
return base64.b64decode(b64, validate=True)
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def _compress_image(data: bytes) -> str:
|
|
149
|
+
img = PILImage.open(io.BytesIO(data))
|
|
150
|
+
buf = io.BytesIO()
|
|
151
|
+
if img.format == "JPEG":
|
|
152
|
+
img.save(buf, "JPEG", quality=80)
|
|
153
|
+
else:
|
|
154
|
+
if img.mode in ("RGBA", "P"):
|
|
155
|
+
img = img.convert("RGB")
|
|
156
|
+
img.save(buf, "JPEG", quality=80)
|
|
157
|
+
# logger.info("图片处理完成!!!")
|
|
158
|
+
return base64.b64encode(buf.getvalue()).decode()
|
|
159
|
+
|
|
160
|
+
async def _post(self, session, url, payload):
|
|
161
|
+
params = {"key": self.adapter.auth_key}
|
|
162
|
+
try:
|
|
163
|
+
async with session.post(url, params=params, json=payload) as resp:
|
|
164
|
+
data = await resp.json()
|
|
165
|
+
if resp.status != 200 or data.get("Code") != 200:
|
|
166
|
+
logger.error(f"{url} failed: {resp.status} {data}")
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.error(f"{url} error: {e}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# TODO: 添加对其他消息组件类型的处理 (Record, Video, At等)
|
|
172
|
+
# elif isinstance(component, Record):
|
|
173
|
+
# pass
|
|
174
|
+
# elif isinstance(component, Video):
|
|
175
|
+
# pass
|
|
176
|
+
# elif isinstance(component, At):
|
|
177
|
+
# pass
|
|
178
|
+
# ...
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from defusedxml import ElementTree as eT
|
|
2
|
+
|
|
3
|
+
from astrbot.api import logger
|
|
4
|
+
from astrbot.api.message_components import (
|
|
5
|
+
BaseMessageComponent,
|
|
6
|
+
Image,
|
|
7
|
+
Plain,
|
|
8
|
+
)
|
|
9
|
+
from astrbot.api.message_components import (
|
|
10
|
+
WechatEmoji as Emoji,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GeweDataParser:
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
content: str,
|
|
18
|
+
is_private_chat: bool = False,
|
|
19
|
+
cached_texts=None,
|
|
20
|
+
cached_images=None,
|
|
21
|
+
raw_message: dict | None = None,
|
|
22
|
+
downloader=None,
|
|
23
|
+
):
|
|
24
|
+
self._xml = None
|
|
25
|
+
self.content = content
|
|
26
|
+
self.is_private_chat = is_private_chat
|
|
27
|
+
self.cached_texts = cached_texts or {}
|
|
28
|
+
self.cached_images = cached_images or {}
|
|
29
|
+
self.downloader = downloader
|
|
30
|
+
|
|
31
|
+
raw_message = raw_message or {}
|
|
32
|
+
self.from_user_name = raw_message.get("from_user_name", {}).get("str", "")
|
|
33
|
+
self.to_user_name = raw_message.get("to_user_name", {}).get("str", "")
|
|
34
|
+
self.msg_id = raw_message.get("msg_id", "")
|
|
35
|
+
|
|
36
|
+
def _format_to_xml(self):
|
|
37
|
+
if self._xml:
|
|
38
|
+
return self._xml
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
msg_str = self.content
|
|
42
|
+
if not self.is_private_chat:
|
|
43
|
+
parts = self.content.split(":\n", 1)
|
|
44
|
+
msg_str = parts[1] if len(parts) == 2 else self.content
|
|
45
|
+
|
|
46
|
+
self._xml = eT.fromstring(msg_str)
|
|
47
|
+
return self._xml
|
|
48
|
+
except Exception as e:
|
|
49
|
+
logger.error(f"[XML解析失败] {e}")
|
|
50
|
+
raise
|
|
51
|
+
|
|
52
|
+
async def parse_mutil_49(self) -> list[BaseMessageComponent] | None:
|
|
53
|
+
"""处理 msg_type == 49 的多种 appmsg 类型(目前支持 type==57)"""
|
|
54
|
+
try:
|
|
55
|
+
appmsg_type = self._format_to_xml().findtext(".//appmsg/type")
|
|
56
|
+
if appmsg_type == "57":
|
|
57
|
+
return await self.parse_reply()
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.warning(f"[parse_mutil_49] 解析失败: {e}")
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
async def parse_reply(self) -> list[BaseMessageComponent]:
|
|
63
|
+
"""处理 type == 57 的引用消息:支持文本(1)、图片(3)、嵌套49(49)"""
|
|
64
|
+
components = []
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
appmsg = self._format_to_xml().find("appmsg")
|
|
68
|
+
if appmsg is None:
|
|
69
|
+
return [Plain("[引用消息解析失败]")]
|
|
70
|
+
|
|
71
|
+
refermsg = appmsg.find("refermsg")
|
|
72
|
+
if refermsg is None:
|
|
73
|
+
return [Plain("[引用消息解析失败]")]
|
|
74
|
+
|
|
75
|
+
quote_type = int(refermsg.findtext("type", "0"))
|
|
76
|
+
nickname = refermsg.findtext("displayname", "未知发送者")
|
|
77
|
+
quote_content = refermsg.findtext("content", "")
|
|
78
|
+
svrid = refermsg.findtext("svrid")
|
|
79
|
+
|
|
80
|
+
match quote_type:
|
|
81
|
+
case 1: # 文本引用
|
|
82
|
+
quoted_text = self.cached_texts.get(str(svrid), quote_content)
|
|
83
|
+
components.append(Plain(f"[引用] {nickname}: {quoted_text}"))
|
|
84
|
+
|
|
85
|
+
case 3: # 图片引用
|
|
86
|
+
quoted_image_b64 = self.cached_images.get(str(svrid))
|
|
87
|
+
if not quoted_image_b64:
|
|
88
|
+
try:
|
|
89
|
+
quote_xml = eT.fromstring(quote_content)
|
|
90
|
+
img = quote_xml.find("img")
|
|
91
|
+
cdn_url = (
|
|
92
|
+
img.get("cdnbigimgurl") or img.get("cdnmidimgurl")
|
|
93
|
+
if img is not None
|
|
94
|
+
else None
|
|
95
|
+
)
|
|
96
|
+
if cdn_url and self.downloader:
|
|
97
|
+
image_resp = await self.downloader(
|
|
98
|
+
self.from_user_name,
|
|
99
|
+
self.to_user_name,
|
|
100
|
+
self.msg_id,
|
|
101
|
+
)
|
|
102
|
+
quoted_image_b64 = (
|
|
103
|
+
image_resp.get("Data", {})
|
|
104
|
+
.get("Data", {})
|
|
105
|
+
.get("Buffer")
|
|
106
|
+
)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.warning(f"[引用图片解析失败] svrid={svrid} err={e}")
|
|
109
|
+
|
|
110
|
+
if quoted_image_b64:
|
|
111
|
+
components.extend(
|
|
112
|
+
[
|
|
113
|
+
Image.fromBase64(quoted_image_b64),
|
|
114
|
+
Plain(f"[引用] {nickname}: [引用的图片]"),
|
|
115
|
+
],
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
components.append(
|
|
119
|
+
Plain(f"[引用] {nickname}: [引用的图片 - 未能获取]"),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
case 49: # 嵌套引用
|
|
123
|
+
try:
|
|
124
|
+
nested_root = eT.fromstring(quote_content)
|
|
125
|
+
nested_title = nested_root.findtext(".//appmsg/title", "")
|
|
126
|
+
components.append(Plain(f"[引用] {nickname}: {nested_title}"))
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.warning(f"[嵌套引用解析失败] err={e}")
|
|
129
|
+
components.append(Plain(f"[引用] {nickname}: [嵌套引用消息]"))
|
|
130
|
+
|
|
131
|
+
case _: # 其他未识别类型
|
|
132
|
+
logger.info(f"[未知引用类型] quote_type={quote_type}")
|
|
133
|
+
components.append(Plain(f"[引用] {nickname}: [不支持的引用类型]"))
|
|
134
|
+
|
|
135
|
+
# 主消息标题
|
|
136
|
+
title = appmsg.findtext("title", "")
|
|
137
|
+
if title:
|
|
138
|
+
components.append(Plain(title))
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.error(f"[parse_reply] 总体解析失败: {e}")
|
|
142
|
+
return [Plain("[引用消息解析失败]")]
|
|
143
|
+
|
|
144
|
+
return components
|
|
145
|
+
|
|
146
|
+
def parse_emoji(self) -> Emoji | None:
|
|
147
|
+
"""处理 msg_type == 47 的表情消息(emoji)"""
|
|
148
|
+
try:
|
|
149
|
+
emoji_element = self._format_to_xml().find(".//emoji")
|
|
150
|
+
if emoji_element is not None:
|
|
151
|
+
return Emoji(
|
|
152
|
+
md5=emoji_element.get("md5"),
|
|
153
|
+
md5_len=emoji_element.get("len"),
|
|
154
|
+
cdnurl=emoji_element.get("cdnurl"),
|
|
155
|
+
)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
logger.error(f"[parse_emoji] 解析失败: {e}")
|
|
158
|
+
|
|
159
|
+
return None
|
|
@@ -1,28 +1,33 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
1
3
|
import sys
|
|
2
4
|
import uuid
|
|
3
|
-
|
|
5
|
+
|
|
4
6
|
import quart
|
|
7
|
+
from requests import Response
|
|
8
|
+
from wechatpy.enterprise import WeChatClient, parse_message
|
|
9
|
+
from wechatpy.enterprise.crypto import WeChatCrypto
|
|
10
|
+
from wechatpy.enterprise.messages import ImageMessage, TextMessage, VoiceMessage
|
|
11
|
+
from wechatpy.exceptions import InvalidSignatureException
|
|
12
|
+
from wechatpy.messages import BaseMessage
|
|
5
13
|
|
|
14
|
+
from astrbot.api.event import MessageChain
|
|
15
|
+
from astrbot.api.message_components import Image, Plain, Record
|
|
6
16
|
from astrbot.api.platform import (
|
|
7
|
-
Platform,
|
|
8
17
|
AstrBotMessage,
|
|
9
18
|
MessageMember,
|
|
10
|
-
PlatformMetadata,
|
|
11
19
|
MessageType,
|
|
20
|
+
Platform,
|
|
21
|
+
PlatformMetadata,
|
|
22
|
+
register_platform_adapter,
|
|
12
23
|
)
|
|
13
|
-
from astrbot.api.event import MessageChain
|
|
14
|
-
from astrbot.api.message_components import Plain, Image, Record
|
|
15
|
-
from astrbot.core.platform.astr_message_event import MessageSesion
|
|
16
|
-
from astrbot.api.platform import register_platform_adapter
|
|
17
24
|
from astrbot.core import logger
|
|
18
|
-
from
|
|
25
|
+
from astrbot.core.platform.astr_message_event import MessageSesion
|
|
26
|
+
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
|
19
27
|
|
|
20
|
-
from wechatpy.enterprise.crypto import WeChatCrypto
|
|
21
|
-
from wechatpy.enterprise import WeChatClient
|
|
22
|
-
from wechatpy.enterprise.messages import TextMessage, ImageMessage, VoiceMessage
|
|
23
|
-
from wechatpy.exceptions import InvalidSignatureException
|
|
24
|
-
from wechatpy.enterprise import parse_message
|
|
25
28
|
from .wecom_event import WecomPlatformEvent
|
|
29
|
+
from .wecom_kf import WeChatKF
|
|
30
|
+
from .wecom_kf_message import WeChatKFMessage
|
|
26
31
|
|
|
27
32
|
if sys.version_info >= (3, 12):
|
|
28
33
|
from typing import override
|
|
@@ -36,10 +41,14 @@ class WecomServer:
|
|
|
36
41
|
self.port = int(config.get("port"))
|
|
37
42
|
self.callback_server_host = config.get("callback_server_host", "0.0.0.0")
|
|
38
43
|
self.server.add_url_rule(
|
|
39
|
-
"/callback/command",
|
|
44
|
+
"/callback/command",
|
|
45
|
+
view_func=self.verify,
|
|
46
|
+
methods=["GET"],
|
|
40
47
|
)
|
|
41
48
|
self.server.add_url_rule(
|
|
42
|
-
"/callback/command",
|
|
49
|
+
"/callback/command",
|
|
50
|
+
view_func=self.callback_command,
|
|
51
|
+
methods=["POST"],
|
|
43
52
|
)
|
|
44
53
|
self.event_queue = event_queue
|
|
45
54
|
|
|
@@ -89,7 +98,7 @@ class WecomServer:
|
|
|
89
98
|
|
|
90
99
|
async def start_polling(self):
|
|
91
100
|
logger.info(
|
|
92
|
-
f"将在 {self.callback_server_host}:{self.port} 端口启动 企业微信 适配器。"
|
|
101
|
+
f"将在 {self.callback_server_host}:{self.port} 端口启动 企业微信 适配器。",
|
|
93
102
|
)
|
|
94
103
|
await self.server.run_task(
|
|
95
104
|
host=self.callback_server_host,
|
|
@@ -101,24 +110,27 @@ class WecomServer:
|
|
|
101
110
|
await self.shutdown_event.wait()
|
|
102
111
|
|
|
103
112
|
|
|
104
|
-
@register_platform_adapter("wecom", "wecom 适配器")
|
|
113
|
+
@register_platform_adapter("wecom", "wecom 适配器", support_streaming_message=False)
|
|
105
114
|
class WecomPlatformAdapter(Platform):
|
|
106
115
|
def __init__(
|
|
107
|
-
self,
|
|
116
|
+
self,
|
|
117
|
+
platform_config: dict,
|
|
118
|
+
platform_settings: dict,
|
|
119
|
+
event_queue: asyncio.Queue,
|
|
108
120
|
) -> None:
|
|
109
121
|
super().__init__(event_queue)
|
|
110
122
|
self.config = platform_config
|
|
111
123
|
self.settingss = platform_settings
|
|
112
124
|
self.client_self_id = uuid.uuid4().hex[:8]
|
|
113
125
|
self.api_base_url = platform_config.get(
|
|
114
|
-
"api_base_url",
|
|
126
|
+
"api_base_url",
|
|
127
|
+
"https://qyapi.weixin.qq.com/cgi-bin/",
|
|
115
128
|
)
|
|
116
129
|
|
|
117
130
|
if not self.api_base_url:
|
|
118
131
|
self.api_base_url = "https://qyapi.weixin.qq.com/cgi-bin/"
|
|
119
132
|
|
|
120
|
-
|
|
121
|
-
self.api_base_url = self.api_base_url[:-1]
|
|
133
|
+
self.api_base_url = self.api_base_url.removesuffix("/")
|
|
122
134
|
if not self.api_base_url.endswith("/cgi-bin"):
|
|
123
135
|
self.api_base_url += "/cgi-bin"
|
|
124
136
|
|
|
@@ -131,16 +143,50 @@ class WecomPlatformAdapter(Platform):
|
|
|
131
143
|
self.config["corpid"].strip(),
|
|
132
144
|
self.config["secret"].strip(),
|
|
133
145
|
)
|
|
146
|
+
|
|
147
|
+
# 微信客服
|
|
148
|
+
self.kf_name = self.config.get("kf_name", None)
|
|
149
|
+
if self.kf_name:
|
|
150
|
+
# inject
|
|
151
|
+
self.wechat_kf_api = WeChatKF(client=self.client)
|
|
152
|
+
self.wechat_kf_message_api = WeChatKFMessage(self.client)
|
|
153
|
+
self.client.kf = self.wechat_kf_api
|
|
154
|
+
self.client.kf_message = self.wechat_kf_message_api
|
|
155
|
+
|
|
134
156
|
self.client.API_BASE_URL = self.api_base_url
|
|
135
157
|
|
|
136
|
-
async def callback(msg):
|
|
158
|
+
async def callback(msg: BaseMessage):
|
|
159
|
+
if msg.type == "unknown" and msg._data["Event"] == "kf_msg_or_event":
|
|
160
|
+
|
|
161
|
+
def get_latest_msg_item() -> dict | None:
|
|
162
|
+
token = msg._data["Token"]
|
|
163
|
+
kfid = msg._data["OpenKfId"]
|
|
164
|
+
has_more = 1
|
|
165
|
+
ret = {}
|
|
166
|
+
while has_more:
|
|
167
|
+
ret = self.wechat_kf_api.sync_msg(token, kfid)
|
|
168
|
+
has_more = ret["has_more"]
|
|
169
|
+
msg_list = ret.get("msg_list", [])
|
|
170
|
+
if msg_list:
|
|
171
|
+
return msg_list[-1]
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
msg_new = await asyncio.get_event_loop().run_in_executor(
|
|
175
|
+
None,
|
|
176
|
+
get_latest_msg_item,
|
|
177
|
+
)
|
|
178
|
+
if msg_new:
|
|
179
|
+
await self.convert_wechat_kf_message(msg_new)
|
|
180
|
+
return
|
|
137
181
|
await self.convert_message(msg)
|
|
138
182
|
|
|
139
183
|
self.server.callback = callback
|
|
140
184
|
|
|
141
185
|
@override
|
|
142
186
|
async def send_by_session(
|
|
143
|
-
self,
|
|
187
|
+
self,
|
|
188
|
+
session: MessageSesion,
|
|
189
|
+
message_chain: MessageChain,
|
|
144
190
|
):
|
|
145
191
|
await super().send_by_session(session, message_chain)
|
|
146
192
|
|
|
@@ -149,13 +195,46 @@ class WecomPlatformAdapter(Platform):
|
|
|
149
195
|
return PlatformMetadata(
|
|
150
196
|
"wecom",
|
|
151
197
|
"wecom 适配器",
|
|
198
|
+
id=self.config.get("id", "wecom"),
|
|
199
|
+
support_streaming_message=False,
|
|
152
200
|
)
|
|
153
201
|
|
|
154
202
|
@override
|
|
155
203
|
async def run(self):
|
|
204
|
+
loop = asyncio.get_event_loop()
|
|
205
|
+
if self.kf_name:
|
|
206
|
+
try:
|
|
207
|
+
acc_list = (
|
|
208
|
+
await loop.run_in_executor(
|
|
209
|
+
None,
|
|
210
|
+
self.wechat_kf_api.get_account_list,
|
|
211
|
+
)
|
|
212
|
+
).get("account_list", [])
|
|
213
|
+
logger.debug(f"获取到微信客服列表: {acc_list!s}")
|
|
214
|
+
for acc in acc_list:
|
|
215
|
+
name = acc.get("name", None)
|
|
216
|
+
if name != self.kf_name:
|
|
217
|
+
continue
|
|
218
|
+
open_kfid = acc.get("open_kfid", None)
|
|
219
|
+
if not open_kfid:
|
|
220
|
+
logger.error("获取微信客服失败,open_kfid 为空。")
|
|
221
|
+
logger.debug(f"Found open_kfid: {open_kfid!s}")
|
|
222
|
+
kf_url = (
|
|
223
|
+
await loop.run_in_executor(
|
|
224
|
+
None,
|
|
225
|
+
self.wechat_kf_api.add_contact_way,
|
|
226
|
+
open_kfid,
|
|
227
|
+
"astrbot_placeholder",
|
|
228
|
+
)
|
|
229
|
+
).get("url", "")
|
|
230
|
+
logger.info(
|
|
231
|
+
f"请打开以下链接,在微信扫码以获取客服微信: https://api.cl2wm.cn/api/qrcode/code?text={kf_url}",
|
|
232
|
+
)
|
|
233
|
+
except Exception as e:
|
|
234
|
+
logger.error(e)
|
|
156
235
|
await self.server.start_polling()
|
|
157
236
|
|
|
158
|
-
async def convert_message(self, msg):
|
|
237
|
+
async def convert_message(self, msg: BaseMessage) -> AstrBotMessage | None:
|
|
159
238
|
abm = AstrBotMessage()
|
|
160
239
|
if msg.type == "text":
|
|
161
240
|
assert isinstance(msg, TextMessage)
|
|
@@ -189,16 +268,19 @@ class WecomPlatformAdapter(Platform):
|
|
|
189
268
|
assert isinstance(msg, VoiceMessage)
|
|
190
269
|
|
|
191
270
|
resp: Response = await asyncio.get_event_loop().run_in_executor(
|
|
192
|
-
None,
|
|
271
|
+
None,
|
|
272
|
+
self.client.media.download,
|
|
273
|
+
msg.media_id,
|
|
193
274
|
)
|
|
194
|
-
|
|
275
|
+
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
|
276
|
+
path = os.path.join(temp_dir, f"wecom_{msg.media_id}.amr")
|
|
195
277
|
with open(path, "wb") as f:
|
|
196
278
|
f.write(resp.content)
|
|
197
279
|
|
|
198
280
|
try:
|
|
199
281
|
from pydub import AudioSegment
|
|
200
282
|
|
|
201
|
-
path_wav = f"
|
|
283
|
+
path_wav = os.path.join(temp_dir, f"wecom_{msg.media_id}.wav")
|
|
202
284
|
audio = AudioSegment.from_file(path)
|
|
203
285
|
audio.export(path_wav, format="wav")
|
|
204
286
|
except Exception as e:
|
|
@@ -218,10 +300,70 @@ class WecomPlatformAdapter(Platform):
|
|
|
218
300
|
abm.timestamp = msg.time
|
|
219
301
|
abm.session_id = abm.sender.user_id
|
|
220
302
|
abm.raw_message = msg
|
|
303
|
+
else:
|
|
304
|
+
logger.warning(f"暂未实现的事件: {msg.type}")
|
|
305
|
+
return
|
|
221
306
|
|
|
222
307
|
logger.info(f"abm: {abm}")
|
|
223
308
|
await self.handle_msg(abm)
|
|
224
309
|
|
|
310
|
+
async def convert_wechat_kf_message(self, msg: dict) -> AstrBotMessage | None:
|
|
311
|
+
msgtype = msg.get("msgtype")
|
|
312
|
+
external_userid = msg.get("external_userid")
|
|
313
|
+
abm = AstrBotMessage()
|
|
314
|
+
abm.raw_message = msg
|
|
315
|
+
abm.raw_message["_wechat_kf_flag"] = None # 方便处理
|
|
316
|
+
abm.self_id = msg["open_kfid"]
|
|
317
|
+
abm.sender = MessageMember(external_userid, external_userid)
|
|
318
|
+
abm.session_id = external_userid
|
|
319
|
+
abm.type = MessageType.FRIEND_MESSAGE
|
|
320
|
+
abm.message_id = msg.get("msgid", uuid.uuid4().hex[:8])
|
|
321
|
+
abm.message_str = ""
|
|
322
|
+
if msgtype == "text":
|
|
323
|
+
text = msg.get("text", {}).get("content", "").strip()
|
|
324
|
+
abm.message = [Plain(text=text)]
|
|
325
|
+
abm.message_str = text
|
|
326
|
+
elif msgtype == "image":
|
|
327
|
+
media_id = msg.get("image", {}).get("media_id", "")
|
|
328
|
+
resp: Response = await asyncio.get_event_loop().run_in_executor(
|
|
329
|
+
None,
|
|
330
|
+
self.client.media.download,
|
|
331
|
+
media_id,
|
|
332
|
+
)
|
|
333
|
+
path = f"data/temp/wechat_kf_{media_id}.jpg"
|
|
334
|
+
with open(path, "wb") as f:
|
|
335
|
+
f.write(resp.content)
|
|
336
|
+
abm.message = [Image(file=path, url=path)]
|
|
337
|
+
elif msgtype == "voice":
|
|
338
|
+
media_id = msg.get("voice", {}).get("media_id", "")
|
|
339
|
+
resp: Response = await asyncio.get_event_loop().run_in_executor(
|
|
340
|
+
None,
|
|
341
|
+
self.client.media.download,
|
|
342
|
+
media_id,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
|
346
|
+
path = os.path.join(temp_dir, f"weixinkefu_{media_id}.amr")
|
|
347
|
+
with open(path, "wb") as f:
|
|
348
|
+
f.write(resp.content)
|
|
349
|
+
|
|
350
|
+
try:
|
|
351
|
+
from pydub import AudioSegment
|
|
352
|
+
|
|
353
|
+
path_wav = os.path.join(temp_dir, f"weixinkefu_{media_id}.wav")
|
|
354
|
+
audio = AudioSegment.from_file(path)
|
|
355
|
+
audio.export(path_wav, format="wav")
|
|
356
|
+
except Exception as e:
|
|
357
|
+
logger.error(f"转换音频失败: {e}。如果没有安装 ffmpeg 请先安装。")
|
|
358
|
+
path_wav = path
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
abm.message = [Record(file=path_wav, url=path_wav)]
|
|
362
|
+
else:
|
|
363
|
+
logger.warning(f"未实现的微信客服消息事件: {msg}")
|
|
364
|
+
return
|
|
365
|
+
await self.handle_msg(abm)
|
|
366
|
+
|
|
225
367
|
async def handle_msg(self, message: AstrBotMessage):
|
|
226
368
|
message_event = WecomPlatformEvent(
|
|
227
369
|
message_str=message.message_str,
|