AstrBot 4.7.3__py3-none-any.whl → 4.8.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/message.py +21 -5
- astrbot/core/astr_agent_run_util.py +15 -1
- astrbot/core/config/default.py +113 -1
- astrbot/core/db/__init__.py +30 -1
- astrbot/core/db/sqlite.py +55 -1
- astrbot/core/message/components.py +6 -1
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +64 -5
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +1 -1
- astrbot/core/platform/manager.py +67 -9
- astrbot/core/platform/platform.py +99 -2
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +19 -5
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +5 -7
- astrbot/core/platform/sources/discord/discord_platform_adapter.py +1 -2
- astrbot/core/platform/sources/lark/lark_adapter.py +1 -3
- astrbot/core/platform/sources/misskey/misskey_adapter.py +1 -2
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +2 -0
- astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +1 -3
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +32 -9
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +13 -1
- astrbot/core/platform/sources/satori/satori_adapter.py +1 -2
- astrbot/core/platform/sources/slack/client.py +50 -39
- astrbot/core/platform/sources/slack/slack_adapter.py +21 -7
- astrbot/core/platform/sources/slack/slack_event.py +3 -3
- astrbot/core/platform/sources/telegram/tg_adapter.py +4 -3
- astrbot/core/platform/sources/webchat/webchat_adapter.py +95 -29
- astrbot/core/platform/sources/webchat/webchat_event.py +33 -33
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +1 -2
- astrbot/core/platform/sources/wecom/wecom_adapter.py +51 -9
- astrbot/core/platform/sources/wecom/wecom_event.py +1 -1
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +26 -9
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +27 -5
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +52 -11
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +1 -1
- astrbot/core/platform_message_history_mgr.py +3 -3
- astrbot/core/provider/provider.py +35 -0
- astrbot/core/provider/sources/whisper_api_source.py +43 -11
- astrbot/core/utils/file_extract.py +23 -0
- astrbot/core/utils/tencent_record_helper.py +1 -1
- astrbot/core/utils/webhook_utils.py +47 -0
- astrbot/dashboard/routes/__init__.py +2 -0
- astrbot/dashboard/routes/chat.py +300 -70
- astrbot/dashboard/routes/config.py +32 -165
- astrbot/dashboard/routes/knowledge_base.py +1 -1
- astrbot/dashboard/routes/platform.py +100 -0
- astrbot/dashboard/routes/plugin.py +65 -6
- astrbot/dashboard/server.py +3 -1
- {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/METADATA +48 -37
- {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/RECORD +52 -49
- {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/WHEEL +0 -0
- {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/entry_points.txt +0 -0
- {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/licenses/LICENSE +0 -0
astrbot/core/platform/manager.py
CHANGED
|
@@ -6,7 +6,7 @@ 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
8
|
|
|
9
|
-
from .platform import Platform
|
|
9
|
+
from .platform import Platform, PlatformStatus
|
|
10
10
|
from .register import platform_cls_map
|
|
11
11
|
from .sources.webchat.webchat_adapter import WebChatAdapter
|
|
12
12
|
|
|
@@ -16,7 +16,7 @@ class PlatformManager:
|
|
|
16
16
|
self.platform_insts: list[Platform] = []
|
|
17
17
|
"""加载的 Platform 的实例"""
|
|
18
18
|
|
|
19
|
-
self._inst_map = {}
|
|
19
|
+
self._inst_map: dict[str, dict] = {}
|
|
20
20
|
|
|
21
21
|
self.platforms_config = config["platform"]
|
|
22
22
|
self.settings = config["platform_settings"]
|
|
@@ -37,7 +37,10 @@ class PlatformManager:
|
|
|
37
37
|
webchat_inst = WebChatAdapter({}, self.settings, self.event_queue)
|
|
38
38
|
self.platform_insts.append(webchat_inst)
|
|
39
39
|
asyncio.create_task(
|
|
40
|
-
self._task_wrapper(
|
|
40
|
+
self._task_wrapper(
|
|
41
|
+
asyncio.create_task(webchat_inst.run(), name="webchat"),
|
|
42
|
+
platform=webchat_inst,
|
|
43
|
+
),
|
|
41
44
|
)
|
|
42
45
|
|
|
43
46
|
async def load_platform(self, platform_config: dict):
|
|
@@ -107,7 +110,7 @@ class PlatformManager:
|
|
|
107
110
|
)
|
|
108
111
|
except (ImportError, ModuleNotFoundError) as e:
|
|
109
112
|
logger.error(
|
|
110
|
-
f"加载平台适配器 {platform_config['type']} 失败,原因:{e}。请检查依赖库是否安装。提示:可以在
|
|
113
|
+
f"加载平台适配器 {platform_config['type']} 失败,原因:{e}。请检查依赖库是否安装。提示:可以在 管理面板->平台日志->安装Pip库 中安装依赖库。",
|
|
111
114
|
)
|
|
112
115
|
except Exception as e:
|
|
113
116
|
logger.error(f"加载平台适配器 {platform_config['type']} 失败,原因:{e}。")
|
|
@@ -131,6 +134,7 @@ class PlatformManager:
|
|
|
131
134
|
inst.run(),
|
|
132
135
|
name=f"platform_{platform_config['type']}_{platform_config['id']}",
|
|
133
136
|
),
|
|
137
|
+
platform=inst,
|
|
134
138
|
),
|
|
135
139
|
)
|
|
136
140
|
handlers = star_handlers_registry.get_handlers_by_event_type(
|
|
@@ -145,17 +149,28 @@ class PlatformManager:
|
|
|
145
149
|
except Exception:
|
|
146
150
|
logger.error(traceback.format_exc())
|
|
147
151
|
|
|
148
|
-
async def _task_wrapper(self, task: asyncio.Task):
|
|
152
|
+
async def _task_wrapper(self, task: asyncio.Task, platform: Platform | None = None):
|
|
153
|
+
# 设置平台状态为运行中
|
|
154
|
+
if platform:
|
|
155
|
+
platform.status = PlatformStatus.RUNNING
|
|
156
|
+
|
|
149
157
|
try:
|
|
150
158
|
await task
|
|
151
159
|
except asyncio.CancelledError:
|
|
152
|
-
|
|
160
|
+
if platform:
|
|
161
|
+
platform.status = PlatformStatus.STOPPED
|
|
153
162
|
except Exception as e:
|
|
163
|
+
error_msg = str(e)
|
|
164
|
+
tb_str = traceback.format_exc()
|
|
154
165
|
logger.error(f"------- 任务 {task.get_name()} 发生错误: {e}")
|
|
155
|
-
for line in
|
|
166
|
+
for line in tb_str.split("\n"):
|
|
156
167
|
logger.error(f"| {line}")
|
|
157
168
|
logger.error("-------")
|
|
158
169
|
|
|
170
|
+
# 记录错误到平台实例
|
|
171
|
+
if platform:
|
|
172
|
+
platform.record_error(error_msg, tb_str)
|
|
173
|
+
|
|
159
174
|
async def reload(self, platform_config: dict):
|
|
160
175
|
await self.terminate_platform(platform_config["id"])
|
|
161
176
|
if platform_config["enable"]:
|
|
@@ -172,9 +187,9 @@ class PlatformManager:
|
|
|
172
187
|
logger.info(f"正在尝试终止 {platform_id} 平台适配器 ...")
|
|
173
188
|
|
|
174
189
|
# client_id = self._inst_map.pop(platform_id, None)
|
|
175
|
-
info = self._inst_map.pop(platform_id
|
|
190
|
+
info = self._inst_map.pop(platform_id)
|
|
176
191
|
client_id = info["client_id"]
|
|
177
|
-
inst = info["inst"]
|
|
192
|
+
inst: Platform = info["inst"]
|
|
178
193
|
try:
|
|
179
194
|
self.platform_insts.remove(
|
|
180
195
|
next(
|
|
@@ -196,3 +211,46 @@ class PlatformManager:
|
|
|
196
211
|
|
|
197
212
|
def get_insts(self):
|
|
198
213
|
return self.platform_insts
|
|
214
|
+
|
|
215
|
+
def get_all_stats(self) -> dict:
|
|
216
|
+
"""获取所有平台的统计信息
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
包含所有平台统计信息的字典
|
|
220
|
+
"""
|
|
221
|
+
stats_list = []
|
|
222
|
+
total_errors = 0
|
|
223
|
+
running_count = 0
|
|
224
|
+
error_count = 0
|
|
225
|
+
|
|
226
|
+
for inst in self.platform_insts:
|
|
227
|
+
try:
|
|
228
|
+
stat = inst.get_stats()
|
|
229
|
+
stats_list.append(stat)
|
|
230
|
+
total_errors += stat.get("error_count", 0)
|
|
231
|
+
if stat.get("status") == PlatformStatus.RUNNING.value:
|
|
232
|
+
running_count += 1
|
|
233
|
+
elif stat.get("status") == PlatformStatus.ERROR.value:
|
|
234
|
+
error_count += 1
|
|
235
|
+
except Exception as e:
|
|
236
|
+
# 如果获取统计信息失败,记录基本信息
|
|
237
|
+
logger.warning(f"获取平台统计信息失败: {e}")
|
|
238
|
+
stats_list.append(
|
|
239
|
+
{
|
|
240
|
+
"id": getattr(inst, "config", {}).get("id", "unknown"),
|
|
241
|
+
"type": "unknown",
|
|
242
|
+
"status": "unknown",
|
|
243
|
+
"error_count": 0,
|
|
244
|
+
"last_error": None,
|
|
245
|
+
}
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
"platforms": stats_list,
|
|
250
|
+
"summary": {
|
|
251
|
+
"total": len(stats_list),
|
|
252
|
+
"running": running_count,
|
|
253
|
+
"error": error_count,
|
|
254
|
+
"total_errors": total_errors,
|
|
255
|
+
},
|
|
256
|
+
}
|
|
@@ -2,6 +2,9 @@ import abc
|
|
|
2
2
|
import uuid
|
|
3
3
|
from asyncio import Queue
|
|
4
4
|
from collections.abc import Awaitable
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from enum import Enum
|
|
5
8
|
from typing import Any
|
|
6
9
|
|
|
7
10
|
from astrbot.core.message.message_event_result import MessageChain
|
|
@@ -12,13 +15,90 @@ from .message_session import MessageSesion
|
|
|
12
15
|
from .platform_metadata import PlatformMetadata
|
|
13
16
|
|
|
14
17
|
|
|
18
|
+
class PlatformStatus(Enum):
|
|
19
|
+
"""平台运行状态"""
|
|
20
|
+
|
|
21
|
+
PENDING = "pending" # 待启动
|
|
22
|
+
RUNNING = "running" # 运行中
|
|
23
|
+
ERROR = "error" # 发生错误
|
|
24
|
+
STOPPED = "stopped" # 已停止
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class PlatformError:
|
|
29
|
+
"""平台错误信息"""
|
|
30
|
+
|
|
31
|
+
message: str
|
|
32
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
33
|
+
traceback: str | None = None
|
|
34
|
+
|
|
35
|
+
|
|
15
36
|
class Platform(abc.ABC):
|
|
16
|
-
def __init__(self, event_queue: Queue):
|
|
37
|
+
def __init__(self, config: dict, event_queue: Queue):
|
|
17
38
|
super().__init__()
|
|
39
|
+
# 平台配置
|
|
40
|
+
self.config = config
|
|
18
41
|
# 维护了消息平台的事件队列,EventBus 会从这里取出事件并处理。
|
|
19
42
|
self._event_queue = event_queue
|
|
20
43
|
self.client_self_id = uuid.uuid4().hex
|
|
21
44
|
|
|
45
|
+
# 平台运行状态
|
|
46
|
+
self._status: PlatformStatus = PlatformStatus.PENDING
|
|
47
|
+
self._errors: list[PlatformError] = []
|
|
48
|
+
self._started_at: datetime | None = None
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def status(self) -> PlatformStatus:
|
|
52
|
+
"""获取平台运行状态"""
|
|
53
|
+
return self._status
|
|
54
|
+
|
|
55
|
+
@status.setter
|
|
56
|
+
def status(self, value: PlatformStatus):
|
|
57
|
+
"""设置平台运行状态"""
|
|
58
|
+
self._status = value
|
|
59
|
+
if value == PlatformStatus.RUNNING and self._started_at is None:
|
|
60
|
+
self._started_at = datetime.now()
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def errors(self) -> list[PlatformError]:
|
|
64
|
+
"""获取错误列表"""
|
|
65
|
+
return self._errors
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def last_error(self) -> PlatformError | None:
|
|
69
|
+
"""获取最近的错误"""
|
|
70
|
+
return self._errors[-1] if self._errors else None
|
|
71
|
+
|
|
72
|
+
def record_error(self, message: str, traceback_str: str | None = None):
|
|
73
|
+
"""记录一个错误"""
|
|
74
|
+
self._errors.append(PlatformError(message=message, traceback=traceback_str))
|
|
75
|
+
self._status = PlatformStatus.ERROR
|
|
76
|
+
|
|
77
|
+
def clear_errors(self):
|
|
78
|
+
"""清除错误记录"""
|
|
79
|
+
self._errors.clear()
|
|
80
|
+
if self._status == PlatformStatus.ERROR:
|
|
81
|
+
self._status = PlatformStatus.RUNNING
|
|
82
|
+
|
|
83
|
+
def get_stats(self) -> dict:
|
|
84
|
+
"""获取平台统计信息"""
|
|
85
|
+
meta = self.meta()
|
|
86
|
+
return {
|
|
87
|
+
"id": meta.id or self.config.get("id"),
|
|
88
|
+
"type": meta.name,
|
|
89
|
+
"display_name": meta.adapter_display_name or meta.name,
|
|
90
|
+
"status": self._status.value,
|
|
91
|
+
"started_at": self._started_at.isoformat() if self._started_at else None,
|
|
92
|
+
"error_count": len(self._errors),
|
|
93
|
+
"last_error": {
|
|
94
|
+
"message": self.last_error.message,
|
|
95
|
+
"timestamp": self.last_error.timestamp.isoformat(),
|
|
96
|
+
"traceback": self.last_error.traceback,
|
|
97
|
+
}
|
|
98
|
+
if self.last_error
|
|
99
|
+
else None,
|
|
100
|
+
}
|
|
101
|
+
|
|
22
102
|
@abc.abstractmethod
|
|
23
103
|
def run(self) -> Awaitable[Any]:
|
|
24
104
|
"""得到一个平台的运行实例,需要返回一个协程对象。"""
|
|
@@ -36,7 +116,7 @@ class Platform(abc.ABC):
|
|
|
36
116
|
self,
|
|
37
117
|
session: MessageSesion,
|
|
38
118
|
message_chain: MessageChain,
|
|
39
|
-
)
|
|
119
|
+
):
|
|
40
120
|
"""通过会话发送消息。该方法旨在让插件能够直接通过**可持久化的会话数据**发送消息,而不需要保存 event 对象。
|
|
41
121
|
|
|
42
122
|
异步方法。
|
|
@@ -49,3 +129,20 @@ class Platform(abc.ABC):
|
|
|
49
129
|
|
|
50
130
|
def get_client(self):
|
|
51
131
|
"""获取平台的客户端对象。"""
|
|
132
|
+
|
|
133
|
+
async def webhook_callback(self, request: Any) -> Any:
|
|
134
|
+
"""统一 Webhook 回调入口。
|
|
135
|
+
|
|
136
|
+
支持统一 Webhook 模式的平台需要实现此方法。
|
|
137
|
+
当 Dashboard 收到 /api/platform/webhook/{uuid} 请求时,会调用此方法。
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
request: Quart 请求对象
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
响应内容,格式取决于具体平台的要求
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
NotImplementedError: 平台未实现统一 Webhook 模式
|
|
147
|
+
"""
|
|
148
|
+
raise NotImplementedError(f"平台 {self.meta().name} 未实现统一 Webhook 模式")
|
|
@@ -38,9 +38,8 @@ class AiocqhttpAdapter(Platform):
|
|
|
38
38
|
platform_settings: dict,
|
|
39
39
|
event_queue: asyncio.Queue,
|
|
40
40
|
) -> None:
|
|
41
|
-
super().__init__(event_queue)
|
|
41
|
+
super().__init__(platform_config, event_queue)
|
|
42
42
|
|
|
43
|
-
self.config = platform_config
|
|
44
43
|
self.settings = platform_settings
|
|
45
44
|
self.unique_session = platform_settings["unique_session"]
|
|
46
45
|
self.host = platform_config["ws_reverse_host"]
|
|
@@ -154,7 +153,9 @@ class AiocqhttpAdapter(Platform):
|
|
|
154
153
|
"""OneBot V11 通知类事件"""
|
|
155
154
|
abm = AstrBotMessage()
|
|
156
155
|
abm.self_id = str(event.self_id)
|
|
157
|
-
abm.sender = MessageMember(
|
|
156
|
+
abm.sender = MessageMember(
|
|
157
|
+
user_id=str(event.user_id), nickname=str(event.user_id)
|
|
158
|
+
)
|
|
158
159
|
abm.type = MessageType.OTHER_MESSAGE
|
|
159
160
|
if event.get("group_id"):
|
|
160
161
|
abm.group_id = str(event.group_id)
|
|
@@ -246,7 +247,13 @@ class AiocqhttpAdapter(Platform):
|
|
|
246
247
|
if m["data"].get("url") and m["data"].get("url").startswith("http"):
|
|
247
248
|
# Lagrange
|
|
248
249
|
logger.info("guessing lagrange")
|
|
249
|
-
|
|
250
|
+
# 检查多个可能的文件名字段
|
|
251
|
+
file_name = (
|
|
252
|
+
m["data"].get("file_name", "")
|
|
253
|
+
or m["data"].get("name", "")
|
|
254
|
+
or m["data"].get("file", "")
|
|
255
|
+
or "file"
|
|
256
|
+
)
|
|
250
257
|
abm.message.append(File(name=file_name, url=m["data"]["url"]))
|
|
251
258
|
else:
|
|
252
259
|
try:
|
|
@@ -265,7 +272,14 @@ class AiocqhttpAdapter(Platform):
|
|
|
265
272
|
)
|
|
266
273
|
if ret and "url" in ret:
|
|
267
274
|
file_url = ret["url"] # https
|
|
268
|
-
|
|
275
|
+
# 优先从 API 返回值获取文件名,其次从原始消息数据获取
|
|
276
|
+
file_name = (
|
|
277
|
+
ret.get("file_name", "")
|
|
278
|
+
or ret.get("name", "")
|
|
279
|
+
or m["data"].get("file", "")
|
|
280
|
+
or m["data"].get("file_name", "")
|
|
281
|
+
)
|
|
282
|
+
a = File(name=file_name, url=file_url)
|
|
269
283
|
abm.message.append(a)
|
|
270
284
|
else:
|
|
271
285
|
logger.error(f"获取文件失败: {ret}")
|
|
@@ -47,9 +47,7 @@ class DingtalkPlatformAdapter(Platform):
|
|
|
47
47
|
platform_settings: dict,
|
|
48
48
|
event_queue: asyncio.Queue,
|
|
49
49
|
) -> None:
|
|
50
|
-
super().__init__(event_queue)
|
|
51
|
-
|
|
52
|
-
self.config = platform_config
|
|
50
|
+
super().__init__(platform_config, event_queue)
|
|
53
51
|
|
|
54
52
|
self.unique_session = platform_settings["unique_session"]
|
|
55
53
|
|
|
@@ -76,13 +74,13 @@ class DingtalkPlatformAdapter(Platform):
|
|
|
76
74
|
)
|
|
77
75
|
self.client_ = client # 用于 websockets 的 client
|
|
78
76
|
|
|
79
|
-
def _id_to_sid(self, dingtalk_id: str | None) -> str
|
|
77
|
+
def _id_to_sid(self, dingtalk_id: str | None) -> str:
|
|
80
78
|
if not dingtalk_id:
|
|
81
|
-
return dingtalk_id
|
|
79
|
+
return dingtalk_id or "unknown"
|
|
82
80
|
prefix = "$:LWCP_v1:$"
|
|
83
81
|
if dingtalk_id.startswith(prefix):
|
|
84
82
|
return dingtalk_id[len(prefix) :]
|
|
85
|
-
return dingtalk_id
|
|
83
|
+
return dingtalk_id or "unknown"
|
|
86
84
|
|
|
87
85
|
async def send_by_session(
|
|
88
86
|
self,
|
|
@@ -250,7 +248,7 @@ class DingtalkPlatformAdapter(Platform):
|
|
|
250
248
|
|
|
251
249
|
async def terminate(self):
|
|
252
250
|
def monkey_patch_close():
|
|
253
|
-
raise
|
|
251
|
+
raise KeyboardInterrupt("Graceful shutdown")
|
|
254
252
|
|
|
255
253
|
self.client_.open_connection = monkey_patch_close
|
|
256
254
|
await self.client_.websocket.close(code=1000, reason="Graceful shutdown")
|
|
@@ -44,8 +44,7 @@ class DiscordPlatformAdapter(Platform):
|
|
|
44
44
|
platform_settings: dict,
|
|
45
45
|
event_queue: asyncio.Queue,
|
|
46
46
|
) -> None:
|
|
47
|
-
super().__init__(event_queue)
|
|
48
|
-
self.config = platform_config
|
|
47
|
+
super().__init__(platform_config, event_queue)
|
|
49
48
|
self.settings = platform_settings
|
|
50
49
|
self.client_self_id = None
|
|
51
50
|
self.registered_handlers = []
|
|
@@ -33,9 +33,7 @@ class LarkPlatformAdapter(Platform):
|
|
|
33
33
|
platform_settings: dict,
|
|
34
34
|
event_queue: asyncio.Queue,
|
|
35
35
|
) -> None:
|
|
36
|
-
super().__init__(event_queue)
|
|
37
|
-
|
|
38
|
-
self.config = platform_config
|
|
36
|
+
super().__init__(platform_config, event_queue)
|
|
39
37
|
|
|
40
38
|
self.unique_session = platform_settings["unique_session"]
|
|
41
39
|
|
|
@@ -55,8 +55,7 @@ class MisskeyPlatformAdapter(Platform):
|
|
|
55
55
|
platform_settings: dict,
|
|
56
56
|
event_queue: asyncio.Queue,
|
|
57
57
|
) -> None:
|
|
58
|
-
super().__init__(event_queue)
|
|
59
|
-
self.config = platform_config or {}
|
|
58
|
+
super().__init__(platform_config or {}, event_queue)
|
|
60
59
|
self.settings = platform_settings or {}
|
|
61
60
|
self.instance_url = self.config.get("misskey_instance_url", "")
|
|
62
61
|
self.access_token = self.config.get("misskey_token", "")
|
|
@@ -69,6 +69,8 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
69
69
|
# 结束流式对话,并且传输 buffer 中剩余的消息
|
|
70
70
|
stream_payload["state"] = 10
|
|
71
71
|
ret = await self._post_send(stream=stream_payload)
|
|
72
|
+
else:
|
|
73
|
+
ret = await self._post_send()
|
|
72
74
|
|
|
73
75
|
except Exception as e:
|
|
74
76
|
logger.error(f"发送流式消息时出错: {e}", exc_info=True)
|
|
@@ -97,9 +97,7 @@ class QQOfficialPlatformAdapter(Platform):
|
|
|
97
97
|
platform_settings: dict,
|
|
98
98
|
event_queue: asyncio.Queue,
|
|
99
99
|
) -> None:
|
|
100
|
-
super().__init__(event_queue)
|
|
101
|
-
|
|
102
|
-
self.config = platform_config
|
|
100
|
+
super().__init__(platform_config, event_queue)
|
|
103
101
|
|
|
104
102
|
self.appid = platform_config["appid"]
|
|
105
103
|
self.secret = platform_config["secret"]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
|
+
from typing import Any
|
|
3
4
|
|
|
4
5
|
import botpy
|
|
5
6
|
import botpy.message
|
|
@@ -11,6 +12,7 @@ from astrbot import logger
|
|
|
11
12
|
from astrbot.api.event import MessageChain
|
|
12
13
|
from astrbot.api.platform import AstrBotMessage, MessageType, Platform, PlatformMetadata
|
|
13
14
|
from astrbot.core.platform.astr_message_event import MessageSesion
|
|
15
|
+
from astrbot.core.utils.webhook_utils import log_webhook_info
|
|
14
16
|
|
|
15
17
|
from ...register import register_platform_adapter
|
|
16
18
|
from ..qqofficial.qqofficial_platform_adapter import QQOfficialPlatformAdapter
|
|
@@ -87,13 +89,12 @@ class QQOfficialWebhookPlatformAdapter(Platform):
|
|
|
87
89
|
platform_settings: dict,
|
|
88
90
|
event_queue: asyncio.Queue,
|
|
89
91
|
) -> None:
|
|
90
|
-
super().__init__(event_queue)
|
|
91
|
-
|
|
92
|
-
self.config = platform_config
|
|
92
|
+
super().__init__(platform_config, event_queue)
|
|
93
93
|
|
|
94
94
|
self.appid = platform_config["appid"]
|
|
95
95
|
self.secret = platform_config["secret"]
|
|
96
96
|
self.unique_session = platform_settings["unique_session"]
|
|
97
|
+
self.unified_webhook_mode = platform_config.get("unified_webhook_mode", False)
|
|
97
98
|
|
|
98
99
|
intents = botpy.Intents(
|
|
99
100
|
public_messages=True,
|
|
@@ -106,6 +107,7 @@ class QQOfficialWebhookPlatformAdapter(Platform):
|
|
|
106
107
|
timeout=20,
|
|
107
108
|
)
|
|
108
109
|
self.client.set_platform(self)
|
|
110
|
+
self.webhook_helper = None
|
|
109
111
|
|
|
110
112
|
async def send_by_session(
|
|
111
113
|
self,
|
|
@@ -128,16 +130,37 @@ class QQOfficialWebhookPlatformAdapter(Platform):
|
|
|
128
130
|
self.client,
|
|
129
131
|
)
|
|
130
132
|
await self.webhook_helper.initialize()
|
|
131
|
-
|
|
133
|
+
|
|
134
|
+
# 如果启用统一 webhook 模式,则不启动独立服务器
|
|
135
|
+
webhook_uuid = self.config.get("webhook_uuid")
|
|
136
|
+
if self.unified_webhook_mode and webhook_uuid:
|
|
137
|
+
log_webhook_info(f"{self.meta().id}(QQ 官方机器人 Webhook)", webhook_uuid)
|
|
138
|
+
# 保持运行状态,等待 shutdown
|
|
139
|
+
await self.webhook_helper.shutdown_event.wait()
|
|
140
|
+
else:
|
|
141
|
+
await self.webhook_helper.start_polling()
|
|
132
142
|
|
|
133
143
|
def get_client(self) -> botClient:
|
|
134
144
|
return self.client
|
|
135
145
|
|
|
146
|
+
async def webhook_callback(self, request: Any) -> Any:
|
|
147
|
+
"""统一 Webhook 回调入口"""
|
|
148
|
+
if not self.webhook_helper:
|
|
149
|
+
return {"error": "Webhook helper not initialized"}, 500
|
|
150
|
+
|
|
151
|
+
# 复用 webhook_helper 的回调处理逻辑
|
|
152
|
+
return await self.webhook_helper.handle_callback(request)
|
|
153
|
+
|
|
136
154
|
async def terminate(self):
|
|
137
|
-
self.webhook_helper
|
|
155
|
+
if self.webhook_helper:
|
|
156
|
+
self.webhook_helper.shutdown_event.set()
|
|
138
157
|
await self.client.close()
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
158
|
+
if self.webhook_helper and not self.unified_webhook_mode:
|
|
159
|
+
try:
|
|
160
|
+
await self.webhook_helper.server.shutdown()
|
|
161
|
+
except Exception as exc:
|
|
162
|
+
logger.warning(
|
|
163
|
+
f"Exception occurred during QQOfficialWebhook server shutdown: {exc}",
|
|
164
|
+
exc_info=True,
|
|
165
|
+
)
|
|
143
166
|
logger.info("QQ 机器人官方 API 适配器已经被优雅地关闭")
|
|
@@ -78,7 +78,19 @@ class QQOfficialWebhook:
|
|
|
78
78
|
return response
|
|
79
79
|
|
|
80
80
|
async def callback(self):
|
|
81
|
-
|
|
81
|
+
"""内部服务器的回调入口"""
|
|
82
|
+
return await self.handle_callback(quart.request)
|
|
83
|
+
|
|
84
|
+
async def handle_callback(self, request) -> dict:
|
|
85
|
+
"""处理 webhook 回调,可被统一 webhook 入口复用
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
request: Quart 请求对象
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
响应数据
|
|
92
|
+
"""
|
|
93
|
+
msg: dict = await request.json
|
|
82
94
|
logger.debug(f"收到 qq_official_webhook 回调: {msg}")
|
|
83
95
|
|
|
84
96
|
event = msg.get("t")
|
|
@@ -38,8 +38,7 @@ class SatoriPlatformAdapter(Platform):
|
|
|
38
38
|
platform_settings: dict,
|
|
39
39
|
event_queue: asyncio.Queue,
|
|
40
40
|
) -> None:
|
|
41
|
-
super().__init__(event_queue)
|
|
42
|
-
self.config = platform_config
|
|
41
|
+
super().__init__(platform_config, event_queue)
|
|
43
42
|
self.settings = platform_settings
|
|
44
43
|
|
|
45
44
|
self.api_base_url = self.config.get(
|
|
@@ -47,51 +47,62 @@ class SlackWebhookClient:
|
|
|
47
47
|
|
|
48
48
|
@self.app.route(self.path, methods=["POST"])
|
|
49
49
|
async def slack_events():
|
|
50
|
-
"""
|
|
51
|
-
|
|
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)
|
|
50
|
+
"""内部服务器的 POST 回调入口"""
|
|
51
|
+
return await self.handle_callback(request)
|
|
89
52
|
|
|
90
53
|
@self.app.route("/health", methods=["GET"])
|
|
91
54
|
async def health_check():
|
|
92
55
|
"""健康检查端点"""
|
|
93
56
|
return {"status": "ok", "service": "slack-webhook"}
|
|
94
57
|
|
|
58
|
+
async def handle_callback(self, req):
|
|
59
|
+
"""处理 Slack 回调请求,可被统一 webhook 入口复用
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
req: Quart 请求对象
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Response 对象或字典
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
# 获取请求体和头部
|
|
69
|
+
body = await req.get_data()
|
|
70
|
+
event_data = json.loads(body.decode("utf-8"))
|
|
71
|
+
|
|
72
|
+
# Verify Slack request signature
|
|
73
|
+
timestamp = req.headers.get("X-Slack-Request-Timestamp")
|
|
74
|
+
signature = req.headers.get("X-Slack-Signature")
|
|
75
|
+
if not timestamp or not signature:
|
|
76
|
+
return Response("Missing headers", status=400)
|
|
77
|
+
# Calculate the HMAC signature
|
|
78
|
+
sig_basestring = f"v0:{timestamp}:{body.decode('utf-8')}"
|
|
79
|
+
my_signature = (
|
|
80
|
+
"v0="
|
|
81
|
+
+ hmac.new(
|
|
82
|
+
self.signing_secret.encode("utf-8"),
|
|
83
|
+
sig_basestring.encode("utf-8"),
|
|
84
|
+
hashlib.sha256,
|
|
85
|
+
).hexdigest()
|
|
86
|
+
)
|
|
87
|
+
# Verify the signature
|
|
88
|
+
if not hmac.compare_digest(my_signature, signature):
|
|
89
|
+
logger.warning("Slack request signature verification failed")
|
|
90
|
+
return Response("Invalid signature", status=400)
|
|
91
|
+
logger.info(f"Received Slack event: {event_data}")
|
|
92
|
+
|
|
93
|
+
# 处理 URL 验证事件
|
|
94
|
+
if event_data.get("type") == "url_verification":
|
|
95
|
+
return {"challenge": event_data.get("challenge")}
|
|
96
|
+
# 处理事件
|
|
97
|
+
if self.event_handler and event_data.get("type") == "event_callback":
|
|
98
|
+
await self.event_handler(event_data)
|
|
99
|
+
|
|
100
|
+
return Response("", status=200)
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.error(f"处理 Slack 事件时出错: {e}")
|
|
104
|
+
return Response("Internal Server Error", status=500)
|
|
105
|
+
|
|
95
106
|
async def start(self):
|
|
96
107
|
"""启动 Webhook 服务器"""
|
|
97
108
|
logger.info(
|