AstrBot 4.7.4__py3-none-any.whl → 4.9.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/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_run_util.py +15 -1
- astrbot/core/astr_agent_tool_exec.py +5 -1
- astrbot/core/config/astrbot_config.py +4 -0
- astrbot/core/config/default.py +116 -1
- astrbot/core/core_lifecycle.py +1 -1
- astrbot/core/db/__init__.py +32 -4
- 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 +56 -1
- 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/agent_sub_stages/third_party.py +1 -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 +4 -2
- astrbot/core/pipeline/result_decorate/stage.py +68 -21
- 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 +71 -9
- astrbot/core/platform/platform.py +109 -4
- 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 +13 -8
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +28 -22
- 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 +53 -26
- astrbot/core/platform/sources/discord/discord_platform_event.py +29 -8
- astrbot/core/platform/sources/lark/lark_adapter.py +178 -22
- 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 +3 -5
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +64 -18
- astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +14 -10
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -11
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +15 -2
- astrbot/core/platform/sources/satori/satori_adapter.py +1 -2
- astrbot/core/platform/sources/slack/client.py +58 -40
- astrbot/core/platform/sources/slack/slack_adapter.py +36 -16
- astrbot/core/platform/sources/slack/slack_event.py +11 -10
- astrbot/core/platform/sources/telegram/tg_adapter.py +2 -3
- astrbot/core/platform/sources/telegram/tg_event.py +23 -27
- astrbot/core/platform/sources/webchat/webchat_adapter.py +97 -31
- astrbot/core/platform/sources/webchat/webchat_event.py +35 -35
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +27 -11
- astrbot/core/platform/sources/wecom/wecom_adapter.py +75 -36
- astrbot/core/platform/sources/wecom/wecom_event.py +3 -3
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +26 -9
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +3 -3
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +27 -5
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +81 -35
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +11 -8
- astrbot/core/platform_message_history_mgr.py +3 -3
- 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 +44 -12
- 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 +2 -2
- astrbot/core/utils/version_comparator.py +6 -3
- astrbot/core/utils/webhook_utils.py +66 -0
- astrbot/dashboard/routes/__init__.py +2 -0
- astrbot/dashboard/routes/chat.py +311 -76
- astrbot/dashboard/routes/config.py +14 -5
- astrbot/dashboard/routes/knowledge_base.py +254 -79
- astrbot/dashboard/routes/log.py +13 -8
- astrbot/dashboard/routes/platform.py +100 -0
- astrbot/dashboard/routes/plugin.py +108 -51
- astrbot/dashboard/routes/route.py +2 -0
- astrbot/dashboard/server.py +9 -4
- {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/METADATA +50 -37
- {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/RECORD +111 -108
- {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/WHEEL +0 -0
- {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/entry_points.txt +0 -0
- {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,7 +5,15 @@ import uuid
|
|
|
5
5
|
from io import BytesIO
|
|
6
6
|
|
|
7
7
|
import lark_oapi as lark
|
|
8
|
-
from lark_oapi.api.im.v1 import
|
|
8
|
+
from lark_oapi.api.im.v1 import (
|
|
9
|
+
CreateImageRequest,
|
|
10
|
+
CreateImageRequestBody,
|
|
11
|
+
CreateMessageReactionRequest,
|
|
12
|
+
CreateMessageReactionRequestBody,
|
|
13
|
+
Emoji,
|
|
14
|
+
ReplyMessageRequest,
|
|
15
|
+
ReplyMessageRequestBody,
|
|
16
|
+
)
|
|
9
17
|
|
|
10
18
|
from astrbot import logger
|
|
11
19
|
from astrbot.api.event import AstrMessageEvent, MessageChain
|
|
@@ -44,7 +52,7 @@ class LarkMessageEvent(AstrMessageEvent):
|
|
|
44
52
|
file_path = comp.file.replace("file:///", "")
|
|
45
53
|
elif comp.file and comp.file.startswith("http"):
|
|
46
54
|
image_file_path = await download_image_by_url(comp.file)
|
|
47
|
-
file_path = image_file_path
|
|
55
|
+
file_path = image_file_path if image_file_path else ""
|
|
48
56
|
elif comp.file and comp.file.startswith("base64://"):
|
|
49
57
|
base64_str = comp.file.removeprefix("base64://")
|
|
50
58
|
image_data = base64.b64decode(base64_str)
|
|
@@ -54,10 +62,17 @@ class LarkMessageEvent(AstrMessageEvent):
|
|
|
54
62
|
with open(file_path, "wb") as f:
|
|
55
63
|
f.write(BytesIO(image_data).getvalue())
|
|
56
64
|
else:
|
|
57
|
-
file_path = comp.file
|
|
65
|
+
file_path = comp.file if comp.file else ""
|
|
58
66
|
|
|
59
67
|
if image_file is None:
|
|
60
|
-
|
|
68
|
+
if not file_path:
|
|
69
|
+
logger.error("[Lark] 图片路径为空,无法上传")
|
|
70
|
+
continue
|
|
71
|
+
try:
|
|
72
|
+
image_file = open(file_path, "rb")
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.error(f"[Lark] 无法打开图片文件: {e}")
|
|
75
|
+
continue
|
|
61
76
|
|
|
62
77
|
request = (
|
|
63
78
|
CreateImageRequest.builder()
|
|
@@ -69,9 +84,20 @@ class LarkMessageEvent(AstrMessageEvent):
|
|
|
69
84
|
)
|
|
70
85
|
.build()
|
|
71
86
|
)
|
|
87
|
+
|
|
88
|
+
if lark_client.im is None:
|
|
89
|
+
logger.error("[Lark] API Client im 模块未初始化,无法上传图片")
|
|
90
|
+
continue
|
|
91
|
+
|
|
72
92
|
response = await lark_client.im.v1.image.acreate(request)
|
|
73
93
|
if not response.success():
|
|
74
94
|
logger.error(f"无法上传飞书图片({response.code}): {response.msg}")
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
if response.data is None:
|
|
98
|
+
logger.error("[Lark] 上传图片成功但未返回数据(data is None)")
|
|
99
|
+
continue
|
|
100
|
+
|
|
75
101
|
image_key = response.data.image_key
|
|
76
102
|
logger.debug(image_key)
|
|
77
103
|
ret.append(_stage)
|
|
@@ -107,6 +133,10 @@ class LarkMessageEvent(AstrMessageEvent):
|
|
|
107
133
|
.build()
|
|
108
134
|
)
|
|
109
135
|
|
|
136
|
+
if self.bot.im is None:
|
|
137
|
+
logger.error("[Lark] API Client im 模块未初始化,无法回复消息")
|
|
138
|
+
return
|
|
139
|
+
|
|
110
140
|
response = await self.bot.im.v1.message.areply(request)
|
|
111
141
|
|
|
112
142
|
if not response.success():
|
|
@@ -115,6 +145,10 @@ class LarkMessageEvent(AstrMessageEvent):
|
|
|
115
145
|
await super().send(message)
|
|
116
146
|
|
|
117
147
|
async def react(self, emoji: str):
|
|
148
|
+
if self.bot.im is None:
|
|
149
|
+
logger.error("[Lark] API Client im 模块未初始化,无法发送表情")
|
|
150
|
+
return
|
|
151
|
+
|
|
118
152
|
request = (
|
|
119
153
|
CreateMessageReactionRequest.builder()
|
|
120
154
|
.message_id(self.message_obj.message_id)
|
|
@@ -125,6 +159,7 @@ class LarkMessageEvent(AstrMessageEvent):
|
|
|
125
159
|
)
|
|
126
160
|
.build()
|
|
127
161
|
)
|
|
162
|
+
|
|
128
163
|
response = await self.bot.im.v1.message_reaction.acreate(request)
|
|
129
164
|
if not response.success():
|
|
130
165
|
logger.error(f"发送飞书表情回应失败({response.code}): {response.msg}")
|
|
@@ -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
|
|
@@ -55,8 +54,7 @@ class MisskeyPlatformAdapter(Platform):
|
|
|
55
54
|
platform_settings: dict,
|
|
56
55
|
event_queue: asyncio.Queue,
|
|
57
56
|
) -> None:
|
|
58
|
-
super().__init__(event_queue)
|
|
59
|
-
self.config = platform_config or {}
|
|
57
|
+
super().__init__(platform_config or {}, event_queue)
|
|
60
58
|
self.settings = platform_settings or {}
|
|
61
59
|
self.instance_url = self.config.get("misskey_instance_url", "")
|
|
62
60
|
self.access_token = self.config.get("misskey_token", "")
|
|
@@ -204,7 +202,7 @@ class MisskeyPlatformAdapter(Platform):
|
|
|
204
202
|
if not isinstance(message.raw_message, dict):
|
|
205
203
|
message.raw_message = {}
|
|
206
204
|
message.raw_message["poll"] = poll
|
|
207
|
-
message.poll
|
|
205
|
+
message.__setattr__("poll", poll)
|
|
208
206
|
except Exception:
|
|
209
207
|
pass
|
|
210
208
|
|
|
@@ -373,7 +371,7 @@ class MisskeyPlatformAdapter(Platform):
|
|
|
373
371
|
self,
|
|
374
372
|
session: MessageSession,
|
|
375
373
|
message_chain: MessageChain,
|
|
376
|
-
) ->
|
|
374
|
+
) -> None:
|
|
377
375
|
if not self.api:
|
|
378
376
|
logger.error("[Misskey] API 客户端未初始化")
|
|
379
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()
|
|
@@ -69,6 +73,8 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
69
73
|
# 结束流式对话,并且传输 buffer 中剩余的消息
|
|
70
74
|
stream_payload["state"] = 10
|
|
71
75
|
ret = await self._post_send(stream=stream_payload)
|
|
76
|
+
else:
|
|
77
|
+
ret = await self._post_send()
|
|
72
78
|
|
|
73
79
|
except Exception as e:
|
|
74
80
|
logger.error(f"发送流式消息时出错: {e}", exc_info=True)
|
|
@@ -81,7 +87,8 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
81
87
|
return None
|
|
82
88
|
|
|
83
89
|
source = self.message_obj.raw_message
|
|
84
|
-
|
|
90
|
+
|
|
91
|
+
if not isinstance(
|
|
85
92
|
source,
|
|
86
93
|
(
|
|
87
94
|
botpy.message.Message,
|
|
@@ -89,7 +96,9 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
89
96
|
botpy.message.DirectMessage,
|
|
90
97
|
botpy.message.C2CMessage,
|
|
91
98
|
),
|
|
92
|
-
)
|
|
99
|
+
):
|
|
100
|
+
logger.warning(f"[QQOfficial] 不支持的消息源类型: {type(source)}")
|
|
101
|
+
return None
|
|
93
102
|
|
|
94
103
|
(
|
|
95
104
|
plain_text,
|
|
@@ -106,7 +115,7 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
106
115
|
):
|
|
107
116
|
return None
|
|
108
117
|
|
|
109
|
-
payload = {
|
|
118
|
+
payload: dict = {
|
|
110
119
|
"content": plain_text,
|
|
111
120
|
"msg_id": self.message_obj.message_id,
|
|
112
121
|
}
|
|
@@ -116,8 +125,12 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
116
125
|
|
|
117
126
|
ret = None
|
|
118
127
|
|
|
119
|
-
match
|
|
120
|
-
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
|
+
|
|
121
134
|
if image_base64:
|
|
122
135
|
media = await self.upload_group_and_c2c_image(
|
|
123
136
|
image_base64,
|
|
@@ -138,7 +151,8 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
138
151
|
group_openid=source.group_openid,
|
|
139
152
|
**payload,
|
|
140
153
|
)
|
|
141
|
-
|
|
154
|
+
|
|
155
|
+
case botpy.message.C2CMessage():
|
|
142
156
|
if image_base64:
|
|
143
157
|
media = await self.upload_group_and_c2c_image(
|
|
144
158
|
image_base64,
|
|
@@ -167,18 +181,23 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
167
181
|
**payload,
|
|
168
182
|
)
|
|
169
183
|
logger.debug(f"Message sent to C2C: {ret}")
|
|
170
|
-
|
|
184
|
+
|
|
185
|
+
case botpy.message.Message():
|
|
171
186
|
if image_path:
|
|
172
187
|
payload["file_image"] = image_path
|
|
173
188
|
ret = await self.bot.api.post_message(
|
|
174
189
|
channel_id=source.channel_id,
|
|
175
190
|
**payload,
|
|
176
191
|
)
|
|
177
|
-
|
|
192
|
+
|
|
193
|
+
case botpy.message.DirectMessage():
|
|
178
194
|
if image_path:
|
|
179
195
|
payload["file_image"] = image_path
|
|
180
196
|
ret = await self.bot.api.post_dms(guild_id=source.guild_id, **payload)
|
|
181
197
|
|
|
198
|
+
case _:
|
|
199
|
+
pass
|
|
200
|
+
|
|
182
201
|
await super().send(self.send_buffer)
|
|
183
202
|
|
|
184
203
|
self.send_buffer = None
|
|
@@ -196,18 +215,33 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
196
215
|
"file_type": file_type,
|
|
197
216
|
"srv_send_msg": False,
|
|
198
217
|
}
|
|
218
|
+
|
|
219
|
+
result = None
|
|
199
220
|
if "openid" in kwargs:
|
|
200
221
|
payload["openid"] = kwargs["openid"]
|
|
201
222
|
route = Route("POST", "/v2/users/{openid}/files", openid=kwargs["openid"])
|
|
202
|
-
|
|
203
|
-
|
|
223
|
+
result = await self.bot.api._http.request(route, json=payload)
|
|
224
|
+
elif "group_openid" in kwargs:
|
|
204
225
|
payload["group_openid"] = kwargs["group_openid"]
|
|
205
226
|
route = Route(
|
|
206
227
|
"POST",
|
|
207
228
|
"/v2/groups/{group_openid}/files",
|
|
208
229
|
group_openid=kwargs["group_openid"],
|
|
209
230
|
)
|
|
210
|
-
|
|
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
|
+
)
|
|
211
245
|
|
|
212
246
|
async def upload_group_and_c2c_record(
|
|
213
247
|
self,
|
|
@@ -250,11 +284,14 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
250
284
|
result = await self.bot.api._http.request(route, json=payload)
|
|
251
285
|
|
|
252
286
|
if result:
|
|
287
|
+
if not isinstance(result, dict):
|
|
288
|
+
logger.error(f"上传文件响应格式错误: {result}")
|
|
289
|
+
return None
|
|
290
|
+
|
|
253
291
|
return Media(
|
|
254
|
-
file_uuid=result
|
|
255
|
-
file_info=result
|
|
292
|
+
file_uuid=result["file_uuid"],
|
|
293
|
+
file_info=result["file_info"],
|
|
256
294
|
ttl=result.get("ttl", 0),
|
|
257
|
-
file_id=result.get("id", ""),
|
|
258
295
|
)
|
|
259
296
|
except Exception as e:
|
|
260
297
|
logger.error(f"上传请求错误: {e}")
|
|
@@ -271,7 +308,7 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
271
308
|
message_reference: message.Reference | None = None,
|
|
272
309
|
media: message.Media | None = None,
|
|
273
310
|
msg_id: str | None = None,
|
|
274
|
-
msg_seq:
|
|
311
|
+
msg_seq: int | None = 1,
|
|
275
312
|
event_id: str | None = None,
|
|
276
313
|
markdown: message.MarkdownPayload | None = None,
|
|
277
314
|
keyboard: message.Keyboard | None = None,
|
|
@@ -280,7 +317,14 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
280
317
|
payload = locals()
|
|
281
318
|
payload.pop("self", None)
|
|
282
319
|
route = Route("POST", "/v2/users/{openid}/messages", openid=openid)
|
|
283
|
-
|
|
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)
|
|
284
328
|
|
|
285
329
|
@staticmethod
|
|
286
330
|
async def _parse_to_qqofficial(message: MessageChain):
|
|
@@ -300,8 +344,10 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
300
344
|
image_base64 = file_to_base64(image_file_path)
|
|
301
345
|
elif i.file and i.file.startswith("base64://"):
|
|
302
346
|
image_base64 = i.file
|
|
303
|
-
|
|
347
|
+
elif i.file:
|
|
304
348
|
image_base64 = file_to_base64(i.file)
|
|
349
|
+
else:
|
|
350
|
+
raise ValueError("Unsupported image file format")
|
|
305
351
|
image_base64 = image_base64.removeprefix("base64://")
|
|
306
352
|
elif isinstance(i, Record):
|
|
307
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
|
|
|
@@ -97,13 +100,11 @@ class QQOfficialPlatformAdapter(Platform):
|
|
|
97
100
|
platform_settings: dict,
|
|
98
101
|
event_queue: asyncio.Queue,
|
|
99
102
|
) -> None:
|
|
100
|
-
super().__init__(event_queue)
|
|
101
|
-
|
|
102
|
-
self.config = platform_config
|
|
103
|
+
super().__init__(platform_config, event_queue)
|
|
103
104
|
|
|
104
105
|
self.appid = platform_config["appid"]
|
|
105
106
|
self.secret = platform_config["secret"]
|
|
106
|
-
self.unique_session = platform_settings["unique_session"]
|
|
107
|
+
self.unique_session: bool = platform_settings["unique_session"]
|
|
107
108
|
qq_group = platform_config["enable_group_c2c"]
|
|
108
109
|
guild_dm = platform_config["enable_guild_direct_message"]
|
|
109
110
|
|
|
@@ -139,12 +140,15 @@ class QQOfficialPlatformAdapter(Platform):
|
|
|
139
140
|
return PlatformMetadata(
|
|
140
141
|
name="qq_official",
|
|
141
142
|
description="QQ 机器人官方 API 适配器",
|
|
142
|
-
id=self.config.get("id"),
|
|
143
|
+
id=cast(str, self.config.get("id")),
|
|
143
144
|
)
|
|
144
145
|
|
|
145
146
|
@staticmethod
|
|
146
147
|
def _parse_from_qqofficial(
|
|
147
|
-
message: botpy.message.Message
|
|
148
|
+
message: botpy.message.Message
|
|
149
|
+
| botpy.message.GroupMessage
|
|
150
|
+
| botpy.message.DirectMessage
|
|
151
|
+
| botpy.message.C2CMessage,
|
|
148
152
|
message_type: MessageType,
|
|
149
153
|
):
|
|
150
154
|
abm = AstrBotMessage()
|
|
@@ -152,7 +156,7 @@ class QQOfficialPlatformAdapter(Platform):
|
|
|
152
156
|
abm.timestamp = int(time.time())
|
|
153
157
|
abm.raw_message = message
|
|
154
158
|
abm.message_id = message.id
|
|
155
|
-
abm.tag = "qq_official"
|
|
159
|
+
# abm.tag = "qq_official"
|
|
156
160
|
msg: list[BaseMessageComponent] = []
|
|
157
161
|
|
|
158
162
|
if isinstance(message, botpy.message.GroupMessage) or isinstance(
|
|
@@ -182,9 +186,9 @@ class QQOfficialPlatformAdapter(Platform):
|
|
|
182
186
|
message,
|
|
183
187
|
botpy.message.DirectMessage,
|
|
184
188
|
):
|
|
185
|
-
|
|
189
|
+
if isinstance(message, botpy.message.Message):
|
|
186
190
|
abm.self_id = str(message.mentions[0].id)
|
|
187
|
-
|
|
191
|
+
else:
|
|
188
192
|
abm.self_id = ""
|
|
189
193
|
|
|
190
194
|
plain_content = message.content.replace(
|