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.
Files changed (52) hide show
  1. astrbot/cli/__init__.py +1 -1
  2. astrbot/core/agent/message.py +21 -5
  3. astrbot/core/astr_agent_run_util.py +15 -1
  4. astrbot/core/config/default.py +113 -1
  5. astrbot/core/db/__init__.py +30 -1
  6. astrbot/core/db/sqlite.py +55 -1
  7. astrbot/core/message/components.py +6 -1
  8. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +64 -5
  9. astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +1 -1
  10. astrbot/core/platform/manager.py +67 -9
  11. astrbot/core/platform/platform.py +99 -2
  12. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +19 -5
  13. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +5 -7
  14. astrbot/core/platform/sources/discord/discord_platform_adapter.py +1 -2
  15. astrbot/core/platform/sources/lark/lark_adapter.py +1 -3
  16. astrbot/core/platform/sources/misskey/misskey_adapter.py +1 -2
  17. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +2 -0
  18. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +1 -3
  19. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +32 -9
  20. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +13 -1
  21. astrbot/core/platform/sources/satori/satori_adapter.py +1 -2
  22. astrbot/core/platform/sources/slack/client.py +50 -39
  23. astrbot/core/platform/sources/slack/slack_adapter.py +21 -7
  24. astrbot/core/platform/sources/slack/slack_event.py +3 -3
  25. astrbot/core/platform/sources/telegram/tg_adapter.py +4 -3
  26. astrbot/core/platform/sources/webchat/webchat_adapter.py +95 -29
  27. astrbot/core/platform/sources/webchat/webchat_event.py +33 -33
  28. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +1 -2
  29. astrbot/core/platform/sources/wecom/wecom_adapter.py +51 -9
  30. astrbot/core/platform/sources/wecom/wecom_event.py +1 -1
  31. astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +26 -9
  32. astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +27 -5
  33. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +52 -11
  34. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +1 -1
  35. astrbot/core/platform_message_history_mgr.py +3 -3
  36. astrbot/core/provider/provider.py +35 -0
  37. astrbot/core/provider/sources/whisper_api_source.py +43 -11
  38. astrbot/core/utils/file_extract.py +23 -0
  39. astrbot/core/utils/tencent_record_helper.py +1 -1
  40. astrbot/core/utils/webhook_utils.py +47 -0
  41. astrbot/dashboard/routes/__init__.py +2 -0
  42. astrbot/dashboard/routes/chat.py +300 -70
  43. astrbot/dashboard/routes/config.py +32 -165
  44. astrbot/dashboard/routes/knowledge_base.py +1 -1
  45. astrbot/dashboard/routes/platform.py +100 -0
  46. astrbot/dashboard/routes/plugin.py +65 -6
  47. astrbot/dashboard/server.py +3 -1
  48. {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/METADATA +48 -37
  49. {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/RECORD +52 -49
  50. {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/WHEEL +0 -0
  51. {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/entry_points.txt +0 -0
  52. {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -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(asyncio.create_task(webchat_inst.run(), name="webchat")),
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}。请检查依赖库是否安装。提示:可以在 管理面板->控制台->安装Pip库 中安装依赖库。",
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
- pass
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 traceback.format_exc().split("\n"):
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, None)
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
- ) -> Awaitable[Any]:
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(user_id=str(event.user_id), nickname=event.user_id)
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
- file_name = m["data"].get("file_name", "file")
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
- a = File(name="", url=file_url)
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 | None:
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 Exception("Graceful shutdown")
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
- await self.webhook_helper.start_polling()
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.shutdown_event.set()
155
+ if self.webhook_helper:
156
+ self.webhook_helper.shutdown_event.set()
138
157
  await self.client.close()
139
- try:
140
- await self.webhook_helper.server.shutdown()
141
- except Exception as _:
142
- pass
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
- msg: dict = await quart.request.json
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
- """处理 Slack 事件"""
51
- try:
52
- # 获取请求体和头部
53
- body = await request.get_data()
54
- event_data = json.loads(body.decode("utf-8"))
55
-
56
- # Verify Slack request signature
57
- timestamp = request.headers.get("X-Slack-Request-Timestamp")
58
- signature = request.headers.get("X-Slack-Signature")
59
- if not timestamp or not signature:
60
- return Response("Missing headers", status=400)
61
- # Calculate the HMAC signature
62
- sig_basestring = f"v0:{timestamp}:{body.decode('utf-8')}"
63
- my_signature = (
64
- "v0="
65
- + hmac.new(
66
- self.signing_secret.encode("utf-8"),
67
- sig_basestring.encode("utf-8"),
68
- hashlib.sha256,
69
- ).hexdigest()
70
- )
71
- # Verify the signature
72
- if not hmac.compare_digest(my_signature, signature):
73
- logger.warning("Slack request signature verification failed")
74
- return Response("Invalid signature", status=400)
75
- logger.info(f"Received Slack event: {event_data}")
76
-
77
- # 处理 URL 验证事件
78
- if event_data.get("type") == "url_verification":
79
- return {"challenge": event_data.get("challenge")}
80
- # 处理事件
81
- if self.event_handler and event_data.get("type") == "event_callback":
82
- await self.event_handler(event_data)
83
-
84
- return Response("", status=200)
85
-
86
- except Exception as e:
87
- logger.error(f"处理 Slack 事件时出错: {e}")
88
- return Response("Internal Server Error", status=500)
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(