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,432 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from astrbot.api import logger
|
|
4
|
+
from astrbot.api.event import AstrMessageEvent, MessageChain
|
|
5
|
+
from astrbot.api.message_components import (
|
|
6
|
+
At,
|
|
7
|
+
File,
|
|
8
|
+
Forward,
|
|
9
|
+
Image,
|
|
10
|
+
Node,
|
|
11
|
+
Nodes,
|
|
12
|
+
Plain,
|
|
13
|
+
Record,
|
|
14
|
+
Reply,
|
|
15
|
+
Video,
|
|
16
|
+
)
|
|
17
|
+
from astrbot.api.platform import AstrBotMessage, PlatformMetadata
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from .satori_adapter import SatoriPlatformAdapter
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SatoriPlatformEvent(AstrMessageEvent):
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
message_str: str,
|
|
27
|
+
message_obj: AstrBotMessage,
|
|
28
|
+
platform_meta: PlatformMetadata,
|
|
29
|
+
session_id: str,
|
|
30
|
+
adapter: "SatoriPlatformAdapter",
|
|
31
|
+
):
|
|
32
|
+
# 更新平台元数据
|
|
33
|
+
if adapter and hasattr(adapter, "logins") and adapter.logins:
|
|
34
|
+
current_login = adapter.logins[0]
|
|
35
|
+
platform_name = current_login.get("platform", "satori")
|
|
36
|
+
user = current_login.get("user", {})
|
|
37
|
+
user_id = user.get("id", "") if user else ""
|
|
38
|
+
if not platform_meta.id and user_id:
|
|
39
|
+
platform_meta.id = f"{platform_name}({user_id})"
|
|
40
|
+
|
|
41
|
+
super().__init__(message_str, message_obj, platform_meta, session_id)
|
|
42
|
+
self.adapter = adapter
|
|
43
|
+
self.platform = None
|
|
44
|
+
self.user_id = None
|
|
45
|
+
if (
|
|
46
|
+
hasattr(message_obj, "raw_message")
|
|
47
|
+
and message_obj.raw_message
|
|
48
|
+
and isinstance(message_obj.raw_message, dict)
|
|
49
|
+
):
|
|
50
|
+
login = message_obj.raw_message.get("login", {})
|
|
51
|
+
self.platform = login.get("platform")
|
|
52
|
+
user = login.get("user", {})
|
|
53
|
+
self.user_id = user.get("id") if user else None
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
async def send_with_adapter(
|
|
57
|
+
cls,
|
|
58
|
+
adapter: "SatoriPlatformAdapter",
|
|
59
|
+
message: MessageChain,
|
|
60
|
+
session_id: str,
|
|
61
|
+
):
|
|
62
|
+
try:
|
|
63
|
+
content_parts = []
|
|
64
|
+
|
|
65
|
+
for component in message.chain:
|
|
66
|
+
component_content = await cls._convert_component_to_satori_static(
|
|
67
|
+
component,
|
|
68
|
+
)
|
|
69
|
+
if component_content:
|
|
70
|
+
content_parts.append(component_content)
|
|
71
|
+
|
|
72
|
+
# 特殊处理 Node 和 Nodes 组件
|
|
73
|
+
if isinstance(component, Node):
|
|
74
|
+
# 单个转发节点
|
|
75
|
+
node_content = await cls._convert_node_to_satori_static(component)
|
|
76
|
+
if node_content:
|
|
77
|
+
content_parts.append(node_content)
|
|
78
|
+
|
|
79
|
+
elif isinstance(component, Nodes):
|
|
80
|
+
# 合并转发消息
|
|
81
|
+
node_content = await cls._convert_nodes_to_satori_static(component)
|
|
82
|
+
if node_content:
|
|
83
|
+
content_parts.append(node_content)
|
|
84
|
+
|
|
85
|
+
content = "".join(content_parts)
|
|
86
|
+
channel_id = session_id
|
|
87
|
+
data = {"channel_id": channel_id, "content": content}
|
|
88
|
+
|
|
89
|
+
platform = None
|
|
90
|
+
user_id = None
|
|
91
|
+
|
|
92
|
+
if hasattr(adapter, "logins") and adapter.logins:
|
|
93
|
+
current_login = adapter.logins[0]
|
|
94
|
+
platform = current_login.get("platform", "")
|
|
95
|
+
user = current_login.get("user", {})
|
|
96
|
+
user_id = user.get("id", "") if user else ""
|
|
97
|
+
|
|
98
|
+
result = await adapter.send_http_request(
|
|
99
|
+
"POST",
|
|
100
|
+
"/message.create",
|
|
101
|
+
data,
|
|
102
|
+
platform,
|
|
103
|
+
user_id,
|
|
104
|
+
)
|
|
105
|
+
if result:
|
|
106
|
+
return result
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"Satori 消息发送异常: {e}")
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
async def send(self, message: MessageChain):
|
|
114
|
+
platform = getattr(self, "platform", None)
|
|
115
|
+
user_id = getattr(self, "user_id", None)
|
|
116
|
+
|
|
117
|
+
if not platform or not user_id:
|
|
118
|
+
if hasattr(self.adapter, "logins") and self.adapter.logins:
|
|
119
|
+
current_login = self.adapter.logins[0]
|
|
120
|
+
platform = current_login.get("platform", "")
|
|
121
|
+
user = current_login.get("user", {})
|
|
122
|
+
user_id = user.get("id", "") if user else ""
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
content_parts = []
|
|
126
|
+
|
|
127
|
+
for component in message.chain:
|
|
128
|
+
component_content = await self._convert_component_to_satori(component)
|
|
129
|
+
if component_content:
|
|
130
|
+
content_parts.append(component_content)
|
|
131
|
+
|
|
132
|
+
# 特殊处理 Node 和 Nodes 组件
|
|
133
|
+
if isinstance(component, Node):
|
|
134
|
+
# 单个转发节点
|
|
135
|
+
node_content = await self._convert_node_to_satori(component)
|
|
136
|
+
if node_content:
|
|
137
|
+
content_parts.append(node_content)
|
|
138
|
+
|
|
139
|
+
elif isinstance(component, Nodes):
|
|
140
|
+
# 合并转发消息
|
|
141
|
+
node_content = await self._convert_nodes_to_satori(component)
|
|
142
|
+
if node_content:
|
|
143
|
+
content_parts.append(node_content)
|
|
144
|
+
|
|
145
|
+
content = "".join(content_parts)
|
|
146
|
+
channel_id = self.session_id
|
|
147
|
+
data = {"channel_id": channel_id, "content": content}
|
|
148
|
+
|
|
149
|
+
result = await self.adapter.send_http_request(
|
|
150
|
+
"POST",
|
|
151
|
+
"/message.create",
|
|
152
|
+
data,
|
|
153
|
+
platform,
|
|
154
|
+
user_id,
|
|
155
|
+
)
|
|
156
|
+
if not result:
|
|
157
|
+
logger.error("Satori 消息发送失败")
|
|
158
|
+
except Exception as e:
|
|
159
|
+
logger.error(f"Satori 消息发送异常: {e}")
|
|
160
|
+
|
|
161
|
+
await super().send(message)
|
|
162
|
+
|
|
163
|
+
async def send_streaming(self, generator, use_fallback: bool = False):
|
|
164
|
+
try:
|
|
165
|
+
content_parts = []
|
|
166
|
+
|
|
167
|
+
async for chain in generator:
|
|
168
|
+
if isinstance(chain, MessageChain):
|
|
169
|
+
if chain.type == "break":
|
|
170
|
+
if content_parts:
|
|
171
|
+
content = "".join(content_parts)
|
|
172
|
+
temp_chain = MessageChain([Plain(text=content)])
|
|
173
|
+
await self.send(temp_chain)
|
|
174
|
+
content_parts = []
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
for component in chain.chain:
|
|
178
|
+
if isinstance(component, Plain):
|
|
179
|
+
content_parts.append(component.text)
|
|
180
|
+
elif isinstance(component, Image):
|
|
181
|
+
if content_parts:
|
|
182
|
+
content = "".join(content_parts)
|
|
183
|
+
temp_chain = MessageChain([Plain(text=content)])
|
|
184
|
+
await self.send(temp_chain)
|
|
185
|
+
content_parts = []
|
|
186
|
+
try:
|
|
187
|
+
image_base64 = await component.convert_to_base64()
|
|
188
|
+
if image_base64:
|
|
189
|
+
img_chain = MessageChain(
|
|
190
|
+
[
|
|
191
|
+
Plain(
|
|
192
|
+
text=f'<img src="data:image/jpeg;base64,{image_base64}"/>',
|
|
193
|
+
),
|
|
194
|
+
],
|
|
195
|
+
)
|
|
196
|
+
await self.send(img_chain)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
logger.error(f"图片转换为base64失败: {e}")
|
|
199
|
+
else:
|
|
200
|
+
content_parts.append(str(component))
|
|
201
|
+
|
|
202
|
+
if content_parts:
|
|
203
|
+
content = "".join(content_parts)
|
|
204
|
+
temp_chain = MessageChain([Plain(text=content)])
|
|
205
|
+
await self.send(temp_chain)
|
|
206
|
+
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.error(f"Satori 流式消息发送异常: {e}")
|
|
209
|
+
|
|
210
|
+
return await super().send_streaming(generator, use_fallback)
|
|
211
|
+
|
|
212
|
+
async def _convert_component_to_satori(self, component) -> str:
|
|
213
|
+
"""将单个消息组件转换为 Satori 格式"""
|
|
214
|
+
try:
|
|
215
|
+
if isinstance(component, Plain):
|
|
216
|
+
text = (
|
|
217
|
+
component.text.replace("&", "&")
|
|
218
|
+
.replace("<", "<")
|
|
219
|
+
.replace(">", ">")
|
|
220
|
+
)
|
|
221
|
+
return text
|
|
222
|
+
|
|
223
|
+
if isinstance(component, At):
|
|
224
|
+
if component.qq:
|
|
225
|
+
return f'<at id="{component.qq}"/>'
|
|
226
|
+
if component.name:
|
|
227
|
+
return f'<at name="{component.name}"/>'
|
|
228
|
+
|
|
229
|
+
elif isinstance(component, Image):
|
|
230
|
+
try:
|
|
231
|
+
image_base64 = await component.convert_to_base64()
|
|
232
|
+
if image_base64:
|
|
233
|
+
return f'<img src="data:image/jpeg;base64,{image_base64}"/>'
|
|
234
|
+
except Exception as e:
|
|
235
|
+
logger.error(f"图片转换为base64失败: {e}")
|
|
236
|
+
|
|
237
|
+
elif isinstance(component, File):
|
|
238
|
+
return (
|
|
239
|
+
f'<file src="{component.file}" name="{component.name or "文件"}"/>'
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
elif isinstance(component, Record):
|
|
243
|
+
try:
|
|
244
|
+
record_base64 = await component.convert_to_base64()
|
|
245
|
+
if record_base64:
|
|
246
|
+
return f'<audio src="data:audio/wav;base64,{record_base64}"/>'
|
|
247
|
+
except Exception as e:
|
|
248
|
+
logger.error(f"语音转换为base64失败: {e}")
|
|
249
|
+
|
|
250
|
+
elif isinstance(component, Reply):
|
|
251
|
+
return f'<reply id="{component.id}"/>'
|
|
252
|
+
|
|
253
|
+
elif isinstance(component, Video):
|
|
254
|
+
try:
|
|
255
|
+
video_path_url = await component.convert_to_file_path()
|
|
256
|
+
if video_path_url:
|
|
257
|
+
return f'<video src="{video_path_url}"/>'
|
|
258
|
+
except Exception as e:
|
|
259
|
+
logger.error(f"视频文件转换失败: {e}")
|
|
260
|
+
|
|
261
|
+
elif isinstance(component, Forward):
|
|
262
|
+
return f'<message id="{component.id}" forward/>'
|
|
263
|
+
|
|
264
|
+
# 对于其他未处理的组件类型,返回空字符串
|
|
265
|
+
return ""
|
|
266
|
+
|
|
267
|
+
except Exception as e:
|
|
268
|
+
logger.error(f"转换消息组件失败: {e}")
|
|
269
|
+
return ""
|
|
270
|
+
|
|
271
|
+
async def _convert_node_to_satori(self, node: Node) -> str:
|
|
272
|
+
"""将单个转发节点转换为 Satori 格式"""
|
|
273
|
+
try:
|
|
274
|
+
content_parts = []
|
|
275
|
+
if node.content:
|
|
276
|
+
for content_component in node.content:
|
|
277
|
+
component_content = await self._convert_component_to_satori(
|
|
278
|
+
content_component,
|
|
279
|
+
)
|
|
280
|
+
if component_content:
|
|
281
|
+
content_parts.append(component_content)
|
|
282
|
+
|
|
283
|
+
content = "".join(content_parts)
|
|
284
|
+
|
|
285
|
+
# 如果内容为空,添加默认内容
|
|
286
|
+
if not content.strip():
|
|
287
|
+
content = "[转发消息]"
|
|
288
|
+
|
|
289
|
+
# 构建 Satori 格式的转发节点
|
|
290
|
+
author_attrs = []
|
|
291
|
+
if node.uin:
|
|
292
|
+
author_attrs.append(f'id="{node.uin}"')
|
|
293
|
+
if node.name:
|
|
294
|
+
author_attrs.append(f'name="{node.name}"')
|
|
295
|
+
|
|
296
|
+
author_attr_str = " ".join(author_attrs)
|
|
297
|
+
|
|
298
|
+
return f"<message><author {author_attr_str}/>{content}</message>"
|
|
299
|
+
|
|
300
|
+
except Exception as e:
|
|
301
|
+
logger.error(f"转换转发节点失败: {e}")
|
|
302
|
+
return ""
|
|
303
|
+
|
|
304
|
+
@classmethod
|
|
305
|
+
async def _convert_component_to_satori_static(cls, component) -> str:
|
|
306
|
+
"""将单个消息组件转换为 Satori 格式"""
|
|
307
|
+
try:
|
|
308
|
+
if isinstance(component, Plain):
|
|
309
|
+
text = (
|
|
310
|
+
component.text.replace("&", "&")
|
|
311
|
+
.replace("<", "<")
|
|
312
|
+
.replace(">", ">")
|
|
313
|
+
)
|
|
314
|
+
return text
|
|
315
|
+
|
|
316
|
+
if isinstance(component, At):
|
|
317
|
+
if component.qq:
|
|
318
|
+
return f'<at id="{component.qq}"/>'
|
|
319
|
+
if component.name:
|
|
320
|
+
return f'<at name="{component.name}"/>'
|
|
321
|
+
|
|
322
|
+
elif isinstance(component, Image):
|
|
323
|
+
try:
|
|
324
|
+
image_base64 = await component.convert_to_base64()
|
|
325
|
+
if image_base64:
|
|
326
|
+
return f'<img src="data:image/jpeg;base64,{image_base64}"/>'
|
|
327
|
+
except Exception as e:
|
|
328
|
+
logger.error(f"图片转换为base64失败: {e}")
|
|
329
|
+
|
|
330
|
+
elif isinstance(component, File):
|
|
331
|
+
return (
|
|
332
|
+
f'<file src="{component.file}" name="{component.name or "文件"}"/>'
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
elif isinstance(component, Record):
|
|
336
|
+
try:
|
|
337
|
+
record_base64 = await component.convert_to_base64()
|
|
338
|
+
if record_base64:
|
|
339
|
+
return f'<audio src="data:audio/wav;base64,{record_base64}"/>'
|
|
340
|
+
except Exception as e:
|
|
341
|
+
logger.error(f"语音转换为base64失败: {e}")
|
|
342
|
+
|
|
343
|
+
elif isinstance(component, Reply):
|
|
344
|
+
return f'<reply id="{component.id}"/>'
|
|
345
|
+
|
|
346
|
+
elif isinstance(component, Video):
|
|
347
|
+
try:
|
|
348
|
+
video_path_url = await component.convert_to_file_path()
|
|
349
|
+
if video_path_url:
|
|
350
|
+
return f'<video src="{video_path_url}"/>'
|
|
351
|
+
except Exception as e:
|
|
352
|
+
logger.error(f"视频文件转换失败: {e}")
|
|
353
|
+
|
|
354
|
+
elif isinstance(component, Forward):
|
|
355
|
+
return f'<message id="{component.id}" forward/>'
|
|
356
|
+
|
|
357
|
+
# 对于其他未处理的组件类型,返回空字符串
|
|
358
|
+
return ""
|
|
359
|
+
|
|
360
|
+
except Exception as e:
|
|
361
|
+
logger.error(f"转换消息组件失败: {e}")
|
|
362
|
+
return ""
|
|
363
|
+
|
|
364
|
+
@classmethod
|
|
365
|
+
async def _convert_node_to_satori_static(cls, node: Node) -> str:
|
|
366
|
+
"""将单个转发节点转换为 Satori 格式"""
|
|
367
|
+
try:
|
|
368
|
+
content_parts = []
|
|
369
|
+
if node.content:
|
|
370
|
+
for content_component in node.content:
|
|
371
|
+
component_content = await cls._convert_component_to_satori_static(
|
|
372
|
+
content_component,
|
|
373
|
+
)
|
|
374
|
+
if component_content:
|
|
375
|
+
content_parts.append(component_content)
|
|
376
|
+
|
|
377
|
+
content = "".join(content_parts)
|
|
378
|
+
|
|
379
|
+
# 如果内容为空,添加默认内容
|
|
380
|
+
if not content.strip():
|
|
381
|
+
content = "[转发消息]"
|
|
382
|
+
|
|
383
|
+
author_attrs = []
|
|
384
|
+
if node.uin:
|
|
385
|
+
author_attrs.append(f'id="{node.uin}"')
|
|
386
|
+
if node.name:
|
|
387
|
+
author_attrs.append(f'name="{node.name}"')
|
|
388
|
+
|
|
389
|
+
author_attr_str = " ".join(author_attrs)
|
|
390
|
+
|
|
391
|
+
return f"<message><author {author_attr_str}/>{content}</message>"
|
|
392
|
+
|
|
393
|
+
except Exception as e:
|
|
394
|
+
logger.error(f"转换转发节点失败: {e}")
|
|
395
|
+
return ""
|
|
396
|
+
|
|
397
|
+
async def _convert_nodes_to_satori(self, nodes: Nodes) -> str:
|
|
398
|
+
"""将多个转发节点转换为 Satori 格式的合并转发"""
|
|
399
|
+
try:
|
|
400
|
+
node_parts = []
|
|
401
|
+
|
|
402
|
+
for node in nodes.nodes:
|
|
403
|
+
node_content = await self._convert_node_to_satori(node)
|
|
404
|
+
if node_content:
|
|
405
|
+
node_parts.append(node_content)
|
|
406
|
+
|
|
407
|
+
if node_parts:
|
|
408
|
+
return f"<message forward>{''.join(node_parts)}</message>"
|
|
409
|
+
return ""
|
|
410
|
+
|
|
411
|
+
except Exception as e:
|
|
412
|
+
logger.error(f"转换合并转发消息失败: {e}")
|
|
413
|
+
return ""
|
|
414
|
+
|
|
415
|
+
@classmethod
|
|
416
|
+
async def _convert_nodes_to_satori_static(cls, nodes: Nodes) -> str:
|
|
417
|
+
"""将多个转发节点转换为 Satori 格式的合并转发"""
|
|
418
|
+
try:
|
|
419
|
+
node_parts = []
|
|
420
|
+
|
|
421
|
+
for node in nodes.nodes:
|
|
422
|
+
node_content = await cls._convert_node_to_satori_static(node)
|
|
423
|
+
if node_content:
|
|
424
|
+
node_parts.append(node_content)
|
|
425
|
+
|
|
426
|
+
if node_parts:
|
|
427
|
+
return f"<message forward>{''.join(node_parts)}</message>"
|
|
428
|
+
return ""
|
|
429
|
+
|
|
430
|
+
except Exception as e:
|
|
431
|
+
logger.error(f"转换合并转发消息失败: {e}")
|
|
432
|
+
return ""
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import hashlib
|
|
3
|
+
import hmac
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
|
|
8
|
+
from quart import Quart, Response, request
|
|
9
|
+
from slack_sdk.socket_mode.aiohttp import SocketModeClient
|
|
10
|
+
from slack_sdk.socket_mode.request import SocketModeRequest
|
|
11
|
+
from slack_sdk.socket_mode.response import SocketModeResponse
|
|
12
|
+
from slack_sdk.web.async_client import AsyncWebClient
|
|
13
|
+
|
|
14
|
+
from astrbot.api import logger
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SlackWebhookClient:
|
|
18
|
+
"""Slack Webhook 模式客户端,使用 Quart 作为 Web 服务器"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
web_client: AsyncWebClient,
|
|
23
|
+
signing_secret: str,
|
|
24
|
+
host: str = "0.0.0.0",
|
|
25
|
+
port: int = 3000,
|
|
26
|
+
path: str = "/slack/events",
|
|
27
|
+
event_handler: Callable | None = None,
|
|
28
|
+
):
|
|
29
|
+
self.web_client = web_client
|
|
30
|
+
self.signing_secret = signing_secret
|
|
31
|
+
self.host = host
|
|
32
|
+
self.port = port
|
|
33
|
+
self.path = path
|
|
34
|
+
self.event_handler = event_handler
|
|
35
|
+
|
|
36
|
+
self.app = Quart(__name__)
|
|
37
|
+
self._setup_routes()
|
|
38
|
+
|
|
39
|
+
# 禁用 Quart 的默认日志输出
|
|
40
|
+
logging.getLogger("quart.app").setLevel(logging.WARNING)
|
|
41
|
+
logging.getLogger("quart.serving").setLevel(logging.WARNING)
|
|
42
|
+
|
|
43
|
+
self.shutdown_event = asyncio.Event()
|
|
44
|
+
|
|
45
|
+
def _setup_routes(self):
|
|
46
|
+
"""设置路由"""
|
|
47
|
+
|
|
48
|
+
@self.app.route(self.path, methods=["POST"])
|
|
49
|
+
async def slack_events():
|
|
50
|
+
"""处理 Slack 事件"""
|
|
51
|
+
try:
|
|
52
|
+
# 获取请求体和头部
|
|
53
|
+
body = await request.get_data()
|
|
54
|
+
event_data = json.loads(body.decode("utf-8"))
|
|
55
|
+
|
|
56
|
+
# Verify Slack request signature
|
|
57
|
+
timestamp = request.headers.get("X-Slack-Request-Timestamp")
|
|
58
|
+
signature = request.headers.get("X-Slack-Signature")
|
|
59
|
+
if not timestamp or not signature:
|
|
60
|
+
return Response("Missing headers", status=400)
|
|
61
|
+
# Calculate the HMAC signature
|
|
62
|
+
sig_basestring = f"v0:{timestamp}:{body.decode('utf-8')}"
|
|
63
|
+
my_signature = (
|
|
64
|
+
"v0="
|
|
65
|
+
+ hmac.new(
|
|
66
|
+
self.signing_secret.encode("utf-8"),
|
|
67
|
+
sig_basestring.encode("utf-8"),
|
|
68
|
+
hashlib.sha256,
|
|
69
|
+
).hexdigest()
|
|
70
|
+
)
|
|
71
|
+
# Verify the signature
|
|
72
|
+
if not hmac.compare_digest(my_signature, signature):
|
|
73
|
+
logger.warning("Slack request signature verification failed")
|
|
74
|
+
return Response("Invalid signature", status=400)
|
|
75
|
+
logger.info(f"Received Slack event: {event_data}")
|
|
76
|
+
|
|
77
|
+
# 处理 URL 验证事件
|
|
78
|
+
if event_data.get("type") == "url_verification":
|
|
79
|
+
return {"challenge": event_data.get("challenge")}
|
|
80
|
+
# 处理事件
|
|
81
|
+
if self.event_handler and event_data.get("type") == "event_callback":
|
|
82
|
+
await self.event_handler(event_data)
|
|
83
|
+
|
|
84
|
+
return Response("", status=200)
|
|
85
|
+
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.error(f"处理 Slack 事件时出错: {e}")
|
|
88
|
+
return Response("Internal Server Error", status=500)
|
|
89
|
+
|
|
90
|
+
@self.app.route("/health", methods=["GET"])
|
|
91
|
+
async def health_check():
|
|
92
|
+
"""健康检查端点"""
|
|
93
|
+
return {"status": "ok", "service": "slack-webhook"}
|
|
94
|
+
|
|
95
|
+
async def start(self):
|
|
96
|
+
"""启动 Webhook 服务器"""
|
|
97
|
+
logger.info(
|
|
98
|
+
f"Slack Webhook 服务器启动中,监听 {self.host}:{self.port}{self.path}...",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
await self.app.run_task(
|
|
102
|
+
host=self.host,
|
|
103
|
+
port=self.port,
|
|
104
|
+
debug=False,
|
|
105
|
+
shutdown_trigger=self.shutdown_trigger,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
async def shutdown_trigger(self):
|
|
109
|
+
await self.shutdown_event.wait()
|
|
110
|
+
|
|
111
|
+
async def stop(self):
|
|
112
|
+
"""停止 Webhook 服务器"""
|
|
113
|
+
self.shutdown_event.set()
|
|
114
|
+
logger.info("Slack Webhook 服务器已停止")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class SlackSocketClient:
|
|
118
|
+
"""Slack Socket 模式客户端"""
|
|
119
|
+
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
web_client: AsyncWebClient,
|
|
123
|
+
app_token: str,
|
|
124
|
+
event_handler: Callable | None = None,
|
|
125
|
+
):
|
|
126
|
+
self.web_client = web_client
|
|
127
|
+
self.app_token = app_token
|
|
128
|
+
self.event_handler = event_handler
|
|
129
|
+
self.socket_client = None
|
|
130
|
+
|
|
131
|
+
async def _handle_events(self, _: SocketModeClient, req: SocketModeRequest):
|
|
132
|
+
"""处理 Socket Mode 事件"""
|
|
133
|
+
try:
|
|
134
|
+
# 确认收到事件
|
|
135
|
+
response = SocketModeResponse(envelope_id=req.envelope_id)
|
|
136
|
+
await self.socket_client.send_socket_mode_response(response)
|
|
137
|
+
|
|
138
|
+
# 处理事件
|
|
139
|
+
if self.event_handler:
|
|
140
|
+
await self.event_handler(req)
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.error(f"处理 Socket Mode 事件时出错: {e}")
|
|
144
|
+
|
|
145
|
+
async def start(self):
|
|
146
|
+
"""启动 Socket Mode 连接"""
|
|
147
|
+
self.socket_client = SocketModeClient(
|
|
148
|
+
app_token=self.app_token,
|
|
149
|
+
logger=logger,
|
|
150
|
+
web_client=self.web_client,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# 注册事件处理器
|
|
154
|
+
self.socket_client.socket_mode_request_listeners.append(self._handle_events)
|
|
155
|
+
|
|
156
|
+
logger.info("Slack Socket Mode 客户端启动中...")
|
|
157
|
+
await self.socket_client.connect()
|
|
158
|
+
|
|
159
|
+
async def stop(self):
|
|
160
|
+
"""停止 Socket Mode 连接"""
|
|
161
|
+
if self.socket_client:
|
|
162
|
+
await self.socket_client.disconnect()
|
|
163
|
+
await self.socket_client.close()
|
|
164
|
+
logger.info("Slack Socket Mode 客户端已停止")
|