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,306 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
import quart
|
|
6
|
+
from requests import Response
|
|
7
|
+
from wechatpy import WeChatClient, parse_message
|
|
8
|
+
from wechatpy.crypto import WeChatCrypto
|
|
9
|
+
from wechatpy.exceptions import InvalidSignatureException
|
|
10
|
+
from wechatpy.messages import BaseMessage, ImageMessage, TextMessage, VoiceMessage
|
|
11
|
+
from wechatpy.utils import check_signature
|
|
12
|
+
|
|
13
|
+
from astrbot.api.event import MessageChain
|
|
14
|
+
from astrbot.api.message_components import Image, Plain, Record
|
|
15
|
+
from astrbot.api.platform import (
|
|
16
|
+
AstrBotMessage,
|
|
17
|
+
MessageMember,
|
|
18
|
+
MessageType,
|
|
19
|
+
Platform,
|
|
20
|
+
PlatformMetadata,
|
|
21
|
+
register_platform_adapter,
|
|
22
|
+
)
|
|
23
|
+
from astrbot.core import logger
|
|
24
|
+
from astrbot.core.platform.astr_message_event import MessageSesion
|
|
25
|
+
|
|
26
|
+
from .weixin_offacc_event import WeixinOfficialAccountPlatformEvent
|
|
27
|
+
|
|
28
|
+
if sys.version_info >= (3, 12):
|
|
29
|
+
from typing import override
|
|
30
|
+
else:
|
|
31
|
+
from typing_extensions import override
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class WecomServer:
|
|
35
|
+
def __init__(self, event_queue: asyncio.Queue, config: dict):
|
|
36
|
+
self.server = quart.Quart(__name__)
|
|
37
|
+
self.port = int(config.get("port"))
|
|
38
|
+
self.callback_server_host = config.get("callback_server_host", "0.0.0.0")
|
|
39
|
+
self.token = config.get("token")
|
|
40
|
+
self.encoding_aes_key = config.get("encoding_aes_key")
|
|
41
|
+
self.appid = config.get("appid")
|
|
42
|
+
self.server.add_url_rule(
|
|
43
|
+
"/callback/command",
|
|
44
|
+
view_func=self.verify,
|
|
45
|
+
methods=["GET"],
|
|
46
|
+
)
|
|
47
|
+
self.server.add_url_rule(
|
|
48
|
+
"/callback/command",
|
|
49
|
+
view_func=self.callback_command,
|
|
50
|
+
methods=["POST"],
|
|
51
|
+
)
|
|
52
|
+
self.crypto = WeChatCrypto(self.token, self.encoding_aes_key, self.appid)
|
|
53
|
+
|
|
54
|
+
self.event_queue = event_queue
|
|
55
|
+
|
|
56
|
+
self.callback = None
|
|
57
|
+
self.shutdown_event = asyncio.Event()
|
|
58
|
+
|
|
59
|
+
async def verify(self):
|
|
60
|
+
logger.info(f"验证请求有效性: {quart.request.args}")
|
|
61
|
+
|
|
62
|
+
args = quart.request.args
|
|
63
|
+
if not args.get("signature", None):
|
|
64
|
+
logger.error("未知的响应,请检查回调地址是否填写正确。")
|
|
65
|
+
return "err"
|
|
66
|
+
try:
|
|
67
|
+
check_signature(
|
|
68
|
+
self.token,
|
|
69
|
+
args.get("signature"),
|
|
70
|
+
args.get("timestamp"),
|
|
71
|
+
args.get("nonce"),
|
|
72
|
+
)
|
|
73
|
+
logger.info("验证请求有效性成功。")
|
|
74
|
+
return args.get("echostr", "empty")
|
|
75
|
+
except InvalidSignatureException:
|
|
76
|
+
logger.error("验证请求有效性失败,签名异常,请检查配置。")
|
|
77
|
+
return "err"
|
|
78
|
+
|
|
79
|
+
async def callback_command(self):
|
|
80
|
+
data = await quart.request.get_data()
|
|
81
|
+
msg_signature = quart.request.args.get("msg_signature")
|
|
82
|
+
timestamp = quart.request.args.get("timestamp")
|
|
83
|
+
nonce = quart.request.args.get("nonce")
|
|
84
|
+
try:
|
|
85
|
+
xml = self.crypto.decrypt_message(data, msg_signature, timestamp, nonce)
|
|
86
|
+
except InvalidSignatureException:
|
|
87
|
+
logger.error("解密失败,签名异常,请检查配置。")
|
|
88
|
+
raise
|
|
89
|
+
else:
|
|
90
|
+
msg = parse_message(xml)
|
|
91
|
+
logger.info(f"解析成功: {msg}")
|
|
92
|
+
|
|
93
|
+
if self.callback:
|
|
94
|
+
result_xml = await self.callback(msg)
|
|
95
|
+
if not result_xml:
|
|
96
|
+
return "success"
|
|
97
|
+
if isinstance(result_xml, str):
|
|
98
|
+
return result_xml
|
|
99
|
+
|
|
100
|
+
return "success"
|
|
101
|
+
|
|
102
|
+
async def start_polling(self):
|
|
103
|
+
logger.info(
|
|
104
|
+
f"将在 {self.callback_server_host}:{self.port} 端口启动 微信公众平台 适配器。",
|
|
105
|
+
)
|
|
106
|
+
await self.server.run_task(
|
|
107
|
+
host=self.callback_server_host,
|
|
108
|
+
port=self.port,
|
|
109
|
+
shutdown_trigger=self.shutdown_trigger,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
async def shutdown_trigger(self):
|
|
113
|
+
await self.shutdown_event.wait()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@register_platform_adapter(
|
|
117
|
+
"weixin_official_account", "微信公众平台 适配器", support_streaming_message=False
|
|
118
|
+
)
|
|
119
|
+
class WeixinOfficialAccountPlatformAdapter(Platform):
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
platform_config: dict,
|
|
123
|
+
platform_settings: dict,
|
|
124
|
+
event_queue: asyncio.Queue,
|
|
125
|
+
) -> None:
|
|
126
|
+
super().__init__(event_queue)
|
|
127
|
+
self.config = platform_config
|
|
128
|
+
self.settingss = platform_settings
|
|
129
|
+
self.client_self_id = uuid.uuid4().hex[:8]
|
|
130
|
+
self.api_base_url = platform_config.get(
|
|
131
|
+
"api_base_url",
|
|
132
|
+
"https://api.weixin.qq.com/cgi-bin/",
|
|
133
|
+
)
|
|
134
|
+
self.active_send_mode = self.config.get("active_send_mode", False)
|
|
135
|
+
|
|
136
|
+
if not self.api_base_url:
|
|
137
|
+
self.api_base_url = "https://api.weixin.qq.com/cgi-bin/"
|
|
138
|
+
|
|
139
|
+
self.api_base_url = self.api_base_url.removesuffix("/")
|
|
140
|
+
if not self.api_base_url.endswith("/cgi-bin"):
|
|
141
|
+
self.api_base_url += "/cgi-bin"
|
|
142
|
+
|
|
143
|
+
if not self.api_base_url.endswith("/"):
|
|
144
|
+
self.api_base_url += "/"
|
|
145
|
+
|
|
146
|
+
self.server = WecomServer(self._event_queue, self.config)
|
|
147
|
+
|
|
148
|
+
self.client = WeChatClient(
|
|
149
|
+
self.config["appid"].strip(),
|
|
150
|
+
self.config["secret"].strip(),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
self.client.API_BASE_URL = self.api_base_url
|
|
154
|
+
|
|
155
|
+
# 微信公众号必须 5 秒内进行回复,否则会重试 3 次,我们需要对其进行消息排重
|
|
156
|
+
# msgid -> Future
|
|
157
|
+
self.wexin_event_workers: dict[str, asyncio.Future] = {}
|
|
158
|
+
|
|
159
|
+
async def callback(msg: BaseMessage):
|
|
160
|
+
try:
|
|
161
|
+
if self.active_send_mode:
|
|
162
|
+
await self.convert_message(msg, None)
|
|
163
|
+
else:
|
|
164
|
+
if msg.id in self.wexin_event_workers:
|
|
165
|
+
future = self.wexin_event_workers[msg.id]
|
|
166
|
+
logger.debug(f"duplicate message id checked: {msg.id}")
|
|
167
|
+
else:
|
|
168
|
+
future = asyncio.get_event_loop().create_future()
|
|
169
|
+
self.wexin_event_workers[msg.id] = future
|
|
170
|
+
await self.convert_message(msg, future)
|
|
171
|
+
# I love shield so much!
|
|
172
|
+
result = await asyncio.wait_for(
|
|
173
|
+
asyncio.shield(future),
|
|
174
|
+
60,
|
|
175
|
+
) # wait for 60s
|
|
176
|
+
logger.debug(f"Got future result: {result}")
|
|
177
|
+
self.wexin_event_workers.pop(msg.id, None)
|
|
178
|
+
return result # xml. see weixin_offacc_event.py
|
|
179
|
+
except asyncio.TimeoutError:
|
|
180
|
+
pass
|
|
181
|
+
except Exception as e:
|
|
182
|
+
logger.error(f"转换消息时出现异常: {e}")
|
|
183
|
+
|
|
184
|
+
self.server.callback = callback
|
|
185
|
+
|
|
186
|
+
@override
|
|
187
|
+
async def send_by_session(
|
|
188
|
+
self,
|
|
189
|
+
session: MessageSesion,
|
|
190
|
+
message_chain: MessageChain,
|
|
191
|
+
):
|
|
192
|
+
await super().send_by_session(session, message_chain)
|
|
193
|
+
|
|
194
|
+
@override
|
|
195
|
+
def meta(self) -> PlatformMetadata:
|
|
196
|
+
return PlatformMetadata(
|
|
197
|
+
"weixin_official_account",
|
|
198
|
+
"微信公众平台 适配器",
|
|
199
|
+
id=self.config.get("id", "weixin_official_account"),
|
|
200
|
+
support_streaming_message=False,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
@override
|
|
204
|
+
async def run(self):
|
|
205
|
+
await self.server.start_polling()
|
|
206
|
+
|
|
207
|
+
async def convert_message(
|
|
208
|
+
self,
|
|
209
|
+
msg,
|
|
210
|
+
future: asyncio.Future = None,
|
|
211
|
+
) -> AstrBotMessage | None:
|
|
212
|
+
abm = AstrBotMessage()
|
|
213
|
+
if isinstance(msg, TextMessage):
|
|
214
|
+
abm.message_str = msg.content
|
|
215
|
+
abm.self_id = str(msg.target)
|
|
216
|
+
abm.message = [Plain(msg.content)]
|
|
217
|
+
abm.type = MessageType.FRIEND_MESSAGE
|
|
218
|
+
abm.sender = MessageMember(
|
|
219
|
+
msg.source,
|
|
220
|
+
msg.source,
|
|
221
|
+
)
|
|
222
|
+
abm.message_id = msg.id
|
|
223
|
+
abm.timestamp = msg.time
|
|
224
|
+
abm.session_id = abm.sender.user_id
|
|
225
|
+
elif msg.type == "image":
|
|
226
|
+
assert isinstance(msg, ImageMessage)
|
|
227
|
+
abm.message_str = "[图片]"
|
|
228
|
+
abm.self_id = str(msg.target)
|
|
229
|
+
abm.message = [Image(file=msg.image, url=msg.image)]
|
|
230
|
+
abm.type = MessageType.FRIEND_MESSAGE
|
|
231
|
+
abm.sender = MessageMember(
|
|
232
|
+
msg.source,
|
|
233
|
+
msg.source,
|
|
234
|
+
)
|
|
235
|
+
abm.message_id = msg.id
|
|
236
|
+
abm.timestamp = msg.time
|
|
237
|
+
abm.session_id = abm.sender.user_id
|
|
238
|
+
elif msg.type == "voice":
|
|
239
|
+
assert isinstance(msg, VoiceMessage)
|
|
240
|
+
|
|
241
|
+
resp: Response = await asyncio.get_event_loop().run_in_executor(
|
|
242
|
+
None,
|
|
243
|
+
self.client.media.download,
|
|
244
|
+
msg.media_id,
|
|
245
|
+
)
|
|
246
|
+
path = f"data/temp/wecom_{msg.media_id}.amr"
|
|
247
|
+
with open(path, "wb") as f:
|
|
248
|
+
f.write(resp.content)
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
from pydub import AudioSegment
|
|
252
|
+
|
|
253
|
+
path_wav = f"data/temp/wecom_{msg.media_id}.wav"
|
|
254
|
+
audio = AudioSegment.from_file(path)
|
|
255
|
+
audio.export(path_wav, format="wav")
|
|
256
|
+
except Exception as e:
|
|
257
|
+
logger.error(
|
|
258
|
+
f"转换音频失败: {e}。如果没有安装 pydub 和 ffmpeg 请先安装。",
|
|
259
|
+
)
|
|
260
|
+
path_wav = path
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
abm.message_str = ""
|
|
264
|
+
abm.self_id = str(msg.target)
|
|
265
|
+
abm.message = [Record(file=path_wav, url=path_wav)]
|
|
266
|
+
abm.type = MessageType.FRIEND_MESSAGE
|
|
267
|
+
abm.sender = MessageMember(
|
|
268
|
+
msg.source,
|
|
269
|
+
msg.source,
|
|
270
|
+
)
|
|
271
|
+
abm.message_id = msg.id
|
|
272
|
+
abm.timestamp = msg.time
|
|
273
|
+
abm.session_id = abm.sender.user_id
|
|
274
|
+
else:
|
|
275
|
+
logger.warning(f"暂未实现的事件: {msg.type}")
|
|
276
|
+
future.set_result(None)
|
|
277
|
+
return
|
|
278
|
+
# 很不优雅 :(
|
|
279
|
+
abm.raw_message = {
|
|
280
|
+
"message": msg,
|
|
281
|
+
"future": future,
|
|
282
|
+
"active_send_mode": self.active_send_mode,
|
|
283
|
+
}
|
|
284
|
+
logger.info(f"abm: {abm}")
|
|
285
|
+
await self.handle_msg(abm)
|
|
286
|
+
|
|
287
|
+
async def handle_msg(self, message: AstrBotMessage):
|
|
288
|
+
message_event = WeixinOfficialAccountPlatformEvent(
|
|
289
|
+
message_str=message.message_str,
|
|
290
|
+
message_obj=message,
|
|
291
|
+
platform_meta=self.meta(),
|
|
292
|
+
session_id=message.session_id,
|
|
293
|
+
client=self.client,
|
|
294
|
+
)
|
|
295
|
+
self.commit_event(message_event)
|
|
296
|
+
|
|
297
|
+
def get_client(self) -> WeChatClient:
|
|
298
|
+
return self.client
|
|
299
|
+
|
|
300
|
+
async def terminate(self):
|
|
301
|
+
self.server.shutdown_event.set()
|
|
302
|
+
try:
|
|
303
|
+
await self.server.server.shutdown()
|
|
304
|
+
except Exception as _:
|
|
305
|
+
pass
|
|
306
|
+
logger.info("微信公众平台 适配器已被优雅地关闭")
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
from wechatpy import WeChatClient
|
|
5
|
+
from wechatpy.replies import ImageReply, TextReply, VoiceReply
|
|
6
|
+
|
|
7
|
+
from astrbot.api import logger
|
|
8
|
+
from astrbot.api.event import AstrMessageEvent, MessageChain
|
|
9
|
+
from astrbot.api.message_components import Image, Plain, Record
|
|
10
|
+
from astrbot.api.platform import AstrBotMessage, PlatformMetadata
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import pydub
|
|
14
|
+
except Exception:
|
|
15
|
+
logger.warning(
|
|
16
|
+
"检测到 pydub 库未安装,微信公众平台将无法语音收发。如需使用语音,请前往管理面板 -> 控制台 -> 安装 Pip 库安装 pydub。",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WeixinOfficialAccountPlatformEvent(AstrMessageEvent):
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
message_str: str,
|
|
24
|
+
message_obj: AstrBotMessage,
|
|
25
|
+
platform_meta: PlatformMetadata,
|
|
26
|
+
session_id: str,
|
|
27
|
+
client: WeChatClient,
|
|
28
|
+
):
|
|
29
|
+
super().__init__(message_str, message_obj, platform_meta, session_id)
|
|
30
|
+
self.client = client
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
async def send_with_client(
|
|
34
|
+
client: WeChatClient,
|
|
35
|
+
message: MessageChain,
|
|
36
|
+
user_name: str,
|
|
37
|
+
):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
async def split_plain(self, plain: str) -> list[str]:
|
|
41
|
+
"""将长文本分割成多个小文本, 每个小文本长度不超过 2048 字符
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
plain (str): 要分割的长文本
|
|
45
|
+
Returns:
|
|
46
|
+
list[str]: 分割后的文本列表
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
if len(plain) <= 2048:
|
|
50
|
+
return [plain]
|
|
51
|
+
result = []
|
|
52
|
+
start = 0
|
|
53
|
+
while start < len(plain):
|
|
54
|
+
# 剩下的字符串长度<2048时结束
|
|
55
|
+
if start + 2048 >= len(plain):
|
|
56
|
+
result.append(plain[start:])
|
|
57
|
+
break
|
|
58
|
+
|
|
59
|
+
# 向前搜索分割标点符号
|
|
60
|
+
end = min(start + 2048, len(plain))
|
|
61
|
+
cut_position = end
|
|
62
|
+
for i in range(end, start, -1):
|
|
63
|
+
if i < len(plain) and plain[i - 1] in [
|
|
64
|
+
"。",
|
|
65
|
+
"!",
|
|
66
|
+
"?",
|
|
67
|
+
".",
|
|
68
|
+
"!",
|
|
69
|
+
"?",
|
|
70
|
+
"\n",
|
|
71
|
+
";",
|
|
72
|
+
";",
|
|
73
|
+
]:
|
|
74
|
+
cut_position = i
|
|
75
|
+
break
|
|
76
|
+
|
|
77
|
+
# 没找到合适的位置分割, 直接切分
|
|
78
|
+
if cut_position == end and end < len(plain):
|
|
79
|
+
cut_position = end
|
|
80
|
+
|
|
81
|
+
result.append(plain[start:cut_position])
|
|
82
|
+
start = cut_position
|
|
83
|
+
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
async def send(self, message: MessageChain):
|
|
87
|
+
message_obj = self.message_obj
|
|
88
|
+
active_send_mode = message_obj.raw_message.get("active_send_mode", False)
|
|
89
|
+
for comp in message.chain:
|
|
90
|
+
if isinstance(comp, Plain):
|
|
91
|
+
# Split long text messages if needed
|
|
92
|
+
plain_chunks = await self.split_plain(comp.text)
|
|
93
|
+
for chunk in plain_chunks:
|
|
94
|
+
if active_send_mode:
|
|
95
|
+
self.client.message.send_text(message_obj.sender.user_id, chunk)
|
|
96
|
+
else:
|
|
97
|
+
reply = TextReply(
|
|
98
|
+
content=chunk,
|
|
99
|
+
message=self.message_obj.raw_message["message"],
|
|
100
|
+
)
|
|
101
|
+
xml = reply.render()
|
|
102
|
+
future = self.message_obj.raw_message["future"]
|
|
103
|
+
assert isinstance(future, asyncio.Future)
|
|
104
|
+
future.set_result(xml)
|
|
105
|
+
await asyncio.sleep(0.5) # Avoid sending too fast
|
|
106
|
+
elif isinstance(comp, Image):
|
|
107
|
+
img_path = await comp.convert_to_file_path()
|
|
108
|
+
|
|
109
|
+
with open(img_path, "rb") as f:
|
|
110
|
+
try:
|
|
111
|
+
response = self.client.media.upload("image", f)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.error(f"微信公众平台上传图片失败: {e}")
|
|
114
|
+
await self.send(
|
|
115
|
+
MessageChain().message(f"微信公众平台上传图片失败: {e}"),
|
|
116
|
+
)
|
|
117
|
+
return
|
|
118
|
+
logger.debug(f"微信公众平台上传图片返回: {response}")
|
|
119
|
+
|
|
120
|
+
if active_send_mode:
|
|
121
|
+
self.client.message.send_image(
|
|
122
|
+
message_obj.sender.user_id,
|
|
123
|
+
response["media_id"],
|
|
124
|
+
)
|
|
125
|
+
else:
|
|
126
|
+
reply = ImageReply(
|
|
127
|
+
media_id=response["media_id"],
|
|
128
|
+
message=self.message_obj.raw_message["message"],
|
|
129
|
+
)
|
|
130
|
+
xml = reply.render()
|
|
131
|
+
future = self.message_obj.raw_message["future"]
|
|
132
|
+
assert isinstance(future, asyncio.Future)
|
|
133
|
+
future.set_result(xml)
|
|
134
|
+
|
|
135
|
+
elif isinstance(comp, Record):
|
|
136
|
+
record_path = await comp.convert_to_file_path()
|
|
137
|
+
# 转成amr
|
|
138
|
+
record_path_amr = f"data/temp/{uuid.uuid4()}.amr"
|
|
139
|
+
pydub.AudioSegment.from_wav(record_path).export(
|
|
140
|
+
record_path_amr,
|
|
141
|
+
format="amr",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
with open(record_path_amr, "rb") as f:
|
|
145
|
+
try:
|
|
146
|
+
response = self.client.media.upload("voice", f)
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.error(f"微信公众平台上传语音失败: {e}")
|
|
149
|
+
await self.send(
|
|
150
|
+
MessageChain().message(f"微信公众平台上传语音失败: {e}"),
|
|
151
|
+
)
|
|
152
|
+
return
|
|
153
|
+
logger.info(f"微信公众平台上传语音返回: {response}")
|
|
154
|
+
|
|
155
|
+
if active_send_mode:
|
|
156
|
+
self.client.message.send_voice(
|
|
157
|
+
message_obj.sender.user_id,
|
|
158
|
+
response["media_id"],
|
|
159
|
+
)
|
|
160
|
+
else:
|
|
161
|
+
reply = VoiceReply(
|
|
162
|
+
media_id=response["media_id"],
|
|
163
|
+
message=self.message_obj.raw_message["message"],
|
|
164
|
+
)
|
|
165
|
+
xml = reply.render()
|
|
166
|
+
future = self.message_obj.raw_message["future"]
|
|
167
|
+
assert isinstance(future, asyncio.Future)
|
|
168
|
+
future.set_result(xml)
|
|
169
|
+
|
|
170
|
+
else:
|
|
171
|
+
logger.warning(f"还没实现这个消息类型的发送逻辑: {comp.type}。")
|
|
172
|
+
|
|
173
|
+
await super().send(message)
|
|
174
|
+
|
|
175
|
+
async def send_streaming(self, generator, use_fallback: bool = False):
|
|
176
|
+
buffer = None
|
|
177
|
+
async for chain in generator:
|
|
178
|
+
if not buffer:
|
|
179
|
+
buffer = chain
|
|
180
|
+
else:
|
|
181
|
+
buffer.chain.extend(chain.chain)
|
|
182
|
+
if not buffer:
|
|
183
|
+
return None
|
|
184
|
+
buffer.squash_plain()
|
|
185
|
+
await self.send(buffer)
|
|
186
|
+
return await super().send_streaming(generator, use_fallback)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from astrbot.core.db import BaseDatabase
|
|
2
|
+
from astrbot.core.db.po import PlatformMessageHistory
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class PlatformMessageHistoryManager:
|
|
6
|
+
def __init__(self, db_helper: BaseDatabase):
|
|
7
|
+
self.db = db_helper
|
|
8
|
+
|
|
9
|
+
async def insert(
|
|
10
|
+
self,
|
|
11
|
+
platform_id: str,
|
|
12
|
+
user_id: str,
|
|
13
|
+
content: list[dict], # TODO: parse from message chain
|
|
14
|
+
sender_id: str | None = None,
|
|
15
|
+
sender_name: str | None = None,
|
|
16
|
+
):
|
|
17
|
+
"""Insert a new platform message history record."""
|
|
18
|
+
await self.db.insert_platform_message_history(
|
|
19
|
+
platform_id=platform_id,
|
|
20
|
+
user_id=user_id,
|
|
21
|
+
content=content,
|
|
22
|
+
sender_id=sender_id,
|
|
23
|
+
sender_name=sender_name,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
async def get(
|
|
27
|
+
self,
|
|
28
|
+
platform_id: str,
|
|
29
|
+
user_id: str,
|
|
30
|
+
page: int = 1,
|
|
31
|
+
page_size: int = 200,
|
|
32
|
+
) -> list[PlatformMessageHistory]:
|
|
33
|
+
"""Get platform message history for a specific user."""
|
|
34
|
+
history = await self.db.get_platform_message_history(
|
|
35
|
+
platform_id=platform_id,
|
|
36
|
+
user_id=user_id,
|
|
37
|
+
page=page,
|
|
38
|
+
page_size=page_size,
|
|
39
|
+
)
|
|
40
|
+
history.reverse()
|
|
41
|
+
return history
|
|
42
|
+
|
|
43
|
+
async def delete(self, platform_id: str, user_id: str, offset_sec: int = 86400):
|
|
44
|
+
"""Delete platform message history records older than the specified offset."""
|
|
45
|
+
await self.db.delete_platform_message_offset(
|
|
46
|
+
platform_id=platform_id,
|
|
47
|
+
user_id=user_id,
|
|
48
|
+
offset_sec=offset_sec,
|
|
49
|
+
)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
from .provider import Provider, Personality, STTProvider
|
|
2
|
-
|
|
3
1
|
from .entities import ProviderMetaData
|
|
2
|
+
from .provider import Provider, STTProvider
|
|
4
3
|
|
|
5
|
-
__all__ = ["Provider", "
|
|
4
|
+
__all__ = ["Provider", "ProviderMetaData", "STTProvider"]
|
astrbot/core/provider/entites.py
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
from astrbot.core.provider.entities import (
|
|
2
|
+
AssistantMessageSegment,
|
|
3
|
+
LLMResponse,
|
|
4
|
+
ProviderMetaData,
|
|
2
5
|
ProviderRequest,
|
|
3
6
|
ProviderType,
|
|
4
|
-
ProviderMetaData,
|
|
5
|
-
ToolCallsResult,
|
|
6
|
-
AssistantMessageSegment,
|
|
7
7
|
ToolCallMessageSegment,
|
|
8
|
-
|
|
8
|
+
ToolCallsResult,
|
|
9
9
|
)
|
|
10
10
|
|
|
11
11
|
__all__ = [
|
|
12
|
+
"AssistantMessageSegment",
|
|
13
|
+
"LLMResponse",
|
|
14
|
+
"ProviderMetaData",
|
|
12
15
|
"ProviderRequest",
|
|
13
16
|
"ProviderType",
|
|
14
|
-
"ProviderMetaData",
|
|
15
|
-
"ToolCallsResult",
|
|
16
|
-
"AssistantMessageSegment",
|
|
17
17
|
"ToolCallMessageSegment",
|
|
18
|
-
"
|
|
18
|
+
"ToolCallsResult",
|
|
19
19
|
]
|