AstrBot 4.8.0__py3-none-any.whl → 4.9.1__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/cli/__init__.py +1 -1
- astrbot/core/agent/runners/tool_loop_agent_runner.py +0 -1
- astrbot/core/agent/tool.py +7 -2
- astrbot/core/astr_agent_tool_exec.py +5 -1
- astrbot/core/config/astrbot_config.py +4 -0
- astrbot/core/config/default.py +72 -1
- astrbot/core/config/i18n_utils.py +1 -0
- astrbot/core/core_lifecycle.py +1 -1
- astrbot/core/db/__init__.py +2 -3
- astrbot/core/db/migration/migra_3_to_4.py +2 -0
- astrbot/core/db/migration/sqlite_v3.py +6 -4
- astrbot/core/db/po.py +16 -15
- astrbot/core/db/sqlite.py +4 -3
- astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +2 -0
- astrbot/core/event_bus.py +6 -1
- astrbot/core/knowledge_base/retrieval/manager.py +5 -1
- astrbot/core/log.py +2 -1
- astrbot/core/message/components.py +9 -3
- astrbot/core/persona_mgr.py +2 -2
- astrbot/core/pipeline/content_safety_check/stage.py +1 -1
- astrbot/core/pipeline/context_utils.py +2 -1
- astrbot/core/pipeline/process_stage/method/star_request.py +1 -2
- astrbot/core/pipeline/process_stage/stage.py +1 -1
- astrbot/core/pipeline/respond/stage.py +8 -2
- astrbot/core/pipeline/result_decorate/stage.py +89 -22
- astrbot/core/pipeline/scheduler.py +5 -1
- astrbot/core/pipeline/waking_check/stage.py +10 -0
- astrbot/core/platform/astr_message_event.py +5 -3
- astrbot/core/platform/astrbot_message.py +2 -2
- astrbot/core/platform/manager.py +4 -0
- astrbot/core/platform/platform.py +11 -3
- astrbot/core/platform/platform_metadata.py +1 -1
- astrbot/core/platform/register.py +1 -0
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +8 -6
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +9 -5
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +24 -16
- astrbot/core/platform/sources/dingtalk/dingtalk_event.py +5 -2
- astrbot/core/platform/sources/discord/client.py +16 -4
- astrbot/core/platform/sources/discord/components.py +2 -2
- astrbot/core/platform/sources/discord/discord_platform_adapter.py +52 -24
- astrbot/core/platform/sources/discord/discord_platform_event.py +29 -8
- astrbot/core/platform/sources/lark/lark_adapter.py +183 -20
- astrbot/core/platform/sources/lark/lark_event.py +39 -4
- astrbot/core/platform/sources/lark/server.py +206 -0
- astrbot/core/platform/sources/misskey/misskey_adapter.py +2 -3
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +62 -18
- astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +13 -7
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +5 -3
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +2 -1
- astrbot/core/platform/sources/slack/client.py +9 -2
- astrbot/core/platform/sources/slack/slack_adapter.py +15 -9
- astrbot/core/platform/sources/slack/slack_event.py +8 -7
- astrbot/core/platform/sources/telegram/tg_adapter.py +1 -1
- astrbot/core/platform/sources/telegram/tg_event.py +23 -27
- astrbot/core/platform/sources/webchat/webchat_adapter.py +2 -2
- astrbot/core/platform/sources/webchat/webchat_event.py +2 -2
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +26 -9
- astrbot/core/platform/sources/wecom/wecom_adapter.py +25 -28
- astrbot/core/platform/sources/wecom/wecom_event.py +2 -2
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +3 -3
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +30 -25
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +10 -7
- astrbot/core/provider/func_tool_manager.py +3 -3
- astrbot/core/provider/manager.py +130 -74
- astrbot/core/provider/provider.py +12 -1
- astrbot/core/provider/sources/azure_tts_source.py +31 -9
- astrbot/core/provider/sources/bailian_rerank_source.py +4 -0
- astrbot/core/provider/sources/dashscope_tts.py +3 -2
- astrbot/core/provider/sources/edge_tts_source.py +1 -1
- astrbot/core/provider/sources/fishaudio_tts_api_source.py +5 -4
- astrbot/core/provider/sources/gemini_embedding_source.py +15 -5
- astrbot/core/provider/sources/gemini_source.py +12 -10
- astrbot/core/provider/sources/minimax_tts_api_source.py +4 -2
- astrbot/core/provider/sources/openai_embedding_source.py +2 -2
- astrbot/core/provider/sources/openai_source.py +4 -0
- astrbot/core/provider/sources/sensevoice_selfhosted_source.py +5 -2
- astrbot/core/provider/sources/vllm_rerank_source.py +1 -0
- astrbot/core/provider/sources/whisper_api_source.py +1 -1
- astrbot/core/provider/sources/whisper_selfhosted_source.py +6 -2
- astrbot/core/provider/sources/xinference_rerank_source.py +10 -2
- astrbot/core/star/context.py +2 -2
- astrbot/core/star/register/star_handler.py +22 -5
- astrbot/core/star/star_handler.py +85 -4
- astrbot/core/updator.py +3 -3
- astrbot/core/utils/io.py +1 -1
- astrbot/core/utils/session_waiter.py +17 -10
- astrbot/core/utils/shared_preferences.py +32 -0
- astrbot/core/utils/t2i/__init__.py +2 -2
- astrbot/core/utils/t2i/local_strategy.py +25 -31
- astrbot/core/utils/tencent_record_helper.py +1 -1
- astrbot/core/utils/version_comparator.py +6 -3
- astrbot/core/utils/webhook_utils.py +19 -0
- astrbot/dashboard/routes/chat.py +14 -9
- astrbot/dashboard/routes/config.py +10 -20
- astrbot/dashboard/routes/conversation.py +91 -1
- astrbot/dashboard/routes/knowledge_base.py +253 -78
- astrbot/dashboard/routes/log.py +13 -8
- astrbot/dashboard/routes/platform.py +1 -1
- astrbot/dashboard/routes/plugin.py +113 -52
- astrbot/dashboard/routes/route.py +2 -0
- astrbot/dashboard/server.py +6 -3
- {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/METADATA +9 -1
- {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/RECORD +106 -105
- {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/WHEEL +0 -0
- {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/entry_points.txt +0 -0
- {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import random
|
|
1
2
|
import re
|
|
2
3
|
import time
|
|
3
4
|
import traceback
|
|
@@ -6,6 +7,7 @@ from collections.abc import AsyncGenerator
|
|
|
6
7
|
from astrbot.core import file_token_service, html_renderer, logger
|
|
7
8
|
from astrbot.core.message.components import At, File, Image, Node, Plain, Record, Reply
|
|
8
9
|
from astrbot.core.message.message_event_result import ResultContentType
|
|
10
|
+
from astrbot.core.pipeline.content_safety_check.stage import ContentSafetyCheckStage
|
|
9
11
|
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
|
10
12
|
from astrbot.core.platform.message_type import MessageType
|
|
11
13
|
from astrbot.core.star.session_llm_manager import SessionServiceManager
|
|
@@ -41,6 +43,18 @@ class ResultDecorateStage(Stage):
|
|
|
41
43
|
"forward_threshold"
|
|
42
44
|
]
|
|
43
45
|
|
|
46
|
+
trigger_probability = ctx.astrbot_config["provider_tts_settings"].get(
|
|
47
|
+
"trigger_probability",
|
|
48
|
+
1,
|
|
49
|
+
)
|
|
50
|
+
try:
|
|
51
|
+
self.tts_trigger_probability = max(
|
|
52
|
+
0.0,
|
|
53
|
+
min(float(trigger_probability), 1.0),
|
|
54
|
+
)
|
|
55
|
+
except (TypeError, ValueError):
|
|
56
|
+
self.tts_trigger_probability = 1.0
|
|
57
|
+
|
|
44
58
|
# 分段回复
|
|
45
59
|
self.words_count_threshold = int(
|
|
46
60
|
ctx.astrbot_config["platform_settings"]["segmented_reply"][
|
|
@@ -53,7 +67,22 @@ class ResultDecorateStage(Stage):
|
|
|
53
67
|
self.only_llm_result = ctx.astrbot_config["platform_settings"][
|
|
54
68
|
"segmented_reply"
|
|
55
69
|
]["only_llm_result"]
|
|
70
|
+
self.split_mode = ctx.astrbot_config["platform_settings"][
|
|
71
|
+
"segmented_reply"
|
|
72
|
+
].get("split_mode", "regex")
|
|
56
73
|
self.regex = ctx.astrbot_config["platform_settings"]["segmented_reply"]["regex"]
|
|
74
|
+
self.split_words = ctx.astrbot_config["platform_settings"][
|
|
75
|
+
"segmented_reply"
|
|
76
|
+
].get("split_words", ["。", "?", "!", "~", "…"])
|
|
77
|
+
if self.split_words:
|
|
78
|
+
escaped_words = sorted(
|
|
79
|
+
[re.escape(word) for word in self.split_words], key=len, reverse=True
|
|
80
|
+
)
|
|
81
|
+
self.split_words_pattern = re.compile(
|
|
82
|
+
f"(.*?({'|'.join(escaped_words)})|.+$)", re.DOTALL
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
self.split_words_pattern = None
|
|
57
86
|
self.content_cleanup_rule = ctx.astrbot_config["platform_settings"][
|
|
58
87
|
"segmented_reply"
|
|
59
88
|
]["content_cleanup_rule"]
|
|
@@ -69,6 +98,28 @@ class ResultDecorateStage(Stage):
|
|
|
69
98
|
self.content_safe_check_stage = stage_cls()
|
|
70
99
|
await self.content_safe_check_stage.initialize(ctx)
|
|
71
100
|
|
|
101
|
+
def _split_text_by_words(self, text: str) -> list[str]:
|
|
102
|
+
"""使用分段词列表分段文本"""
|
|
103
|
+
if not self.split_words_pattern:
|
|
104
|
+
return [text]
|
|
105
|
+
|
|
106
|
+
segments = self.split_words_pattern.findall(text)
|
|
107
|
+
result = []
|
|
108
|
+
for seg in segments:
|
|
109
|
+
if isinstance(seg, tuple):
|
|
110
|
+
content = seg[0]
|
|
111
|
+
if not isinstance(content, str):
|
|
112
|
+
continue
|
|
113
|
+
for word in self.split_words:
|
|
114
|
+
if content.endswith(word):
|
|
115
|
+
content = content[: -len(word)]
|
|
116
|
+
break
|
|
117
|
+
if content.strip():
|
|
118
|
+
result.append(content)
|
|
119
|
+
elif seg and seg.strip():
|
|
120
|
+
result.append(seg)
|
|
121
|
+
return result if result else [text]
|
|
122
|
+
|
|
72
123
|
async def process(
|
|
73
124
|
self,
|
|
74
125
|
event: AstrMessageEvent,
|
|
@@ -93,11 +144,13 @@ class ResultDecorateStage(Stage):
|
|
|
93
144
|
for comp in result.chain:
|
|
94
145
|
if isinstance(comp, Plain):
|
|
95
146
|
text += comp.text
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
147
|
+
|
|
148
|
+
if isinstance(self.content_safe_check_stage, ContentSafetyCheckStage):
|
|
149
|
+
async for _ in self.content_safe_check_stage.process(
|
|
150
|
+
event,
|
|
151
|
+
check_text=text,
|
|
152
|
+
):
|
|
153
|
+
yield
|
|
101
154
|
|
|
102
155
|
# 发送消息前事件钩子
|
|
103
156
|
handlers = star_handlers_registry.get_handlers_by_event_type(
|
|
@@ -114,7 +167,8 @@ class ResultDecorateStage(Stage):
|
|
|
114
167
|
"启用流式输出时,依赖发送消息前事件钩子的插件可能无法正常工作",
|
|
115
168
|
)
|
|
116
169
|
await handler.handler(event)
|
|
117
|
-
|
|
170
|
+
|
|
171
|
+
if (result := event.get_result()) is None or not result.chain:
|
|
118
172
|
logger.debug(
|
|
119
173
|
f"hook(on_decorating_result) -> {star_map[handler.handler_module_path].name} - {handler.handler_name} 将消息结果清空。",
|
|
120
174
|
)
|
|
@@ -161,21 +215,27 @@ class ResultDecorateStage(Stage):
|
|
|
161
215
|
# 不分段回复
|
|
162
216
|
new_chain.append(comp)
|
|
163
217
|
continue
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
218
|
+
|
|
219
|
+
# 根据 split_mode 选择分段方式
|
|
220
|
+
if self.split_mode == "words":
|
|
221
|
+
split_response = self._split_text_by_words(comp.text)
|
|
222
|
+
else: # regex 模式
|
|
223
|
+
try:
|
|
224
|
+
split_response = re.findall(
|
|
225
|
+
self.regex,
|
|
226
|
+
comp.text,
|
|
227
|
+
re.DOTALL | re.MULTILINE,
|
|
228
|
+
)
|
|
229
|
+
except re.error:
|
|
230
|
+
logger.error(
|
|
231
|
+
f"分段回复正则表达式错误,使用默认分段方式: {traceback.format_exc()}",
|
|
232
|
+
)
|
|
233
|
+
split_response = re.findall(
|
|
234
|
+
r".*?[。?!~…]+|.+$",
|
|
235
|
+
comp.text,
|
|
236
|
+
re.DOTALL | re.MULTILINE,
|
|
237
|
+
)
|
|
238
|
+
|
|
179
239
|
if not split_response:
|
|
180
240
|
new_chain.append(comp)
|
|
181
241
|
continue
|
|
@@ -199,7 +259,14 @@ class ResultDecorateStage(Stage):
|
|
|
199
259
|
and result.is_llm_result()
|
|
200
260
|
and SessionServiceManager.should_process_tts_request(event)
|
|
201
261
|
):
|
|
202
|
-
|
|
262
|
+
should_tts = self.tts_trigger_probability >= 1.0 or (
|
|
263
|
+
self.tts_trigger_probability > 0.0
|
|
264
|
+
and random.random() <= self.tts_trigger_probability
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if not should_tts:
|
|
268
|
+
logger.debug("跳过 TTS:触发概率未命中。")
|
|
269
|
+
elif not tts_provider:
|
|
203
270
|
logger.warning(
|
|
204
271
|
f"会话 {event.unified_msg_origin} 未配置文本转语音模型。",
|
|
205
272
|
)
|
|
@@ -2,6 +2,10 @@ from collections.abc import AsyncGenerator
|
|
|
2
2
|
|
|
3
3
|
from astrbot.core import logger
|
|
4
4
|
from astrbot.core.platform import AstrMessageEvent
|
|
5
|
+
from astrbot.core.platform.sources.webchat.webchat_event import WebChatMessageEvent
|
|
6
|
+
from astrbot.core.platform.sources.wecom_ai_bot.wecomai_event import (
|
|
7
|
+
WecomAIBotMessageEvent,
|
|
8
|
+
)
|
|
5
9
|
|
|
6
10
|
from . import STAGES_ORDER
|
|
7
11
|
from .context import PipelineContext
|
|
@@ -78,7 +82,7 @@ class PipelineScheduler:
|
|
|
78
82
|
await self._process_stages(event)
|
|
79
83
|
|
|
80
84
|
# 如果没有发送操作, 则发送一个空消息, 以便于后续的处理
|
|
81
|
-
if event
|
|
85
|
+
if isinstance(event, (WebChatMessageEvent, WecomAIBotMessageEvent)):
|
|
82
86
|
await event.send(None)
|
|
83
87
|
|
|
84
88
|
logger.debug("pipeline 执行完毕。")
|
|
@@ -50,6 +50,9 @@ class WakingCheckStage(Stage):
|
|
|
50
50
|
"ignore_at_all",
|
|
51
51
|
False,
|
|
52
52
|
)
|
|
53
|
+
self.disable_builtin_commands = self.ctx.astrbot_config.get(
|
|
54
|
+
"disable_builtin_commands", False
|
|
55
|
+
)
|
|
53
56
|
|
|
54
57
|
async def process(
|
|
55
58
|
self,
|
|
@@ -131,6 +134,13 @@ class WakingCheckStage(Stage):
|
|
|
131
134
|
EventType.AdapterMessageEvent,
|
|
132
135
|
plugins_name=event.plugins_name,
|
|
133
136
|
):
|
|
137
|
+
if (
|
|
138
|
+
self.disable_builtin_commands
|
|
139
|
+
and handler.handler_module_path == "packages.builtin_commands.main"
|
|
140
|
+
):
|
|
141
|
+
logger.debug("skipping builtin command")
|
|
142
|
+
continue
|
|
143
|
+
|
|
134
144
|
# filter 需满足 AND 逻辑关系
|
|
135
145
|
passed = True
|
|
136
146
|
permission_not_pass = False
|
|
@@ -153,7 +153,9 @@ class AstrMessageEvent(abc.ABC):
|
|
|
153
153
|
|
|
154
154
|
def get_sender_name(self) -> str:
|
|
155
155
|
"""获取消息发送者的名称。(可能会返回空字符串)"""
|
|
156
|
-
|
|
156
|
+
if isinstance(self.message_obj.sender.nickname, str):
|
|
157
|
+
return self.message_obj.sender.nickname
|
|
158
|
+
return ""
|
|
157
159
|
|
|
158
160
|
def set_extra(self, key, value):
|
|
159
161
|
"""设置额外的信息。"""
|
|
@@ -270,7 +272,7 @@ class AstrMessageEvent(abc.ABC):
|
|
|
270
272
|
"""
|
|
271
273
|
self.call_llm = call_llm
|
|
272
274
|
|
|
273
|
-
def get_result(self) -> MessageEventResult:
|
|
275
|
+
def get_result(self) -> MessageEventResult | None:
|
|
274
276
|
"""获取消息事件的结果。"""
|
|
275
277
|
return self._result
|
|
276
278
|
|
|
@@ -320,7 +322,7 @@ class AstrMessageEvent(abc.ABC):
|
|
|
320
322
|
self,
|
|
321
323
|
prompt: str,
|
|
322
324
|
func_tool_manager=None,
|
|
323
|
-
session_id: str =
|
|
325
|
+
session_id: str = "",
|
|
324
326
|
image_urls: list[str] | None = None,
|
|
325
327
|
contexts: list | None = None,
|
|
326
328
|
system_prompt: str = "",
|
|
@@ -54,7 +54,7 @@ class AstrBotMessage:
|
|
|
54
54
|
self_id: str # 机器人的识别id
|
|
55
55
|
session_id: str # 会话id。取决于 unique_session 的设置。
|
|
56
56
|
message_id: str # 消息id
|
|
57
|
-
group: Group # 群组
|
|
57
|
+
group: Group | None # 群组
|
|
58
58
|
sender: MessageMember # 发送者
|
|
59
59
|
message: list[BaseMessageComponent] # 消息链使用 Nakuru 的消息链格式
|
|
60
60
|
message_str: str # 最直观的纯文本消息字符串
|
|
@@ -78,7 +78,7 @@ class AstrBotMessage:
|
|
|
78
78
|
return ""
|
|
79
79
|
|
|
80
80
|
@group_id.setter
|
|
81
|
-
def group_id(self, value: str):
|
|
81
|
+
def group_id(self, value: str | None):
|
|
82
82
|
"""设置 group_id"""
|
|
83
83
|
if value:
|
|
84
84
|
if self.group:
|
astrbot/core/platform/manager.py
CHANGED
|
@@ -5,6 +5,7 @@ from asyncio import Queue
|
|
|
5
5
|
from astrbot.core import logger
|
|
6
6
|
from astrbot.core.config.astrbot_config import AstrBotConfig
|
|
7
7
|
from astrbot.core.star.star_handler import EventType, star_handlers_registry, star_map
|
|
8
|
+
from astrbot.core.utils.webhook_utils import ensure_platform_webhook_config
|
|
8
9
|
|
|
9
10
|
from .platform import Platform, PlatformStatus
|
|
10
11
|
from .register import platform_cls_map
|
|
@@ -18,6 +19,7 @@ class PlatformManager:
|
|
|
18
19
|
|
|
19
20
|
self._inst_map: dict[str, dict] = {}
|
|
20
21
|
|
|
22
|
+
self.astrbot_config = config
|
|
21
23
|
self.platforms_config = config["platform"]
|
|
22
24
|
self.settings = config["platform_settings"]
|
|
23
25
|
"""NOTE: 这里是 default 的配置文件,以保证最大的兼容性;
|
|
@@ -29,6 +31,8 @@ class PlatformManager:
|
|
|
29
31
|
"""初始化所有平台适配器"""
|
|
30
32
|
for platform in self.platforms_config:
|
|
31
33
|
try:
|
|
34
|
+
if ensure_platform_webhook_config(platform):
|
|
35
|
+
self.astrbot_config.save_config()
|
|
32
36
|
await self.load_platform(platform)
|
|
33
37
|
except Exception as e:
|
|
34
38
|
logger.error(f"初始化 {platform} 平台适配器失败: {e}")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
import uuid
|
|
3
3
|
from asyncio import Queue
|
|
4
|
-
from collections.abc import
|
|
4
|
+
from collections.abc import Coroutine
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from datetime import datetime
|
|
7
7
|
from enum import Enum
|
|
@@ -80,6 +80,13 @@ class Platform(abc.ABC):
|
|
|
80
80
|
if self._status == PlatformStatus.ERROR:
|
|
81
81
|
self._status = PlatformStatus.RUNNING
|
|
82
82
|
|
|
83
|
+
def unified_webhook(self) -> bool:
|
|
84
|
+
"""是否正在使用统一 Webhook 模式"""
|
|
85
|
+
return bool(
|
|
86
|
+
self.config.get("unified_webhook_mode", False)
|
|
87
|
+
and self.config.get("webhook_uuid")
|
|
88
|
+
)
|
|
89
|
+
|
|
83
90
|
def get_stats(self) -> dict:
|
|
84
91
|
"""获取平台统计信息"""
|
|
85
92
|
meta = self.meta()
|
|
@@ -97,10 +104,11 @@ class Platform(abc.ABC):
|
|
|
97
104
|
}
|
|
98
105
|
if self.last_error
|
|
99
106
|
else None,
|
|
107
|
+
"unified_webhook": self.unified_webhook(),
|
|
100
108
|
}
|
|
101
109
|
|
|
102
110
|
@abc.abstractmethod
|
|
103
|
-
def run(self) ->
|
|
111
|
+
def run(self) -> Coroutine[Any, Any, None]:
|
|
104
112
|
"""得到一个平台的运行实例,需要返回一个协程对象。"""
|
|
105
113
|
raise NotImplementedError
|
|
106
114
|
|
|
@@ -116,7 +124,7 @@ class Platform(abc.ABC):
|
|
|
116
124
|
self,
|
|
117
125
|
session: MessageSesion,
|
|
118
126
|
message_chain: MessageChain,
|
|
119
|
-
):
|
|
127
|
+
) -> None:
|
|
120
128
|
"""通过会话发送消息。该方法旨在让插件能够直接通过**可持久化的会话数据**发送消息,而不需要保存 event 对象。
|
|
121
129
|
|
|
122
130
|
异步方法。
|
|
@@ -70,16 +70,18 @@ class AiocqhttpMessageEvent(AstrMessageEvent):
|
|
|
70
70
|
bot: CQHttp,
|
|
71
71
|
event: Event | None,
|
|
72
72
|
is_group: bool,
|
|
73
|
-
session_id: str,
|
|
73
|
+
session_id: str | None,
|
|
74
74
|
messages: list[dict],
|
|
75
75
|
):
|
|
76
76
|
# session_id 必须是纯数字字符串
|
|
77
|
-
|
|
77
|
+
session_id_int = (
|
|
78
|
+
int(session_id) if session_id and session_id.isdigit() else None
|
|
79
|
+
)
|
|
78
80
|
|
|
79
|
-
if is_group and isinstance(
|
|
80
|
-
await bot.send_group_msg(group_id=
|
|
81
|
-
elif not is_group and isinstance(
|
|
82
|
-
await bot.send_private_msg(user_id=
|
|
81
|
+
if is_group and isinstance(session_id_int, int):
|
|
82
|
+
await bot.send_group_msg(group_id=session_id_int, message=messages)
|
|
83
|
+
elif not is_group and isinstance(session_id_int, int):
|
|
84
|
+
await bot.send_private_msg(user_id=session_id_int, message=messages)
|
|
83
85
|
elif isinstance(event, Event): # 最后兜底
|
|
84
86
|
await bot.send(event=event, message=messages)
|
|
85
87
|
else:
|
|
@@ -4,7 +4,7 @@ import logging
|
|
|
4
4
|
import time
|
|
5
5
|
import uuid
|
|
6
6
|
from collections.abc import Awaitable
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Any, cast
|
|
8
8
|
|
|
9
9
|
from aiocqhttp import CQHttp, Event
|
|
10
10
|
from aiocqhttp.exceptions import ActionFailed
|
|
@@ -48,7 +48,7 @@ class AiocqhttpAdapter(Platform):
|
|
|
48
48
|
self.metadata = PlatformMetadata(
|
|
49
49
|
name="aiocqhttp",
|
|
50
50
|
description="适用于 OneBot 标准的消息平台适配器,支持反向 WebSockets。",
|
|
51
|
-
id=self.config.get("id"),
|
|
51
|
+
id=cast(str, self.config.get("id")),
|
|
52
52
|
support_streaming_message=False,
|
|
53
53
|
)
|
|
54
54
|
|
|
@@ -127,7 +127,9 @@ class AiocqhttpAdapter(Platform):
|
|
|
127
127
|
"""OneBot V11 请求类事件"""
|
|
128
128
|
abm = AstrBotMessage()
|
|
129
129
|
abm.self_id = str(event.self_id)
|
|
130
|
-
abm.sender = MessageMember(
|
|
130
|
+
abm.sender = MessageMember(
|
|
131
|
+
user_id=str(event.user_id), nickname=str(event.user_id)
|
|
132
|
+
)
|
|
131
133
|
abm.type = MessageType.OTHER_MESSAGE
|
|
132
134
|
if event.get("group_id"):
|
|
133
135
|
abm.type = MessageType.GROUP_MESSAGE
|
|
@@ -194,6 +196,7 @@ class AiocqhttpAdapter(Platform):
|
|
|
194
196
|
@param event: 事件对象
|
|
195
197
|
@param get_reply: 是否获取回复消息。这个参数是为了防止多个回复嵌套。
|
|
196
198
|
"""
|
|
199
|
+
assert event.sender is not None
|
|
197
200
|
abm = AstrBotMessage()
|
|
198
201
|
abm.self_id = str(event.self_id)
|
|
199
202
|
abm.sender = MessageMember(
|
|
@@ -203,6 +206,7 @@ class AiocqhttpAdapter(Platform):
|
|
|
203
206
|
if event["message_type"] == "group":
|
|
204
207
|
abm.type = MessageType.GROUP_MESSAGE
|
|
205
208
|
abm.group_id = str(event.group_id)
|
|
209
|
+
abm.group = Group(str(event.group_id))
|
|
206
210
|
abm.group.group_name = event.get("group_name", "N/A")
|
|
207
211
|
elif event["message_type"] == "private":
|
|
208
212
|
abm.type = MessageType.FRIEND_MESSAGE
|
|
@@ -228,7 +232,7 @@ class AiocqhttpAdapter(Platform):
|
|
|
228
232
|
await self.bot.send(event, err)
|
|
229
233
|
except BaseException as e:
|
|
230
234
|
logger.error(f"回复消息失败: {e}")
|
|
231
|
-
|
|
235
|
+
raise ValueError(err)
|
|
232
236
|
|
|
233
237
|
# 按消息段类型类型适配
|
|
234
238
|
for t, m_group in itertools.groupby(event.message, key=lambda x: x["type"]):
|
|
@@ -417,7 +421,7 @@ class AiocqhttpAdapter(Platform):
|
|
|
417
421
|
|
|
418
422
|
async def shutdown_trigger_placeholder(self):
|
|
419
423
|
await self.shutdown_event.wait()
|
|
420
|
-
logger.info("aiocqhttp
|
|
424
|
+
logger.info("aiocqhttp 适配器已被关闭")
|
|
421
425
|
|
|
422
426
|
def meta(self) -> PlatformMetadata:
|
|
423
427
|
return self.metadata
|
|
@@ -2,6 +2,7 @@ import asyncio
|
|
|
2
2
|
import os
|
|
3
3
|
import threading
|
|
4
4
|
import uuid
|
|
5
|
+
from typing import cast
|
|
5
6
|
|
|
6
7
|
import aiohttp
|
|
7
8
|
import dingtalk_stream
|
|
@@ -54,12 +55,14 @@ class DingtalkPlatformAdapter(Platform):
|
|
|
54
55
|
self.client_id = platform_config["client_id"]
|
|
55
56
|
self.client_secret = platform_config["client_secret"]
|
|
56
57
|
|
|
58
|
+
outer_self = self
|
|
59
|
+
|
|
57
60
|
class AstrCallbackClient(dingtalk_stream.ChatbotHandler):
|
|
58
|
-
async def process(
|
|
61
|
+
async def process(self, message: dingtalk_stream.CallbackMessage):
|
|
59
62
|
logger.debug(f"dingtalk: {message.data}")
|
|
60
63
|
im = dingtalk_stream.ChatbotMessage.from_dict(message.data)
|
|
61
|
-
abm = await
|
|
62
|
-
await
|
|
64
|
+
abm = await outer_self.convert_msg(im)
|
|
65
|
+
await outer_self.handle_msg(abm)
|
|
63
66
|
|
|
64
67
|
return AckMessage.STATUS_OK, "OK"
|
|
65
68
|
|
|
@@ -73,6 +76,7 @@ class DingtalkPlatformAdapter(Platform):
|
|
|
73
76
|
self.client,
|
|
74
77
|
)
|
|
75
78
|
self.client_ = client # 用于 websockets 的 client
|
|
79
|
+
self._shutdown_event: threading.Event | None = None
|
|
76
80
|
|
|
77
81
|
def _id_to_sid(self, dingtalk_id: str | None) -> str:
|
|
78
82
|
if not dingtalk_id:
|
|
@@ -93,7 +97,7 @@ class DingtalkPlatformAdapter(Platform):
|
|
|
93
97
|
return PlatformMetadata(
|
|
94
98
|
name="dingtalk",
|
|
95
99
|
description="钉钉机器人官方 API 适配器",
|
|
96
|
-
id=self.config.get("id"),
|
|
100
|
+
id=cast(str, self.config.get("id")),
|
|
97
101
|
support_streaming_message=False,
|
|
98
102
|
)
|
|
99
103
|
|
|
@@ -104,7 +108,7 @@ class DingtalkPlatformAdapter(Platform):
|
|
|
104
108
|
abm = AstrBotMessage()
|
|
105
109
|
abm.message = []
|
|
106
110
|
abm.message_str = ""
|
|
107
|
-
abm.timestamp = int(message.create_at / 1000)
|
|
111
|
+
abm.timestamp = int(cast(int, message.create_at) / 1000)
|
|
108
112
|
abm.type = (
|
|
109
113
|
MessageType.GROUP_MESSAGE
|
|
110
114
|
if message.conversation_type == "2"
|
|
@@ -115,7 +119,7 @@ class DingtalkPlatformAdapter(Platform):
|
|
|
115
119
|
nickname=message.sender_nick,
|
|
116
120
|
)
|
|
117
121
|
abm.self_id = self._id_to_sid(message.chatbot_user_id)
|
|
118
|
-
abm.message_id = message.message_id
|
|
122
|
+
abm.message_id = cast(str, message.message_id)
|
|
119
123
|
abm.raw_message = message
|
|
120
124
|
|
|
121
125
|
if abm.type == MessageType.GROUP_MESSAGE:
|
|
@@ -132,14 +136,16 @@ class DingtalkPlatformAdapter(Platform):
|
|
|
132
136
|
else:
|
|
133
137
|
abm.session_id = abm.sender.user_id
|
|
134
138
|
|
|
135
|
-
message_type: str = message.message_type
|
|
139
|
+
message_type: str = cast(str, message.message_type)
|
|
136
140
|
match message_type:
|
|
137
141
|
case "text":
|
|
138
142
|
abm.message_str = message.text.content.strip()
|
|
139
143
|
abm.message.append(Plain(abm.message_str))
|
|
140
144
|
case "richText":
|
|
141
|
-
rtc: dingtalk_stream.RichTextContent =
|
|
142
|
-
|
|
145
|
+
rtc: dingtalk_stream.RichTextContent = cast(
|
|
146
|
+
dingtalk_stream.RichTextContent, message.rich_text_content
|
|
147
|
+
)
|
|
148
|
+
contents: list[dict] = cast(list[dict], rtc.rich_text_list)
|
|
143
149
|
for content in contents:
|
|
144
150
|
plains = ""
|
|
145
151
|
if "text" in content:
|
|
@@ -148,7 +154,7 @@ class DingtalkPlatformAdapter(Platform):
|
|
|
148
154
|
elif "type" in content and content["type"] == "picture":
|
|
149
155
|
f_path = await self.download_ding_file(
|
|
150
156
|
content["downloadCode"],
|
|
151
|
-
message.robot_code,
|
|
157
|
+
cast(str, message.robot_code),
|
|
152
158
|
"jpg",
|
|
153
159
|
)
|
|
154
160
|
abm.message.append(Image.fromFileSystem(f_path))
|
|
@@ -193,7 +199,7 @@ class DingtalkPlatformAdapter(Platform):
|
|
|
193
199
|
logger.error(
|
|
194
200
|
f"下载钉钉文件失败: {resp.status}, {await resp.text()}",
|
|
195
201
|
)
|
|
196
|
-
return
|
|
202
|
+
return ""
|
|
197
203
|
resp_data = await resp.json()
|
|
198
204
|
download_url = resp_data["data"]["downloadUrl"]
|
|
199
205
|
await download_file(download_url, f_path)
|
|
@@ -213,7 +219,7 @@ class DingtalkPlatformAdapter(Platform):
|
|
|
213
219
|
logger.error(
|
|
214
220
|
f"获取钉钉机器人 access_token 失败: {resp.status}, {await resp.text()}",
|
|
215
221
|
)
|
|
216
|
-
return
|
|
222
|
+
return ""
|
|
217
223
|
return (await resp.json())["data"]["accessToken"]
|
|
218
224
|
|
|
219
225
|
async def handle_msg(self, abm: AstrBotMessage):
|
|
@@ -239,7 +245,7 @@ class DingtalkPlatformAdapter(Platform):
|
|
|
239
245
|
task.result()
|
|
240
246
|
except Exception as e:
|
|
241
247
|
if "Graceful shutdown" in str(e):
|
|
242
|
-
logger.info("
|
|
248
|
+
logger.info("钉钉适配器已被关闭")
|
|
243
249
|
return
|
|
244
250
|
logger.error(f"钉钉机器人启动失败: {e}")
|
|
245
251
|
|
|
@@ -250,9 +256,11 @@ class DingtalkPlatformAdapter(Platform):
|
|
|
250
256
|
def monkey_patch_close():
|
|
251
257
|
raise KeyboardInterrupt("Graceful shutdown")
|
|
252
258
|
|
|
253
|
-
self.client_.
|
|
254
|
-
|
|
255
|
-
|
|
259
|
+
if self.client_.websocket is not None:
|
|
260
|
+
self.client_.open_connection = monkey_patch_close
|
|
261
|
+
await self.client_.websocket.close(code=1000, reason="Graceful shutdown")
|
|
262
|
+
if self._shutdown_event is not None:
|
|
263
|
+
self._shutdown_event.set()
|
|
256
264
|
|
|
257
265
|
def get_client(self):
|
|
258
266
|
return self.client
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from typing import cast
|
|
2
3
|
|
|
3
4
|
import dingtalk_stream
|
|
4
5
|
|
|
@@ -32,7 +33,7 @@ class DingtalkMessageEvent(AstrMessageEvent):
|
|
|
32
33
|
client.reply_markdown,
|
|
33
34
|
segment.text,
|
|
34
35
|
segment.text,
|
|
35
|
-
self.message_obj.raw_message,
|
|
36
|
+
cast(dingtalk_stream.ChatbotMessage, self.message_obj.raw_message),
|
|
36
37
|
)
|
|
37
38
|
elif isinstance(segment, Comp.Image):
|
|
38
39
|
markdown_str = ""
|
|
@@ -53,7 +54,9 @@ class DingtalkMessageEvent(AstrMessageEvent):
|
|
|
53
54
|
client.reply_markdown,
|
|
54
55
|
"😄",
|
|
55
56
|
markdown_str,
|
|
56
|
-
|
|
57
|
+
cast(
|
|
58
|
+
dingtalk_stream.ChatbotMessage, self.message_obj.raw_message
|
|
59
|
+
),
|
|
57
60
|
)
|
|
58
61
|
logger.debug(f"send image: {ret}")
|
|
59
62
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import sys
|
|
2
|
+
from collections.abc import Awaitable, Callable
|
|
2
3
|
|
|
3
4
|
import discord
|
|
4
5
|
|
|
@@ -27,13 +28,16 @@ class DiscordBotClient(discord.Bot):
|
|
|
27
28
|
super().__init__(intents=intents, proxy=proxy)
|
|
28
29
|
|
|
29
30
|
# 回调函数
|
|
30
|
-
self.on_message_received = None
|
|
31
|
-
self.on_ready_once_callback = None
|
|
31
|
+
self.on_message_received: Callable[[dict], Awaitable[None]] | None = None
|
|
32
|
+
self.on_ready_once_callback: Callable[[], Awaitable[None]] | None = None
|
|
32
33
|
self._ready_once_fired = False
|
|
33
34
|
|
|
34
|
-
@override
|
|
35
35
|
async def on_ready(self):
|
|
36
36
|
"""当机器人成功连接并准备就绪时触发"""
|
|
37
|
+
if self.user is None:
|
|
38
|
+
logger.error("[Discord] 客户端未正确加载用户信息 (self.user is None)")
|
|
39
|
+
return
|
|
40
|
+
|
|
37
41
|
logger.info(f"[Discord] 已作为 {self.user} (ID: {self.user.id}) 登录")
|
|
38
42
|
logger.info("[Discord] 客户端已准备就绪。")
|
|
39
43
|
|
|
@@ -49,6 +53,9 @@ class DiscordBotClient(discord.Bot):
|
|
|
49
53
|
|
|
50
54
|
def _create_message_data(self, message: discord.Message) -> dict:
|
|
51
55
|
"""从 discord.Message 创建数据字典"""
|
|
56
|
+
if self.user is None:
|
|
57
|
+
raise RuntimeError("Bot is not ready: self.user is None")
|
|
58
|
+
|
|
52
59
|
is_mentioned = self.user in message.mentions
|
|
53
60
|
return {
|
|
54
61
|
"message": message,
|
|
@@ -66,6 +73,12 @@ class DiscordBotClient(discord.Bot):
|
|
|
66
73
|
|
|
67
74
|
def _create_interaction_data(self, interaction: discord.Interaction) -> dict:
|
|
68
75
|
"""从 discord.Interaction 创建数据字典"""
|
|
76
|
+
if self.user is None:
|
|
77
|
+
raise RuntimeError("Bot is not ready: self.user is None")
|
|
78
|
+
|
|
79
|
+
if interaction.user is None:
|
|
80
|
+
raise ValueError("Interaction received without a valid user")
|
|
81
|
+
|
|
69
82
|
return {
|
|
70
83
|
"interaction": interaction,
|
|
71
84
|
"bot_id": str(self.user.id),
|
|
@@ -80,7 +93,6 @@ class DiscordBotClient(discord.Bot):
|
|
|
80
93
|
"type": "interaction",
|
|
81
94
|
}
|
|
82
95
|
|
|
83
|
-
@override
|
|
84
96
|
async def on_message(self, message: discord.Message):
|
|
85
97
|
"""当接收到消息时触发"""
|
|
86
98
|
if message.author.bot:
|
|
@@ -97,8 +97,8 @@ class DiscordView(BaseMessageComponent):
|
|
|
97
97
|
|
|
98
98
|
def __init__(
|
|
99
99
|
self,
|
|
100
|
-
components: list[BaseMessageComponent] = None,
|
|
101
|
-
timeout: float = None,
|
|
100
|
+
components: list[BaseMessageComponent] | None = None,
|
|
101
|
+
timeout: float | None = None,
|
|
102
102
|
):
|
|
103
103
|
self.components = components or []
|
|
104
104
|
self.timeout = timeout
|