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.
- astrbot/cli/__init__.py +1 -1
- astrbot/core/astr_agent_run_util.py +15 -1
- astrbot/core/config/default.py +58 -1
- astrbot/core/db/__init__.py +30 -1
- astrbot/core/db/sqlite.py +55 -1
- 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 +4 -3
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +4 -6
- 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 +1 -2
- 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/sources/whisper_api_source.py +43 -11
- 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 +19 -0
- astrbot/dashboard/routes/knowledge_base.py +1 -1
- astrbot/dashboard/routes/platform.py +100 -0
- astrbot/dashboard/server.py +3 -1
- {astrbot-4.7.4.dist-info → astrbot-4.8.0.dist-info}/METADATA +48 -37
- {astrbot-4.7.4.dist-info → astrbot-4.8.0.dist-info}/RECORD +46 -44
- {astrbot-4.7.4.dist-info → astrbot-4.8.0.dist-info}/WHEEL +0 -0
- {astrbot-4.7.4.dist-info → astrbot-4.8.0.dist-info}/entry_points.txt +0 -0
- {astrbot-4.7.4.dist-info → astrbot-4.8.0.dist-info}/licenses/LICENSE +0 -0
astrbot/cli/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "4.
|
|
1
|
+
__version__ = "4.8.0"
|
|
@@ -9,6 +9,7 @@ from astrbot.core.message.message_event_result import (
|
|
|
9
9
|
MessageEventResult,
|
|
10
10
|
ResultContentType,
|
|
11
11
|
)
|
|
12
|
+
from astrbot.core.provider.entities import LLMResponse
|
|
12
13
|
|
|
13
14
|
AgentRunner = ToolLoopAgentRunner[AstrAgentContext]
|
|
14
15
|
|
|
@@ -72,7 +73,20 @@ async def run_agent(
|
|
|
72
73
|
|
|
73
74
|
except Exception as e:
|
|
74
75
|
logger.error(traceback.format_exc())
|
|
75
|
-
|
|
76
|
+
|
|
77
|
+
err_msg = f"\n\nAstrBot 请求失败。\n错误类型: {type(e).__name__}\n错误信息: {e!s}\n\n请在平台日志查看和分享错误详情。\n"
|
|
78
|
+
|
|
79
|
+
error_llm_response = LLMResponse(
|
|
80
|
+
role="err",
|
|
81
|
+
completion_text=err_msg,
|
|
82
|
+
)
|
|
83
|
+
try:
|
|
84
|
+
await agent_runner.agent_hooks.on_agent_done(
|
|
85
|
+
agent_runner.run_context, error_llm_response
|
|
86
|
+
)
|
|
87
|
+
except Exception:
|
|
88
|
+
logger.exception("Error in on_agent_done hook")
|
|
89
|
+
|
|
76
90
|
if agent_runner.streaming:
|
|
77
91
|
yield MessageChain().message(err_msg)
|
|
78
92
|
else:
|
astrbot/core/config/default.py
CHANGED
|
@@ -4,9 +4,17 @@ import os
|
|
|
4
4
|
|
|
5
5
|
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
|
6
6
|
|
|
7
|
-
VERSION = "4.
|
|
7
|
+
VERSION = "4.8.0"
|
|
8
8
|
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
|
9
9
|
|
|
10
|
+
WEBHOOK_SUPPORTED_PLATFORMS = [
|
|
11
|
+
"qq_official_webhook",
|
|
12
|
+
"weixin_official_account",
|
|
13
|
+
"wecom",
|
|
14
|
+
"wecom_ai_bot",
|
|
15
|
+
"slack",
|
|
16
|
+
]
|
|
17
|
+
|
|
10
18
|
# 默认配置
|
|
11
19
|
DEFAULT_CONFIG = {
|
|
12
20
|
"config_version": 2,
|
|
@@ -185,6 +193,8 @@ CONFIG_METADATA_2 = {
|
|
|
185
193
|
"appid": "",
|
|
186
194
|
"secret": "",
|
|
187
195
|
"is_sandbox": False,
|
|
196
|
+
"unified_webhook_mode": True,
|
|
197
|
+
"webhook_uuid": "",
|
|
188
198
|
"callback_server_host": "0.0.0.0",
|
|
189
199
|
"port": 6196,
|
|
190
200
|
},
|
|
@@ -215,6 +225,8 @@ CONFIG_METADATA_2 = {
|
|
|
215
225
|
"token": "",
|
|
216
226
|
"encoding_aes_key": "",
|
|
217
227
|
"api_base_url": "https://api.weixin.qq.com/cgi-bin/",
|
|
228
|
+
"unified_webhook_mode": True,
|
|
229
|
+
"webhook_uuid": "",
|
|
218
230
|
"callback_server_host": "0.0.0.0",
|
|
219
231
|
"port": 6194,
|
|
220
232
|
"active_send_mode": False,
|
|
@@ -229,6 +241,8 @@ CONFIG_METADATA_2 = {
|
|
|
229
241
|
"encoding_aes_key": "",
|
|
230
242
|
"kf_name": "",
|
|
231
243
|
"api_base_url": "https://qyapi.weixin.qq.com/cgi-bin/",
|
|
244
|
+
"unified_webhook_mode": True,
|
|
245
|
+
"webhook_uuid": "",
|
|
232
246
|
"callback_server_host": "0.0.0.0",
|
|
233
247
|
"port": 6195,
|
|
234
248
|
},
|
|
@@ -241,6 +255,8 @@ CONFIG_METADATA_2 = {
|
|
|
241
255
|
"wecom_ai_bot_name": "",
|
|
242
256
|
"token": "",
|
|
243
257
|
"encoding_aes_key": "",
|
|
258
|
+
"unified_webhook_mode": True,
|
|
259
|
+
"webhook_uuid": "",
|
|
244
260
|
"callback_server_host": "0.0.0.0",
|
|
245
261
|
"port": 6198,
|
|
246
262
|
},
|
|
@@ -308,6 +324,8 @@ CONFIG_METADATA_2 = {
|
|
|
308
324
|
"app_token": "",
|
|
309
325
|
"signing_secret": "",
|
|
310
326
|
"slack_connection_mode": "socket", # webhook, socket
|
|
327
|
+
"unified_webhook_mode": True,
|
|
328
|
+
"webhook_uuid": "",
|
|
311
329
|
"slack_webhook_host": "0.0.0.0",
|
|
312
330
|
"slack_webhook_port": 6197,
|
|
313
331
|
"slack_webhook_path": "/astrbot-slack-webhook/callback",
|
|
@@ -387,16 +405,28 @@ CONFIG_METADATA_2 = {
|
|
|
387
405
|
"description": "Slack Webhook Host",
|
|
388
406
|
"type": "string",
|
|
389
407
|
"hint": "Only valid when Slack connection mode is `webhook`.",
|
|
408
|
+
"condition": {
|
|
409
|
+
"slack_connection_mode": "webhook",
|
|
410
|
+
"unified_webhook_mode": False,
|
|
411
|
+
},
|
|
390
412
|
},
|
|
391
413
|
"slack_webhook_port": {
|
|
392
414
|
"description": "Slack Webhook Port",
|
|
393
415
|
"type": "int",
|
|
394
416
|
"hint": "Only valid when Slack connection mode is `webhook`.",
|
|
417
|
+
"condition": {
|
|
418
|
+
"slack_connection_mode": "webhook",
|
|
419
|
+
"unified_webhook_mode": False,
|
|
420
|
+
},
|
|
395
421
|
},
|
|
396
422
|
"slack_webhook_path": {
|
|
397
423
|
"description": "Slack Webhook Path",
|
|
398
424
|
"type": "string",
|
|
399
425
|
"hint": "Only valid when Slack connection mode is `webhook`.",
|
|
426
|
+
"condition": {
|
|
427
|
+
"slack_connection_mode": "webhook",
|
|
428
|
+
"unified_webhook_mode": False,
|
|
429
|
+
},
|
|
400
430
|
},
|
|
401
431
|
"active_send_mode": {
|
|
402
432
|
"description": "是否换用主动发送接口",
|
|
@@ -587,6 +617,33 @@ CONFIG_METADATA_2 = {
|
|
|
587
617
|
"type": "string",
|
|
588
618
|
"hint": "可选的 Discord 活动名称。留空则不设置活动。",
|
|
589
619
|
},
|
|
620
|
+
"port": {
|
|
621
|
+
"description": "回调服务器端口",
|
|
622
|
+
"type": "int",
|
|
623
|
+
"hint": "回调服务器端口。留空则不启用回调服务器。",
|
|
624
|
+
"condition": {
|
|
625
|
+
"unified_webhook_mode": False,
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
"callback_server_host": {
|
|
629
|
+
"description": "回调服务器主机",
|
|
630
|
+
"type": "string",
|
|
631
|
+
"hint": "回调服务器主机。留空则不启用回调服务器。",
|
|
632
|
+
"condition": {
|
|
633
|
+
"unified_webhook_mode": False,
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
"unified_webhook_mode": {
|
|
637
|
+
"description": "统一 Webhook 模式",
|
|
638
|
+
"type": "bool",
|
|
639
|
+
"hint": "启用后,将使用 AstrBot 统一 Webhook 入口,无需单独开启端口。回调地址为 /api/platform/webhook/{webhook_uuid}。",
|
|
640
|
+
},
|
|
641
|
+
"webhook_uuid": {
|
|
642
|
+
"invisible": True,
|
|
643
|
+
"description": "Webhook UUID",
|
|
644
|
+
"type": "string",
|
|
645
|
+
"hint": "统一 Webhook 模式下的唯一标识符,创建平台时自动生成。",
|
|
646
|
+
},
|
|
590
647
|
},
|
|
591
648
|
},
|
|
592
649
|
"platform_settings": {
|
astrbot/core/db/__init__.py
CHANGED
|
@@ -173,7 +173,7 @@ class BaseDatabase(abc.ABC):
|
|
|
173
173
|
content: dict,
|
|
174
174
|
sender_id: str | None = None,
|
|
175
175
|
sender_name: str | None = None,
|
|
176
|
-
) ->
|
|
176
|
+
) -> PlatformMessageHistory:
|
|
177
177
|
"""Insert a new platform message history record."""
|
|
178
178
|
...
|
|
179
179
|
|
|
@@ -198,6 +198,14 @@ class BaseDatabase(abc.ABC):
|
|
|
198
198
|
"""Get platform message history for a specific user."""
|
|
199
199
|
...
|
|
200
200
|
|
|
201
|
+
@abc.abstractmethod
|
|
202
|
+
async def get_platform_message_history_by_id(
|
|
203
|
+
self,
|
|
204
|
+
message_id: int,
|
|
205
|
+
) -> PlatformMessageHistory | None:
|
|
206
|
+
"""Get a platform message history record by its ID."""
|
|
207
|
+
...
|
|
208
|
+
|
|
201
209
|
@abc.abstractmethod
|
|
202
210
|
async def insert_attachment(
|
|
203
211
|
self,
|
|
@@ -213,6 +221,27 @@ class BaseDatabase(abc.ABC):
|
|
|
213
221
|
"""Get an attachment by its ID."""
|
|
214
222
|
...
|
|
215
223
|
|
|
224
|
+
@abc.abstractmethod
|
|
225
|
+
async def get_attachments(self, attachment_ids: list[str]) -> list[Attachment]:
|
|
226
|
+
"""Get multiple attachments by their IDs."""
|
|
227
|
+
...
|
|
228
|
+
|
|
229
|
+
@abc.abstractmethod
|
|
230
|
+
async def delete_attachment(self, attachment_id: str) -> bool:
|
|
231
|
+
"""Delete an attachment by its ID.
|
|
232
|
+
|
|
233
|
+
Returns True if the attachment was deleted, False if it was not found.
|
|
234
|
+
"""
|
|
235
|
+
...
|
|
236
|
+
|
|
237
|
+
@abc.abstractmethod
|
|
238
|
+
async def delete_attachments(self, attachment_ids: list[str]) -> int:
|
|
239
|
+
"""Delete multiple attachments by their IDs.
|
|
240
|
+
|
|
241
|
+
Returns the number of attachments deleted.
|
|
242
|
+
"""
|
|
243
|
+
...
|
|
244
|
+
|
|
216
245
|
@abc.abstractmethod
|
|
217
246
|
async def insert_persona(
|
|
218
247
|
self,
|
astrbot/core/db/sqlite.py
CHANGED
|
@@ -105,8 +105,8 @@ class SQLiteDatabase(BaseDatabase):
|
|
|
105
105
|
text("""
|
|
106
106
|
SELECT * FROM platform_stats
|
|
107
107
|
WHERE timestamp >= :start_time
|
|
108
|
-
ORDER BY timestamp DESC
|
|
109
108
|
GROUP BY platform_id
|
|
109
|
+
ORDER BY timestamp DESC
|
|
110
110
|
"""),
|
|
111
111
|
{"start_time": start_time},
|
|
112
112
|
)
|
|
@@ -449,6 +449,18 @@ class SQLiteDatabase(BaseDatabase):
|
|
|
449
449
|
result = await session.execute(query.offset(offset).limit(page_size))
|
|
450
450
|
return result.scalars().all()
|
|
451
451
|
|
|
452
|
+
async def get_platform_message_history_by_id(
|
|
453
|
+
self, message_id: int
|
|
454
|
+
) -> PlatformMessageHistory | None:
|
|
455
|
+
"""Get a platform message history record by its ID."""
|
|
456
|
+
async with self.get_db() as session:
|
|
457
|
+
session: AsyncSession
|
|
458
|
+
query = select(PlatformMessageHistory).where(
|
|
459
|
+
PlatformMessageHistory.id == message_id
|
|
460
|
+
)
|
|
461
|
+
result = await session.execute(query)
|
|
462
|
+
return result.scalar_one_or_none()
|
|
463
|
+
|
|
452
464
|
async def insert_attachment(self, path, type, mime_type):
|
|
453
465
|
"""Insert a new attachment record."""
|
|
454
466
|
async with self.get_db() as session:
|
|
@@ -470,6 +482,48 @@ class SQLiteDatabase(BaseDatabase):
|
|
|
470
482
|
result = await session.execute(query)
|
|
471
483
|
return result.scalar_one_or_none()
|
|
472
484
|
|
|
485
|
+
async def get_attachments(self, attachment_ids: list[str]) -> list:
|
|
486
|
+
"""Get multiple attachments by their IDs."""
|
|
487
|
+
if not attachment_ids:
|
|
488
|
+
return []
|
|
489
|
+
async with self.get_db() as session:
|
|
490
|
+
session: AsyncSession
|
|
491
|
+
query = select(Attachment).where(
|
|
492
|
+
Attachment.attachment_id.in_(attachment_ids)
|
|
493
|
+
)
|
|
494
|
+
result = await session.execute(query)
|
|
495
|
+
return list(result.scalars().all())
|
|
496
|
+
|
|
497
|
+
async def delete_attachment(self, attachment_id: str) -> bool:
|
|
498
|
+
"""Delete an attachment by its ID.
|
|
499
|
+
|
|
500
|
+
Returns True if the attachment was deleted, False if it was not found.
|
|
501
|
+
"""
|
|
502
|
+
async with self.get_db() as session:
|
|
503
|
+
session: AsyncSession
|
|
504
|
+
async with session.begin():
|
|
505
|
+
query = delete(Attachment).where(
|
|
506
|
+
col(Attachment.attachment_id) == attachment_id
|
|
507
|
+
)
|
|
508
|
+
result = await session.execute(query)
|
|
509
|
+
return result.rowcount > 0
|
|
510
|
+
|
|
511
|
+
async def delete_attachments(self, attachment_ids: list[str]) -> int:
|
|
512
|
+
"""Delete multiple attachments by their IDs.
|
|
513
|
+
|
|
514
|
+
Returns the number of attachments deleted.
|
|
515
|
+
"""
|
|
516
|
+
if not attachment_ids:
|
|
517
|
+
return 0
|
|
518
|
+
async with self.get_db() as session:
|
|
519
|
+
session: AsyncSession
|
|
520
|
+
async with session.begin():
|
|
521
|
+
query = delete(Attachment).where(
|
|
522
|
+
col(Attachment.attachment_id).in_(attachment_ids)
|
|
523
|
+
)
|
|
524
|
+
result = await session.execute(query)
|
|
525
|
+
return result.rowcount
|
|
526
|
+
|
|
473
527
|
async def insert_persona(
|
|
474
528
|
self,
|
|
475
529
|
persona_id,
|
|
@@ -57,7 +57,7 @@ async def run_third_party_agent(
|
|
|
57
57
|
logger.error(f"Third party agent runner error: {e}")
|
|
58
58
|
err_msg = (
|
|
59
59
|
f"\nAstrBot 请求失败。\n错误类型: {type(e).__name__}\n"
|
|
60
|
-
f"错误信息: {e!s}\n\n
|
|
60
|
+
f"错误信息: {e!s}\n\n请在平台日志查看和分享错误详情。\n"
|
|
61
61
|
)
|
|
62
62
|
yield MessageChain().message(err_msg)
|
|
63
63
|
|
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)
|
|
@@ -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,
|
|
@@ -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"]
|