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
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""飞书(Lark) Webhook 服务器实现
|
|
2
|
+
|
|
3
|
+
实现飞书事件订阅的 Webhook 模式,支持:
|
|
4
|
+
1. 请求 URL 验证 (challenge 验证)
|
|
5
|
+
2. 事件加密/解密 (AES-256-CBC)
|
|
6
|
+
3. 签名校验 (SHA256)
|
|
7
|
+
4. 事件接收和处理
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import base64
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
from collections.abc import Awaitable, Callable
|
|
15
|
+
|
|
16
|
+
from Crypto.Cipher import AES
|
|
17
|
+
|
|
18
|
+
from astrbot.api import logger
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AESCipher:
|
|
22
|
+
"""AES 加密/解密工具类"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, key: str):
|
|
25
|
+
self.bs = AES.block_size
|
|
26
|
+
self.key = hashlib.sha256(self.str_to_bytes(key)).digest()
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def str_to_bytes(data):
|
|
30
|
+
u_type = type(b"".decode("utf8"))
|
|
31
|
+
if isinstance(data, u_type):
|
|
32
|
+
return data.encode("utf8")
|
|
33
|
+
return data
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def _unpad(s):
|
|
37
|
+
return s[: -ord(s[len(s) - 1 :])]
|
|
38
|
+
|
|
39
|
+
def decrypt(self, enc):
|
|
40
|
+
iv = enc[: AES.block_size]
|
|
41
|
+
cipher = AES.new(self.key, AES.MODE_CBC, iv)
|
|
42
|
+
return self._unpad(cipher.decrypt(enc[AES.block_size :]))
|
|
43
|
+
|
|
44
|
+
def decrypt_string(self, enc):
|
|
45
|
+
enc = base64.b64decode(enc)
|
|
46
|
+
return self.decrypt(enc).decode("utf8")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class LarkWebhookServer:
|
|
50
|
+
"""飞书 Webhook 服务器
|
|
51
|
+
|
|
52
|
+
仅支持统一 Webhook 模式
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, config: dict, event_queue: asyncio.Queue):
|
|
56
|
+
"""初始化 Webhook 服务器
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
config: 飞书配置
|
|
60
|
+
event_queue: 事件队列
|
|
61
|
+
"""
|
|
62
|
+
self.app_id = config["app_id"]
|
|
63
|
+
self.app_secret = config["app_secret"]
|
|
64
|
+
self.encrypt_key = config.get("lark_encrypt_key", "")
|
|
65
|
+
self.verification_token = config.get("lark_verification_token", "")
|
|
66
|
+
|
|
67
|
+
self.event_queue = event_queue
|
|
68
|
+
self.callback: Callable[[dict], Awaitable[None]] | None = None
|
|
69
|
+
|
|
70
|
+
# 初始化加密工具
|
|
71
|
+
self.cipher = None
|
|
72
|
+
if self.encrypt_key:
|
|
73
|
+
self.cipher = AESCipher(self.encrypt_key)
|
|
74
|
+
|
|
75
|
+
def verify_signature(
|
|
76
|
+
self,
|
|
77
|
+
timestamp: str,
|
|
78
|
+
nonce: str,
|
|
79
|
+
encrypt_key: str,
|
|
80
|
+
body: bytes,
|
|
81
|
+
signature: str,
|
|
82
|
+
) -> bool:
|
|
83
|
+
"""验证签名
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
timestamp: 请求时间戳
|
|
87
|
+
nonce: 随机数
|
|
88
|
+
encrypt_key: 加密密钥
|
|
89
|
+
body: 请求体
|
|
90
|
+
signature: 签名
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
签名是否有效
|
|
94
|
+
"""
|
|
95
|
+
# 拼接字符串: timestamp + nonce + encrypt_key + body
|
|
96
|
+
bytes_b1 = (timestamp + nonce + encrypt_key).encode("utf-8")
|
|
97
|
+
bytes_b = bytes_b1 + body
|
|
98
|
+
h = hashlib.sha256(bytes_b)
|
|
99
|
+
calculated_signature = h.hexdigest()
|
|
100
|
+
return calculated_signature == signature
|
|
101
|
+
|
|
102
|
+
def decrypt_event(self, encrypted_data: str) -> dict:
|
|
103
|
+
"""解密事件数据
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
encrypted_data: 加密的事件数据
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
解密后的事件字典
|
|
110
|
+
"""
|
|
111
|
+
if not self.cipher:
|
|
112
|
+
raise ValueError("未配置 encrypt_key,无法解密事件")
|
|
113
|
+
|
|
114
|
+
decrypted_str = self.cipher.decrypt_string(encrypted_data)
|
|
115
|
+
return json.loads(decrypted_str)
|
|
116
|
+
|
|
117
|
+
async def handle_challenge(self, event_data: dict) -> dict:
|
|
118
|
+
"""处理 challenge 验证请求
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
event_data: 事件数据
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
包含 challenge 的响应
|
|
125
|
+
"""
|
|
126
|
+
challenge = event_data.get("challenge", "")
|
|
127
|
+
logger.info(f"[Lark Webhook] 收到 challenge 验证请求: {challenge}")
|
|
128
|
+
|
|
129
|
+
return {"challenge": challenge}
|
|
130
|
+
|
|
131
|
+
async def handle_callback(self, request) -> tuple[dict, int] | dict:
|
|
132
|
+
"""处理 webhook 回调,可被统一 webhook 入口复用
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
request: Quart 请求对象
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
响应数据
|
|
139
|
+
"""
|
|
140
|
+
# 获取原始请求体
|
|
141
|
+
body = await request.get_data()
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
event_data = await request.json
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.error(f"[Lark Webhook] 解析请求体失败: {e}")
|
|
147
|
+
return {"error": "Invalid JSON"}, 400
|
|
148
|
+
|
|
149
|
+
if not event_data:
|
|
150
|
+
logger.error("[Lark Webhook] 请求体为空")
|
|
151
|
+
return {"error": "Empty request body"}, 400
|
|
152
|
+
|
|
153
|
+
# 如果配置了 encrypt_key,进行签名验证
|
|
154
|
+
if self.encrypt_key:
|
|
155
|
+
timestamp = request.headers.get("X-Lark-Request-Timestamp", "")
|
|
156
|
+
nonce = request.headers.get("X-Lark-Request-Nonce", "")
|
|
157
|
+
signature = request.headers.get("X-Lark-Signature", "")
|
|
158
|
+
|
|
159
|
+
if timestamp and nonce and signature:
|
|
160
|
+
if not self.verify_signature(
|
|
161
|
+
timestamp, nonce, self.encrypt_key, body, signature
|
|
162
|
+
):
|
|
163
|
+
logger.error("[Lark Webhook] 签名验证失败")
|
|
164
|
+
return {"error": "Invalid signature"}, 401
|
|
165
|
+
|
|
166
|
+
# 检查是否是加密事件
|
|
167
|
+
if "encrypt" in event_data:
|
|
168
|
+
try:
|
|
169
|
+
event_data = self.decrypt_event(event_data["encrypt"])
|
|
170
|
+
logger.debug(f"[Lark Webhook] 解密后的事件: {event_data}")
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.error(f"[Lark Webhook] 解密事件失败: {e}")
|
|
173
|
+
return {"error": "Decryption failed"}, 400
|
|
174
|
+
|
|
175
|
+
# 验证 token
|
|
176
|
+
if self.verification_token:
|
|
177
|
+
header = event_data.get("header", {})
|
|
178
|
+
if header:
|
|
179
|
+
token = header.get("token", "")
|
|
180
|
+
else:
|
|
181
|
+
token = event_data.get("token", "")
|
|
182
|
+
if token != self.verification_token:
|
|
183
|
+
logger.error("[Lark Webhook] Verification Token 不匹配。")
|
|
184
|
+
return {"error": "Invalid verification token"}, 401
|
|
185
|
+
|
|
186
|
+
# 处理 URL 验证 (challenge)
|
|
187
|
+
if event_data.get("type") == "url_verification":
|
|
188
|
+
return await self.handle_challenge(event_data)
|
|
189
|
+
|
|
190
|
+
# 调用回调函数处理事件
|
|
191
|
+
if self.callback:
|
|
192
|
+
try:
|
|
193
|
+
await self.callback(event_data)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
logger.error(f"[Lark Webhook] 处理事件回调失败: {e}", exc_info=True)
|
|
196
|
+
return {"error": "Event processing failed"}, 500
|
|
197
|
+
|
|
198
|
+
return {}
|
|
199
|
+
|
|
200
|
+
def set_callback(self, callback: Callable[[dict], Awaitable[None]]):
|
|
201
|
+
"""设置事件回调函数
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
callback: 处理事件的异步函数
|
|
205
|
+
"""
|
|
206
|
+
self.callback = callback
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import os
|
|
3
3
|
import random
|
|
4
|
-
from collections.abc import Awaitable
|
|
5
4
|
from typing import Any
|
|
6
5
|
|
|
7
6
|
import astrbot.api.message_components as Comp
|
|
@@ -203,7 +202,7 @@ class MisskeyPlatformAdapter(Platform):
|
|
|
203
202
|
if not isinstance(message.raw_message, dict):
|
|
204
203
|
message.raw_message = {}
|
|
205
204
|
message.raw_message["poll"] = poll
|
|
206
|
-
message.poll
|
|
205
|
+
message.__setattr__("poll", poll)
|
|
207
206
|
except Exception:
|
|
208
207
|
pass
|
|
209
208
|
|
|
@@ -372,7 +371,7 @@ class MisskeyPlatformAdapter(Platform):
|
|
|
372
371
|
self,
|
|
373
372
|
session: MessageSession,
|
|
374
373
|
message_chain: MessageChain,
|
|
375
|
-
) ->
|
|
374
|
+
) -> None:
|
|
376
375
|
if not self.api:
|
|
377
376
|
logger.error("[Misskey] API 客户端未初始化")
|
|
378
377
|
return await super().send_by_session(session, message_chain)
|
|
@@ -3,6 +3,7 @@ import base64
|
|
|
3
3
|
import os
|
|
4
4
|
import random
|
|
5
5
|
import uuid
|
|
6
|
+
from typing import cast
|
|
6
7
|
|
|
7
8
|
import aiofiles
|
|
8
9
|
import botpy
|
|
@@ -60,7 +61,10 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
60
61
|
time_since_last_edit = current_time - last_edit_time
|
|
61
62
|
|
|
62
63
|
if time_since_last_edit >= throttle_interval:
|
|
63
|
-
ret =
|
|
64
|
+
ret = cast(
|
|
65
|
+
message.Message,
|
|
66
|
+
await self._post_send(stream=stream_payload),
|
|
67
|
+
)
|
|
64
68
|
stream_payload["index"] += 1
|
|
65
69
|
stream_payload["id"] = ret["id"]
|
|
66
70
|
last_edit_time = asyncio.get_event_loop().time()
|
|
@@ -83,7 +87,8 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
83
87
|
return None
|
|
84
88
|
|
|
85
89
|
source = self.message_obj.raw_message
|
|
86
|
-
|
|
90
|
+
|
|
91
|
+
if not isinstance(
|
|
87
92
|
source,
|
|
88
93
|
(
|
|
89
94
|
botpy.message.Message,
|
|
@@ -91,7 +96,9 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
91
96
|
botpy.message.DirectMessage,
|
|
92
97
|
botpy.message.C2CMessage,
|
|
93
98
|
),
|
|
94
|
-
)
|
|
99
|
+
):
|
|
100
|
+
logger.warning(f"[QQOfficial] 不支持的消息源类型: {type(source)}")
|
|
101
|
+
return None
|
|
95
102
|
|
|
96
103
|
(
|
|
97
104
|
plain_text,
|
|
@@ -108,7 +115,7 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
108
115
|
):
|
|
109
116
|
return None
|
|
110
117
|
|
|
111
|
-
payload = {
|
|
118
|
+
payload: dict = {
|
|
112
119
|
"content": plain_text,
|
|
113
120
|
"msg_id": self.message_obj.message_id,
|
|
114
121
|
}
|
|
@@ -118,8 +125,12 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
118
125
|
|
|
119
126
|
ret = None
|
|
120
127
|
|
|
121
|
-
match
|
|
122
|
-
case botpy.message.GroupMessage:
|
|
128
|
+
match source:
|
|
129
|
+
case botpy.message.GroupMessage():
|
|
130
|
+
if not source.group_openid:
|
|
131
|
+
logger.error("[QQOfficial] GroupMessage 缺少 group_openid")
|
|
132
|
+
return None
|
|
133
|
+
|
|
123
134
|
if image_base64:
|
|
124
135
|
media = await self.upload_group_and_c2c_image(
|
|
125
136
|
image_base64,
|
|
@@ -140,7 +151,8 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
140
151
|
group_openid=source.group_openid,
|
|
141
152
|
**payload,
|
|
142
153
|
)
|
|
143
|
-
|
|
154
|
+
|
|
155
|
+
case botpy.message.C2CMessage():
|
|
144
156
|
if image_base64:
|
|
145
157
|
media = await self.upload_group_and_c2c_image(
|
|
146
158
|
image_base64,
|
|
@@ -169,18 +181,23 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
169
181
|
**payload,
|
|
170
182
|
)
|
|
171
183
|
logger.debug(f"Message sent to C2C: {ret}")
|
|
172
|
-
|
|
184
|
+
|
|
185
|
+
case botpy.message.Message():
|
|
173
186
|
if image_path:
|
|
174
187
|
payload["file_image"] = image_path
|
|
175
188
|
ret = await self.bot.api.post_message(
|
|
176
189
|
channel_id=source.channel_id,
|
|
177
190
|
**payload,
|
|
178
191
|
)
|
|
179
|
-
|
|
192
|
+
|
|
193
|
+
case botpy.message.DirectMessage():
|
|
180
194
|
if image_path:
|
|
181
195
|
payload["file_image"] = image_path
|
|
182
196
|
ret = await self.bot.api.post_dms(guild_id=source.guild_id, **payload)
|
|
183
197
|
|
|
198
|
+
case _:
|
|
199
|
+
pass
|
|
200
|
+
|
|
184
201
|
await super().send(self.send_buffer)
|
|
185
202
|
|
|
186
203
|
self.send_buffer = None
|
|
@@ -198,18 +215,33 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
198
215
|
"file_type": file_type,
|
|
199
216
|
"srv_send_msg": False,
|
|
200
217
|
}
|
|
218
|
+
|
|
219
|
+
result = None
|
|
201
220
|
if "openid" in kwargs:
|
|
202
221
|
payload["openid"] = kwargs["openid"]
|
|
203
222
|
route = Route("POST", "/v2/users/{openid}/files", openid=kwargs["openid"])
|
|
204
|
-
|
|
205
|
-
|
|
223
|
+
result = await self.bot.api._http.request(route, json=payload)
|
|
224
|
+
elif "group_openid" in kwargs:
|
|
206
225
|
payload["group_openid"] = kwargs["group_openid"]
|
|
207
226
|
route = Route(
|
|
208
227
|
"POST",
|
|
209
228
|
"/v2/groups/{group_openid}/files",
|
|
210
229
|
group_openid=kwargs["group_openid"],
|
|
211
230
|
)
|
|
212
|
-
|
|
231
|
+
result = await self.bot.api._http.request(route, json=payload)
|
|
232
|
+
else:
|
|
233
|
+
raise ValueError("Invalid upload parameters")
|
|
234
|
+
|
|
235
|
+
if not isinstance(result, dict):
|
|
236
|
+
raise RuntimeError(
|
|
237
|
+
f"Failed to upload image, response is not dict: {result}"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
return Media(
|
|
241
|
+
file_uuid=result["file_uuid"],
|
|
242
|
+
file_info=result["file_info"],
|
|
243
|
+
ttl=result.get("ttl", 0),
|
|
244
|
+
)
|
|
213
245
|
|
|
214
246
|
async def upload_group_and_c2c_record(
|
|
215
247
|
self,
|
|
@@ -252,11 +284,14 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
252
284
|
result = await self.bot.api._http.request(route, json=payload)
|
|
253
285
|
|
|
254
286
|
if result:
|
|
287
|
+
if not isinstance(result, dict):
|
|
288
|
+
logger.error(f"上传文件响应格式错误: {result}")
|
|
289
|
+
return None
|
|
290
|
+
|
|
255
291
|
return Media(
|
|
256
|
-
file_uuid=result
|
|
257
|
-
file_info=result
|
|
292
|
+
file_uuid=result["file_uuid"],
|
|
293
|
+
file_info=result["file_info"],
|
|
258
294
|
ttl=result.get("ttl", 0),
|
|
259
|
-
file_id=result.get("id", ""),
|
|
260
295
|
)
|
|
261
296
|
except Exception as e:
|
|
262
297
|
logger.error(f"上传请求错误: {e}")
|
|
@@ -273,7 +308,7 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
273
308
|
message_reference: message.Reference | None = None,
|
|
274
309
|
media: message.Media | None = None,
|
|
275
310
|
msg_id: str | None = None,
|
|
276
|
-
msg_seq:
|
|
311
|
+
msg_seq: int | None = 1,
|
|
277
312
|
event_id: str | None = None,
|
|
278
313
|
markdown: message.MarkdownPayload | None = None,
|
|
279
314
|
keyboard: message.Keyboard | None = None,
|
|
@@ -282,7 +317,14 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
282
317
|
payload = locals()
|
|
283
318
|
payload.pop("self", None)
|
|
284
319
|
route = Route("POST", "/v2/users/{openid}/messages", openid=openid)
|
|
285
|
-
|
|
320
|
+
result = await self.bot.api._http.request(route, json=payload)
|
|
321
|
+
|
|
322
|
+
if not isinstance(result, dict):
|
|
323
|
+
raise RuntimeError(
|
|
324
|
+
f"Failed to post c2c message, response is not dict: {result}"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
return message.Message(**result)
|
|
286
328
|
|
|
287
329
|
@staticmethod
|
|
288
330
|
async def _parse_to_qqofficial(message: MessageChain):
|
|
@@ -302,8 +344,10 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
302
344
|
image_base64 = file_to_base64(image_file_path)
|
|
303
345
|
elif i.file and i.file.startswith("base64://"):
|
|
304
346
|
image_base64 = i.file
|
|
305
|
-
|
|
347
|
+
elif i.file:
|
|
306
348
|
image_base64 = file_to_base64(i.file)
|
|
349
|
+
else:
|
|
350
|
+
raise ValueError("Unsupported image file format")
|
|
307
351
|
image_base64 = image_base64.removeprefix("base64://")
|
|
308
352
|
elif isinstance(i, Record):
|
|
309
353
|
if i.file:
|
|
@@ -4,6 +4,7 @@ import asyncio
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import time
|
|
7
|
+
from typing import cast
|
|
7
8
|
|
|
8
9
|
import botpy
|
|
9
10
|
import botpy.message
|
|
@@ -44,7 +45,9 @@ class botClient(Client):
|
|
|
44
45
|
MessageType.GROUP_MESSAGE,
|
|
45
46
|
)
|
|
46
47
|
abm.session_id = (
|
|
47
|
-
abm.sender.user_id
|
|
48
|
+
abm.sender.user_id
|
|
49
|
+
if self.platform.unique_session
|
|
50
|
+
else cast(str, message.group_openid)
|
|
48
51
|
)
|
|
49
52
|
self._commit(abm)
|
|
50
53
|
|
|
@@ -101,7 +104,7 @@ class QQOfficialPlatformAdapter(Platform):
|
|
|
101
104
|
|
|
102
105
|
self.appid = platform_config["appid"]
|
|
103
106
|
self.secret = platform_config["secret"]
|
|
104
|
-
self.unique_session = platform_settings["unique_session"]
|
|
107
|
+
self.unique_session: bool = platform_settings["unique_session"]
|
|
105
108
|
qq_group = platform_config["enable_group_c2c"]
|
|
106
109
|
guild_dm = platform_config["enable_guild_direct_message"]
|
|
107
110
|
|
|
@@ -137,12 +140,15 @@ class QQOfficialPlatformAdapter(Platform):
|
|
|
137
140
|
return PlatformMetadata(
|
|
138
141
|
name="qq_official",
|
|
139
142
|
description="QQ 机器人官方 API 适配器",
|
|
140
|
-
id=self.config.get("id"),
|
|
143
|
+
id=cast(str, self.config.get("id")),
|
|
141
144
|
)
|
|
142
145
|
|
|
143
146
|
@staticmethod
|
|
144
147
|
def _parse_from_qqofficial(
|
|
145
|
-
message: botpy.message.Message
|
|
148
|
+
message: botpy.message.Message
|
|
149
|
+
| botpy.message.GroupMessage
|
|
150
|
+
| botpy.message.DirectMessage
|
|
151
|
+
| botpy.message.C2CMessage,
|
|
146
152
|
message_type: MessageType,
|
|
147
153
|
):
|
|
148
154
|
abm = AstrBotMessage()
|
|
@@ -150,7 +156,7 @@ class QQOfficialPlatformAdapter(Platform):
|
|
|
150
156
|
abm.timestamp = int(time.time())
|
|
151
157
|
abm.raw_message = message
|
|
152
158
|
abm.message_id = message.id
|
|
153
|
-
abm.tag = "qq_official"
|
|
159
|
+
# abm.tag = "qq_official"
|
|
154
160
|
msg: list[BaseMessageComponent] = []
|
|
155
161
|
|
|
156
162
|
if isinstance(message, botpy.message.GroupMessage) or isinstance(
|
|
@@ -180,9 +186,9 @@ class QQOfficialPlatformAdapter(Platform):
|
|
|
180
186
|
message,
|
|
181
187
|
botpy.message.DirectMessage,
|
|
182
188
|
):
|
|
183
|
-
|
|
189
|
+
if isinstance(message, botpy.message.Message):
|
|
184
190
|
abm.self_id = str(message.mentions[0].id)
|
|
185
|
-
|
|
191
|
+
else:
|
|
186
192
|
abm.self_id = ""
|
|
187
193
|
|
|
188
194
|
plain_content = message.content.replace(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any, cast
|
|
4
4
|
|
|
5
5
|
import botpy
|
|
6
6
|
import botpy.message
|
|
@@ -36,7 +36,9 @@ class botClient(Client):
|
|
|
36
36
|
MessageType.GROUP_MESSAGE,
|
|
37
37
|
)
|
|
38
38
|
abm.session_id = (
|
|
39
|
-
abm.sender.user_id
|
|
39
|
+
abm.sender.user_id
|
|
40
|
+
if self.platform.unique_session
|
|
41
|
+
else cast(str, message.group_openid)
|
|
40
42
|
)
|
|
41
43
|
self._commit(abm)
|
|
42
44
|
|
|
@@ -120,7 +122,7 @@ class QQOfficialWebhookPlatformAdapter(Platform):
|
|
|
120
122
|
return PlatformMetadata(
|
|
121
123
|
name="qq_official_webhook",
|
|
122
124
|
description="QQ 机器人官方 API 适配器",
|
|
123
|
-
id=self.config.get("id"),
|
|
125
|
+
id=cast(str, self.config.get("id")),
|
|
124
126
|
)
|
|
125
127
|
|
|
126
128
|
async def run(self):
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
|
+
from typing import cast
|
|
3
4
|
|
|
4
5
|
import quart
|
|
5
6
|
from botpy import BotAPI, BotHttp, BotWebSocket, Client, ConnectionSession, Token
|
|
@@ -99,7 +100,7 @@ class QQOfficialWebhook:
|
|
|
99
100
|
|
|
100
101
|
if opcode == 13:
|
|
101
102
|
# validation
|
|
102
|
-
signed = await self.webhook_validation(data)
|
|
103
|
+
signed = await self.webhook_validation(cast(dict, data))
|
|
103
104
|
print(signed)
|
|
104
105
|
return signed
|
|
105
106
|
|
|
@@ -4,9 +4,11 @@ import hmac
|
|
|
4
4
|
import json
|
|
5
5
|
import logging
|
|
6
6
|
from collections.abc import Callable
|
|
7
|
+
from typing import cast
|
|
7
8
|
|
|
8
9
|
from quart import Quart, Response, request
|
|
9
10
|
from slack_sdk.socket_mode.aiohttp import SocketModeClient
|
|
11
|
+
from slack_sdk.socket_mode.async_client import AsyncBaseSocketModeClient
|
|
10
12
|
from slack_sdk.socket_mode.request import SocketModeRequest
|
|
11
13
|
from slack_sdk.socket_mode.response import SocketModeResponse
|
|
12
14
|
from slack_sdk.web.async_client import AsyncWebClient
|
|
@@ -66,7 +68,7 @@ class SlackWebhookClient:
|
|
|
66
68
|
"""
|
|
67
69
|
try:
|
|
68
70
|
# 获取请求体和头部
|
|
69
|
-
body = await req.get_data()
|
|
71
|
+
body = cast(bytes, await req.get_data())
|
|
70
72
|
event_data = json.loads(body.decode("utf-8"))
|
|
71
73
|
|
|
72
74
|
# Verify Slack request signature
|
|
@@ -139,9 +141,14 @@ class SlackSocketClient:
|
|
|
139
141
|
self.event_handler = event_handler
|
|
140
142
|
self.socket_client = None
|
|
141
143
|
|
|
142
|
-
async def _handle_events(
|
|
144
|
+
async def _handle_events(
|
|
145
|
+
self, _: AsyncBaseSocketModeClient, req: SocketModeRequest
|
|
146
|
+
):
|
|
143
147
|
"""处理 Socket Mode 事件"""
|
|
144
148
|
try:
|
|
149
|
+
if self.socket_client is None:
|
|
150
|
+
raise RuntimeError("Socket client is not initialized")
|
|
151
|
+
|
|
145
152
|
# 确认收到事件
|
|
146
153
|
response = SocketModeResponse(envelope_id=req.envelope_id)
|
|
147
154
|
await self.socket_client.send_socket_mode_response(response)
|
|
@@ -3,8 +3,7 @@ import base64
|
|
|
3
3
|
import re
|
|
4
4
|
import time
|
|
5
5
|
import uuid
|
|
6
|
-
from
|
|
7
|
-
from typing import Any
|
|
6
|
+
from typing import Any, cast
|
|
8
7
|
|
|
9
8
|
import aiohttp
|
|
10
9
|
from slack_sdk.socket_mode.request import SocketModeRequest
|
|
@@ -68,7 +67,7 @@ class SlackAdapter(Platform):
|
|
|
68
67
|
self.metadata = PlatformMetadata(
|
|
69
68
|
name="slack",
|
|
70
69
|
description="适用于 Slack 的消息平台适配器,支持 Socket Mode 和 Webhook Mode。",
|
|
71
|
-
id=self.config.get("id"),
|
|
70
|
+
id=cast(str, self.config.get("id")),
|
|
72
71
|
support_streaming_message=False,
|
|
73
72
|
)
|
|
74
73
|
|
|
@@ -118,13 +117,13 @@ class SlackAdapter(Platform):
|
|
|
118
117
|
logger.debug(f"[slack] RawMessage {event}")
|
|
119
118
|
|
|
120
119
|
abm = AstrBotMessage()
|
|
121
|
-
abm.self_id = self.bot_self_id
|
|
120
|
+
abm.self_id = cast(str, self.bot_self_id)
|
|
122
121
|
|
|
123
122
|
# 获取用户信息
|
|
124
123
|
user_id = event.get("user", "")
|
|
125
124
|
try:
|
|
126
125
|
user_info = await self.web_client.users_info(user=user_id)
|
|
127
|
-
user_data = user_info["user"]
|
|
126
|
+
user_data = cast(dict, user_info["user"])
|
|
128
127
|
user_name = user_data.get("real_name") or user_data.get("name", user_id)
|
|
129
128
|
except Exception:
|
|
130
129
|
user_name = user_id
|
|
@@ -135,7 +134,7 @@ class SlackAdapter(Platform):
|
|
|
135
134
|
channel_id = event.get("channel", "")
|
|
136
135
|
try:
|
|
137
136
|
channel_info = await self.web_client.conversations_info(channel=channel_id)
|
|
138
|
-
is_im = channel_info["channel"]["is_im"]
|
|
137
|
+
is_im = cast(dict, channel_info["channel"])["is_im"]
|
|
139
138
|
|
|
140
139
|
if is_im:
|
|
141
140
|
abm.type = MessageType.FRIEND_MESSAGE
|
|
@@ -178,7 +177,7 @@ class SlackAdapter(Platform):
|
|
|
178
177
|
for mention in mentions:
|
|
179
178
|
try:
|
|
180
179
|
mentioned_user = await self.web_client.users_info(user=mention)
|
|
181
|
-
user_data = mentioned_user["user"]
|
|
180
|
+
user_data = cast(dict, mentioned_user["user"])
|
|
182
181
|
user_name = user_data.get("real_name") or user_data.get(
|
|
183
182
|
"name",
|
|
184
183
|
mention,
|
|
@@ -329,7 +328,7 @@ class SlackAdapter(Platform):
|
|
|
329
328
|
)
|
|
330
329
|
raise Exception(f"下载文件失败: {resp.status}")
|
|
331
330
|
|
|
332
|
-
async def run(self) ->
|
|
331
|
+
async def run(self) -> None:
|
|
333
332
|
self.bot_self_id = await self.get_bot_user_id()
|
|
334
333
|
logger.info(f"Slack auth test OK. Bot ID: {self.bot_self_id}")
|
|
335
334
|
|
|
@@ -410,7 +409,7 @@ class SlackAdapter(Platform):
|
|
|
410
409
|
await self.socket_client.stop()
|
|
411
410
|
if self.webhook_client:
|
|
412
411
|
await self.webhook_client.stop()
|
|
413
|
-
logger.info("Slack
|
|
412
|
+
logger.info("Slack 适配器已被关闭")
|
|
414
413
|
|
|
415
414
|
def meta(self) -> PlatformMetadata:
|
|
416
415
|
return self.metadata
|
|
@@ -428,3 +427,10 @@ class SlackAdapter(Platform):
|
|
|
428
427
|
|
|
429
428
|
def get_client(self):
|
|
430
429
|
return self.web_client
|
|
430
|
+
|
|
431
|
+
def unified_webhook(self) -> bool:
|
|
432
|
+
return bool(
|
|
433
|
+
self.config.get("unified_webhook_mode", False)
|
|
434
|
+
and self.config.get("slack_connection_mode", "") == "webhook"
|
|
435
|
+
and self.config.get("webhook_uuid")
|
|
436
|
+
)
|