AstrBot 4.7.4__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 (46) hide show
  1. astrbot/cli/__init__.py +1 -1
  2. astrbot/core/astr_agent_run_util.py +15 -1
  3. astrbot/core/config/default.py +58 -1
  4. astrbot/core/db/__init__.py +30 -1
  5. astrbot/core/db/sqlite.py +55 -1
  6. astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +1 -1
  7. astrbot/core/platform/manager.py +67 -9
  8. astrbot/core/platform/platform.py +99 -2
  9. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +4 -3
  10. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +4 -6
  11. astrbot/core/platform/sources/discord/discord_platform_adapter.py +1 -2
  12. astrbot/core/platform/sources/lark/lark_adapter.py +1 -3
  13. astrbot/core/platform/sources/misskey/misskey_adapter.py +1 -2
  14. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +2 -0
  15. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +1 -3
  16. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +32 -9
  17. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +13 -1
  18. astrbot/core/platform/sources/satori/satori_adapter.py +1 -2
  19. astrbot/core/platform/sources/slack/client.py +50 -39
  20. astrbot/core/platform/sources/slack/slack_adapter.py +21 -7
  21. astrbot/core/platform/sources/slack/slack_event.py +3 -3
  22. astrbot/core/platform/sources/telegram/tg_adapter.py +1 -2
  23. astrbot/core/platform/sources/webchat/webchat_adapter.py +95 -29
  24. astrbot/core/platform/sources/webchat/webchat_event.py +33 -33
  25. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +1 -2
  26. astrbot/core/platform/sources/wecom/wecom_adapter.py +51 -9
  27. astrbot/core/platform/sources/wecom/wecom_event.py +1 -1
  28. astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +26 -9
  29. astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +27 -5
  30. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +52 -11
  31. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +1 -1
  32. astrbot/core/platform_message_history_mgr.py +3 -3
  33. astrbot/core/provider/sources/whisper_api_source.py +43 -11
  34. astrbot/core/utils/tencent_record_helper.py +1 -1
  35. astrbot/core/utils/webhook_utils.py +47 -0
  36. astrbot/dashboard/routes/__init__.py +2 -0
  37. astrbot/dashboard/routes/chat.py +300 -70
  38. astrbot/dashboard/routes/config.py +19 -0
  39. astrbot/dashboard/routes/knowledge_base.py +1 -1
  40. astrbot/dashboard/routes/platform.py +100 -0
  41. astrbot/dashboard/server.py +3 -1
  42. {astrbot-4.7.4.dist-info → astrbot-4.8.0.dist-info}/METADATA +48 -37
  43. {astrbot-4.7.4.dist-info → astrbot-4.8.0.dist-info}/RECORD +46 -44
  44. {astrbot-4.7.4.dist-info → astrbot-4.8.0.dist-info}/WHEEL +0 -0
  45. {astrbot-4.7.4.dist-info → astrbot-4.8.0.dist-info}/entry_points.txt +0 -0
  46. {astrbot-4.7.4.dist-info → astrbot-4.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -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(
@@ -21,6 +21,7 @@ from astrbot.api.platform import (
21
21
  PlatformMetadata,
22
22
  )
23
23
  from astrbot.core.platform.astr_message_event import MessageSesion
24
+ from astrbot.core.utils.webhook_utils import log_webhook_info
24
25
 
25
26
  from ...register import register_platform_adapter
26
27
  from .client import SlackSocketClient, SlackWebhookClient
@@ -39,9 +40,7 @@ class SlackAdapter(Platform):
39
40
  platform_settings: dict,
40
41
  event_queue: asyncio.Queue,
41
42
  ) -> None:
42
- super().__init__(event_queue)
43
-
44
- self.config = platform_config
43
+ super().__init__(platform_config, event_queue)
45
44
  self.settings = platform_settings
46
45
  self.unique_session = platform_settings.get("unique_session", False)
47
46
 
@@ -49,6 +48,7 @@ class SlackAdapter(Platform):
49
48
  self.app_token = platform_config.get("app_token")
50
49
  self.signing_secret = platform_config.get("signing_secret")
51
50
  self.connection_mode = platform_config.get("slack_connection_mode", "socket")
51
+ self.unified_webhook_mode = platform_config.get("unified_webhook_mode", False)
52
52
  self.webhook_host = platform_config.get("slack_webhook_host", "0.0.0.0")
53
53
  self.webhook_port = platform_config.get("slack_webhook_port", 3000)
54
54
  self.webhook_path = platform_config.get(
@@ -361,10 +361,17 @@ class SlackAdapter(Platform):
361
361
  self._handle_webhook_event,
362
362
  )
363
363
 
364
- logger.info(
365
- f"Slack 适配器 (Webhook Mode) 启动中,监听 {self.webhook_host}:{self.webhook_port}{self.webhook_path}...",
366
- )
367
- await self.webhook_client.start()
364
+ # 如果启用统一 webhook 模式,则不启动独立服务器
365
+ webhook_uuid = self.config.get("webhook_uuid")
366
+ if self.unified_webhook_mode and webhook_uuid:
367
+ log_webhook_info(f"{self.meta().id}(Slack)", webhook_uuid)
368
+ # 保持运行状态,等待 shutdown
369
+ await self.webhook_client.shutdown_event.wait()
370
+ else:
371
+ logger.info(
372
+ f"Slack 适配器 (Webhook Mode) 启动中,监听 {self.webhook_host}:{self.webhook_port}{self.webhook_path}...",
373
+ )
374
+ await self.webhook_client.start()
368
375
 
369
376
  else:
370
377
  raise ValueError(
@@ -391,6 +398,13 @@ class SlackAdapter(Platform):
391
398
  if abm:
392
399
  await self.handle_msg(abm)
393
400
 
401
+ async def webhook_callback(self, request: Any) -> Any:
402
+ """统一 Webhook 回调入口"""
403
+ if self.connection_mode != "webhook" or not self.webhook_client:
404
+ return {"error": "Slack adapter is not in webhook mode"}, 400
405
+
406
+ return await self.webhook_client.handle_callback(request)
407
+
394
408
  async def terminate(self):
395
409
  if self.socket_client:
396
410
  await self.socket_client.stop()
@@ -31,7 +31,7 @@ class SlackMessageEvent(AstrMessageEvent):
31
31
  async def _from_segment_to_slack_block(
32
32
  segment: BaseMessageComponent,
33
33
  web_client: AsyncWebClient,
34
- ) -> dict:
34
+ ) -> dict | None:
35
35
  """将消息段转换为 Slack 块格式"""
36
36
  if isinstance(segment, Plain):
37
37
  return {"type": "section", "text": {"type": "mrkdwn", "text": segment.text}}
@@ -85,7 +85,6 @@ class SlackMessageEvent(AstrMessageEvent):
85
85
  "text": f"文件: <{file_url}|{segment.name or '文件'}>",
86
86
  },
87
87
  }
88
- return {"type": "section", "text": {"type": "mrkdwn", "text": str(segment)}}
89
88
 
90
89
  @staticmethod
91
90
  async def _parse_slack_blocks(
@@ -115,7 +114,8 @@ class SlackMessageEvent(AstrMessageEvent):
115
114
  segment,
116
115
  web_client,
117
116
  )
118
- blocks.append(block)
117
+ if block:
118
+ blocks.append(block)
119
119
 
120
120
  # 如果最后还有文本内容
121
121
  if text_content.strip():
@@ -42,8 +42,7 @@ class TelegramPlatformAdapter(Platform):
42
42
  platform_settings: dict,
43
43
  event_queue: asyncio.Queue,
44
44
  ) -> None:
45
- super().__init__(event_queue)
46
- self.config = platform_config
45
+ super().__init__(platform_config, event_queue)
47
46
  self.settings = platform_settings
48
47
  self.client_self_id = uuid.uuid4().hex[:8]
49
48
 
@@ -6,7 +6,9 @@ from collections.abc import Awaitable, Callable
6
6
  from typing import Any
7
7
 
8
8
  from astrbot import logger
9
- from astrbot.core.message.components import Image, Plain, Record
9
+ from astrbot.core import db_helper
10
+ from astrbot.core.db.po import PlatformMessageHistory
11
+ from astrbot.core.message.components import File, Image, Plain, Record, Reply, Video
10
12
  from astrbot.core.message.message_event_result import MessageChain
11
13
  from astrbot.core.platform import (
12
14
  AstrBotMessage,
@@ -74,9 +76,8 @@ class WebChatAdapter(Platform):
74
76
  platform_settings: dict,
75
77
  event_queue: asyncio.Queue,
76
78
  ) -> None:
77
- super().__init__(event_queue)
79
+ super().__init__(platform_config, event_queue)
78
80
 
79
- self.config = platform_config
80
81
  self.settings = platform_settings
81
82
  self.unique_session = platform_settings["unique_session"]
82
83
  self.imgs_dir = os.path.join(get_astrbot_data_path(), "webchat", "imgs")
@@ -96,6 +97,92 @@ class WebChatAdapter(Platform):
96
97
  await WebChatMessageEvent._send(message_chain, session.session_id)
97
98
  await super().send_by_session(session, message_chain)
98
99
 
100
+ async def _get_message_history(
101
+ self, message_id: int
102
+ ) -> PlatformMessageHistory | None:
103
+ return await db_helper.get_platform_message_history_by_id(message_id)
104
+
105
+ async def _parse_message_parts(
106
+ self,
107
+ message_parts: list,
108
+ depth: int = 0,
109
+ max_depth: int = 1,
110
+ ) -> tuple[list, list[str]]:
111
+ """解析消息段列表,返回消息组件列表和纯文本列表
112
+
113
+ Args:
114
+ message_parts: 消息段列表
115
+ depth: 当前递归深度
116
+ max_depth: 最大递归深度(用于处理 reply)
117
+
118
+ Returns:
119
+ tuple[list, list[str]]: (消息组件列表, 纯文本列表)
120
+ """
121
+ components = []
122
+ text_parts = []
123
+
124
+ for part in message_parts:
125
+ part_type = part.get("type")
126
+ if part_type == "plain":
127
+ text = part.get("text", "")
128
+ components.append(Plain(text))
129
+ text_parts.append(text)
130
+ elif part_type == "reply":
131
+ message_id = part.get("message_id")
132
+ reply_chain = []
133
+ reply_message_str = ""
134
+ sender_id = None
135
+ sender_name = None
136
+
137
+ # recursively get the content of the referenced message
138
+ if depth < max_depth and message_id:
139
+ history = await self._get_message_history(message_id)
140
+ if history and history.content:
141
+ reply_parts = history.content.get("message", [])
142
+ if isinstance(reply_parts, list):
143
+ (
144
+ reply_chain,
145
+ reply_text_parts,
146
+ ) = await self._parse_message_parts(
147
+ reply_parts,
148
+ depth=depth + 1,
149
+ max_depth=max_depth,
150
+ )
151
+ reply_message_str = "".join(reply_text_parts)
152
+ sender_id = history.sender_id
153
+ sender_name = history.sender_name
154
+
155
+ components.append(
156
+ Reply(
157
+ id=message_id,
158
+ chain=reply_chain,
159
+ message_str=reply_message_str,
160
+ sender_id=sender_id,
161
+ sender_nickname=sender_name,
162
+ )
163
+ )
164
+ elif part_type == "image":
165
+ path = part.get("path")
166
+ if path:
167
+ components.append(Image.fromFileSystem(path))
168
+ elif part_type == "record":
169
+ path = part.get("path")
170
+ if path:
171
+ components.append(Record.fromFileSystem(path))
172
+ elif part_type == "file":
173
+ path = part.get("path")
174
+ if path:
175
+ filename = part.get("filename") or (
176
+ os.path.basename(path) if path else "file"
177
+ )
178
+ components.append(File(name=filename, file=path))
179
+ elif part_type == "video":
180
+ path = part.get("path")
181
+ if path:
182
+ components.append(Video.fromFileSystem(path))
183
+
184
+ return components, text_parts
185
+
99
186
  async def convert_message(self, data: tuple) -> AstrBotMessage:
100
187
  username, cid, payload = data
101
188
 
@@ -108,36 +195,15 @@ class WebChatAdapter(Platform):
108
195
  abm.session_id = f"webchat!{username}!{cid}"
109
196
 
110
197
  abm.message_id = str(uuid.uuid4())
111
- abm.message = []
112
-
113
- if payload["message"]:
114
- abm.message.append(Plain(payload["message"]))
115
- if payload["image_url"]:
116
- if isinstance(payload["image_url"], list):
117
- for img in payload["image_url"]:
118
- abm.message.append(
119
- Image.fromFileSystem(os.path.join(self.imgs_dir, img)),
120
- )
121
- else:
122
- abm.message.append(
123
- Image.fromFileSystem(
124
- os.path.join(self.imgs_dir, payload["image_url"]),
125
- ),
126
- )
127
- if payload["audio_url"]:
128
- if isinstance(payload["audio_url"], list):
129
- for audio in payload["audio_url"]:
130
- path = os.path.join(self.imgs_dir, audio)
131
- abm.message.append(Record(file=path, path=path))
132
- else:
133
- path = os.path.join(self.imgs_dir, payload["audio_url"])
134
- abm.message.append(Record(file=path, path=path))
198
+
199
+ # 处理消息段列表
200
+ message_parts = payload.get("message", [])
201
+ abm.message, message_str_parts = await self._parse_message_parts(message_parts)
135
202
 
136
203
  logger.debug(f"WebChatAdapter: {abm.message}")
137
204
 
138
- message_str = payload["message"]
139
205
  abm.timestamp = int(time.time())
140
- abm.message_str = message_str
206
+ abm.message_str = "".join(message_str_parts)
141
207
  abm.raw_message = data
142
208
  return abm
143
209
 
@@ -1,12 +1,12 @@
1
1
  import base64
2
2
  import os
3
+ import shutil
3
4
  import uuid
4
5
 
5
6
  from astrbot.api import logger
6
7
  from astrbot.api.event import AstrMessageEvent, MessageChain
7
- from astrbot.api.message_components import Image, Plain, Record
8
+ from astrbot.api.message_components import File, Image, Plain, Record
8
9
  from astrbot.core.utils.astrbot_path import get_astrbot_data_path
9
- from astrbot.core.utils.io import download_image_by_url
10
10
 
11
11
  from .webchat_queue_mgr import webchat_queue_mgr
12
12
 
@@ -19,7 +19,9 @@ class WebChatMessageEvent(AstrMessageEvent):
19
19
  os.makedirs(imgs_dir, exist_ok=True)
20
20
 
21
21
  @staticmethod
22
- async def _send(message: MessageChain, session_id: str, streaming: bool = False):
22
+ async def _send(
23
+ message: MessageChain | None, session_id: str, streaming: bool = False
24
+ ) -> str | None:
23
25
  cid = session_id.split("!")[-1]
24
26
  web_chat_back_queue = webchat_queue_mgr.get_or_create_back_queue(cid)
25
27
  if not message:
@@ -30,7 +32,7 @@ class WebChatMessageEvent(AstrMessageEvent):
30
32
  "streaming": False,
31
33
  }, # end means this request is finished
32
34
  )
33
- return ""
35
+ return
34
36
 
35
37
  data = ""
36
38
  for comp in message.chain:
@@ -47,24 +49,11 @@ class WebChatMessageEvent(AstrMessageEvent):
47
49
  )
48
50
  elif isinstance(comp, Image):
49
51
  # save image to local
50
- filename = str(uuid.uuid4()) + ".jpg"
52
+ filename = f"{str(uuid.uuid4())}.jpg"
51
53
  path = os.path.join(imgs_dir, filename)
52
- if comp.file and comp.file.startswith("file:///"):
53
- ph = comp.file[8:]
54
- with open(path, "wb") as f:
55
- with open(ph, "rb") as f2:
56
- f.write(f2.read())
57
- elif comp.file.startswith("base64://"):
58
- base64_str = comp.file[9:]
59
- image_data = base64.b64decode(base64_str)
60
- with open(path, "wb") as f:
61
- f.write(image_data)
62
- elif comp.file and comp.file.startswith("http"):
63
- await download_image_by_url(comp.file, path=path)
64
- else:
65
- with open(path, "wb") as f:
66
- with open(comp.file, "rb") as f2:
67
- f.write(f2.read())
54
+ image_base64 = await comp.convert_to_base64()
55
+ with open(path, "wb") as f:
56
+ f.write(base64.b64decode(image_base64))
68
57
  data = f"[IMAGE]{filename}"
69
58
  await web_chat_back_queue.put(
70
59
  {
@@ -76,19 +65,11 @@ class WebChatMessageEvent(AstrMessageEvent):
76
65
  )
77
66
  elif isinstance(comp, Record):
78
67
  # save record to local
79
- filename = str(uuid.uuid4()) + ".wav"
68
+ filename = f"{str(uuid.uuid4())}.wav"
80
69
  path = os.path.join(imgs_dir, filename)
81
- if comp.file and comp.file.startswith("file:///"):
82
- ph = comp.file[8:]
83
- with open(path, "wb") as f:
84
- with open(ph, "rb") as f2:
85
- f.write(f2.read())
86
- elif comp.file and comp.file.startswith("http"):
87
- await download_image_by_url(comp.file, path=path)
88
- else:
89
- with open(path, "wb") as f:
90
- with open(comp.file, "rb") as f2:
91
- f.write(f2.read())
70
+ record_base64 = await comp.convert_to_base64()
71
+ with open(path, "wb") as f:
72
+ f.write(base64.b64decode(record_base64))
92
73
  data = f"[RECORD]{filename}"
93
74
  await web_chat_back_queue.put(
94
75
  {
@@ -98,6 +79,23 @@ class WebChatMessageEvent(AstrMessageEvent):
98
79
  "streaming": streaming,
99
80
  },
100
81
  )
82
+ elif isinstance(comp, File):
83
+ # save file to local
84
+ file_path = await comp.get_file()
85
+ original_name = comp.name or os.path.basename(file_path)
86
+ ext = os.path.splitext(original_name)[1] or ""
87
+ filename = f"{uuid.uuid4()!s}{ext}"
88
+ dest_path = os.path.join(imgs_dir, filename)
89
+ shutil.copy2(file_path, dest_path)
90
+ data = f"[FILE]{filename}|{original_name}"
91
+ await web_chat_back_queue.put(
92
+ {
93
+ "type": "file",
94
+ "cid": cid,
95
+ "data": data,
96
+ "streaming": streaming,
97
+ },
98
+ )
101
99
  else:
102
100
  logger.debug(f"webchat 忽略: {comp.type}")
103
101
 
@@ -131,6 +129,8 @@ class WebChatMessageEvent(AstrMessageEvent):
131
129
  session_id=self.session_id,
132
130
  streaming=True,
133
131
  )
132
+ if not r:
133
+ continue
134
134
  if chain.type == "reasoning":
135
135
  reasoning_content += chain.get_plain_text()
136
136
  else:
@@ -42,10 +42,9 @@ class WeChatPadProAdapter(Platform):
42
42
  platform_settings: dict,
43
43
  event_queue: asyncio.Queue,
44
44
  ) -> None:
45
- super().__init__(event_queue)
45
+ super().__init__(platform_config, event_queue)
46
46
  self._shutdown_event = None
47
47
  self.wxnewpass = None
48
- self.config = platform_config
49
48
  self.settings = platform_settings
50
49
  self.unique_session = platform_settings.get("unique_session", False)
51
50