AstrBot 4.7.4__py3-none-any.whl → 4.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. astrbot/cli/__init__.py +1 -1
  2. astrbot/core/agent/runners/tool_loop_agent_runner.py +0 -1
  3. astrbot/core/agent/tool.py +7 -2
  4. astrbot/core/astr_agent_run_util.py +15 -1
  5. astrbot/core/astr_agent_tool_exec.py +5 -1
  6. astrbot/core/config/astrbot_config.py +4 -0
  7. astrbot/core/config/default.py +116 -1
  8. astrbot/core/core_lifecycle.py +1 -1
  9. astrbot/core/db/__init__.py +32 -4
  10. astrbot/core/db/migration/migra_3_to_4.py +2 -0
  11. astrbot/core/db/migration/sqlite_v3.py +6 -4
  12. astrbot/core/db/po.py +16 -15
  13. astrbot/core/db/sqlite.py +56 -1
  14. astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +2 -0
  15. astrbot/core/event_bus.py +6 -1
  16. astrbot/core/knowledge_base/retrieval/manager.py +5 -1
  17. astrbot/core/log.py +2 -1
  18. astrbot/core/message/components.py +9 -3
  19. astrbot/core/persona_mgr.py +2 -2
  20. astrbot/core/pipeline/content_safety_check/stage.py +1 -1
  21. astrbot/core/pipeline/context_utils.py +2 -1
  22. astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +1 -1
  23. astrbot/core/pipeline/process_stage/method/star_request.py +1 -2
  24. astrbot/core/pipeline/process_stage/stage.py +1 -1
  25. astrbot/core/pipeline/respond/stage.py +4 -2
  26. astrbot/core/pipeline/result_decorate/stage.py +68 -21
  27. astrbot/core/pipeline/scheduler.py +5 -1
  28. astrbot/core/pipeline/waking_check/stage.py +10 -0
  29. astrbot/core/platform/astr_message_event.py +5 -3
  30. astrbot/core/platform/astrbot_message.py +2 -2
  31. astrbot/core/platform/manager.py +71 -9
  32. astrbot/core/platform/platform.py +109 -4
  33. astrbot/core/platform/platform_metadata.py +1 -1
  34. astrbot/core/platform/register.py +1 -0
  35. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +8 -6
  36. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +13 -8
  37. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +28 -22
  38. astrbot/core/platform/sources/dingtalk/dingtalk_event.py +5 -2
  39. astrbot/core/platform/sources/discord/client.py +16 -4
  40. astrbot/core/platform/sources/discord/components.py +2 -2
  41. astrbot/core/platform/sources/discord/discord_platform_adapter.py +53 -26
  42. astrbot/core/platform/sources/discord/discord_platform_event.py +29 -8
  43. astrbot/core/platform/sources/lark/lark_adapter.py +178 -22
  44. astrbot/core/platform/sources/lark/lark_event.py +39 -4
  45. astrbot/core/platform/sources/lark/server.py +206 -0
  46. astrbot/core/platform/sources/misskey/misskey_adapter.py +3 -5
  47. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +64 -18
  48. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +14 -10
  49. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -11
  50. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +15 -2
  51. astrbot/core/platform/sources/satori/satori_adapter.py +1 -2
  52. astrbot/core/platform/sources/slack/client.py +58 -40
  53. astrbot/core/platform/sources/slack/slack_adapter.py +36 -16
  54. astrbot/core/platform/sources/slack/slack_event.py +11 -10
  55. astrbot/core/platform/sources/telegram/tg_adapter.py +2 -3
  56. astrbot/core/platform/sources/telegram/tg_event.py +23 -27
  57. astrbot/core/platform/sources/webchat/webchat_adapter.py +97 -31
  58. astrbot/core/platform/sources/webchat/webchat_event.py +35 -35
  59. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +27 -11
  60. astrbot/core/platform/sources/wecom/wecom_adapter.py +75 -36
  61. astrbot/core/platform/sources/wecom/wecom_event.py +3 -3
  62. astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +26 -9
  63. astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +3 -3
  64. astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +27 -5
  65. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +81 -35
  66. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +11 -8
  67. astrbot/core/platform_message_history_mgr.py +3 -3
  68. astrbot/core/provider/func_tool_manager.py +3 -3
  69. astrbot/core/provider/manager.py +130 -74
  70. astrbot/core/provider/provider.py +12 -1
  71. astrbot/core/provider/sources/azure_tts_source.py +31 -9
  72. astrbot/core/provider/sources/bailian_rerank_source.py +4 -0
  73. astrbot/core/provider/sources/dashscope_tts.py +3 -2
  74. astrbot/core/provider/sources/edge_tts_source.py +1 -1
  75. astrbot/core/provider/sources/fishaudio_tts_api_source.py +5 -4
  76. astrbot/core/provider/sources/gemini_embedding_source.py +15 -5
  77. astrbot/core/provider/sources/gemini_source.py +12 -10
  78. astrbot/core/provider/sources/minimax_tts_api_source.py +4 -2
  79. astrbot/core/provider/sources/openai_embedding_source.py +2 -2
  80. astrbot/core/provider/sources/openai_source.py +4 -0
  81. astrbot/core/provider/sources/sensevoice_selfhosted_source.py +5 -2
  82. astrbot/core/provider/sources/vllm_rerank_source.py +1 -0
  83. astrbot/core/provider/sources/whisper_api_source.py +44 -12
  84. astrbot/core/provider/sources/whisper_selfhosted_source.py +6 -2
  85. astrbot/core/provider/sources/xinference_rerank_source.py +10 -2
  86. astrbot/core/star/context.py +2 -2
  87. astrbot/core/star/register/star_handler.py +22 -5
  88. astrbot/core/star/star_handler.py +85 -4
  89. astrbot/core/updator.py +3 -3
  90. astrbot/core/utils/io.py +1 -1
  91. astrbot/core/utils/session_waiter.py +17 -10
  92. astrbot/core/utils/shared_preferences.py +32 -0
  93. astrbot/core/utils/t2i/__init__.py +2 -2
  94. astrbot/core/utils/t2i/local_strategy.py +25 -31
  95. astrbot/core/utils/tencent_record_helper.py +2 -2
  96. astrbot/core/utils/version_comparator.py +6 -3
  97. astrbot/core/utils/webhook_utils.py +66 -0
  98. astrbot/dashboard/routes/__init__.py +2 -0
  99. astrbot/dashboard/routes/chat.py +311 -76
  100. astrbot/dashboard/routes/config.py +14 -5
  101. astrbot/dashboard/routes/knowledge_base.py +254 -79
  102. astrbot/dashboard/routes/log.py +13 -8
  103. astrbot/dashboard/routes/platform.py +100 -0
  104. astrbot/dashboard/routes/plugin.py +108 -51
  105. astrbot/dashboard/routes/route.py +2 -0
  106. astrbot/dashboard/server.py +9 -4
  107. {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/METADATA +50 -37
  108. {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/RECORD +111 -108
  109. {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/WHEEL +0 -0
  110. {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/entry_points.txt +0 -0
  111. {astrbot-4.7.4.dist-info → astrbot-4.9.0.dist-info}/licenses/LICENSE +0 -0
astrbot/core/log.py CHANGED
@@ -24,6 +24,7 @@ import asyncio
24
24
  import logging
25
25
  import os
26
26
  import sys
27
+ import time
27
28
  from asyncio import Queue
28
29
  from collections import deque
29
30
 
@@ -148,7 +149,7 @@ class LogQueueHandler(logging.Handler):
148
149
  self.log_broker.publish(
149
150
  {
150
151
  "level": record.levelname,
151
- "time": record.asctime,
152
+ "time": time.time(),
152
153
  "data": log_entry,
153
154
  },
154
155
  )
@@ -66,6 +66,9 @@ class ComponentType(str, Enum):
66
66
  class BaseMessageComponent(BaseModel):
67
67
  type: ComponentType
68
68
 
69
+ def __init__(self, **kwargs):
70
+ super().__init__(**kwargs)
71
+
69
72
  def toDict(self):
70
73
  data = {}
71
74
  for k, v in self.__dict__.items():
@@ -551,7 +554,7 @@ class Node(BaseMessageComponent):
551
554
  id: int | None = 0 # 忽略
552
555
  name: str | None = "" # qq昵称
553
556
  uin: str | None = "0" # qq号
554
- content: list[BaseMessageComponent] | None = []
557
+ content: list[BaseMessageComponent] = []
555
558
  seq: str | list | None = "" # 忽略
556
559
  time: int | None = 0 # 忽略
557
560
 
@@ -615,7 +618,7 @@ class Nodes(BaseMessageComponent):
615
618
  ret["messages"].append(d)
616
619
  return ret
617
620
 
618
- async def to_dict(self):
621
+ async def to_dict(self) -> dict:
619
622
  """将 Nodes 转换为字典格式,适用于 OneBot JSON 格式"""
620
623
  ret = {"messages": []}
621
624
  for node in self.nodes:
@@ -714,12 +717,15 @@ class File(BaseMessageComponent):
714
717
 
715
718
  if self.url:
716
719
  await self._download_file()
717
- return os.path.abspath(self.file_)
720
+ if self.file_:
721
+ return os.path.abspath(self.file_)
718
722
 
719
723
  return ""
720
724
 
721
725
  async def _download_file(self):
722
726
  """下载文件"""
727
+ if not self.url:
728
+ raise ValueError("Download failed: No URL provided in File component.")
723
729
  download_dir = os.path.join(get_astrbot_data_path(), "temp")
724
730
  os.makedirs(download_dir, exist_ok=True)
725
731
  if self.name:
@@ -98,8 +98,8 @@ class PersonaManager:
98
98
  self,
99
99
  persona_id: str,
100
100
  system_prompt: str,
101
- begin_dialogs: list[str] = None,
102
- tools: list[str] = None,
101
+ begin_dialogs: list[str] | None = None,
102
+ tools: list[str] | None = None,
103
103
  ) -> Persona:
104
104
  """创建新的 persona。tools 参数为 None 时表示使用所有工具,空列表表示不使用任何工具"""
105
105
  if await self.db.get_persona_by_id(persona_id):
@@ -24,7 +24,7 @@ class ContentSafetyCheckStage(Stage):
24
24
  self,
25
25
  event: AstrMessageEvent,
26
26
  check_text: str | None = None,
27
- ) -> None | AsyncGenerator[None, None]:
27
+ ) -> AsyncGenerator[None, None]:
28
28
  """检查内容安全"""
29
29
  text = check_text if check_text else event.get_message_str()
30
30
  ok, info = self.strategy_selector.check(text)
@@ -11,7 +11,7 @@ from astrbot.core.star.star_handler import EventType, star_handlers_registry
11
11
 
12
12
  async def call_handler(
13
13
  event: AstrMessageEvent,
14
- handler: T.Callable[..., T.Awaitable[T.Any]],
14
+ handler: T.Callable[..., T.Awaitable[T.Any] | T.AsyncGenerator[T.Any, None]],
15
15
  *args,
16
16
  **kwargs,
17
17
  ) -> T.AsyncGenerator[T.Any, None]:
@@ -91,6 +91,7 @@ async def call_event_hook(
91
91
  )
92
92
  for handler in handlers:
93
93
  try:
94
+ assert inspect.iscoroutinefunction(handler.handler)
94
95
  logger.debug(
95
96
  f"hook({hook_type.name}) -> {star_map[handler.handler_module_path].name} - {handler.handler_name}",
96
97
  )
@@ -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请在控制台查看和分享错误详情。\n"
60
+ f"错误信息: {e!s}\n\n请在平台日志查看和分享错误详情。\n"
61
61
  )
62
62
  yield MessageChain().message(err_msg)
63
63
 
@@ -16,7 +16,6 @@ from ..stage import Stage
16
16
 
17
17
  class StarRequestSubStage(Stage):
18
18
  async def initialize(self, ctx: PipelineContext) -> None:
19
- self.curr_provider = ctx.plugin_manager.context.get_using_provider()
20
19
  self.prompt_prefix = ctx.astrbot_config["provider_settings"]["prompt_prefix"]
21
20
  self.identifier = ctx.astrbot_config["provider_settings"]["identifier"]
22
21
  self.ctx = ctx
@@ -24,7 +23,7 @@ class StarRequestSubStage(Stage):
24
23
  async def process(
25
24
  self,
26
25
  event: AstrMessageEvent,
27
- ) -> AsyncGenerator[None, None]:
26
+ ) -> AsyncGenerator[Any, None]:
28
27
  activated_handlers: list[StarHandlerMetadata] = event.get_extra(
29
28
  "activated_handlers",
30
29
  )
@@ -60,7 +60,7 @@ class ProcessStage(Stage):
60
60
  ):
61
61
  # 是否有过发送操作 and 是否是被 @ 或者通过唤醒前缀
62
62
  if (
63
- event.get_result() and not event.get_result().is_stopped()
63
+ event.get_result() and not event.is_stopped()
64
64
  ) or not event.get_result():
65
65
  async for _ in self.agent_sub_stage.process(event):
66
66
  yield
@@ -117,7 +117,9 @@ class RespondStage(Stage):
117
117
  if not self.enable_seg:
118
118
  return False
119
119
 
120
- if self.only_llm_result and not event.get_result().is_llm_result():
120
+ if (result := event.get_result()) is None:
121
+ return False
122
+ if self.only_llm_result and result.is_llm_result():
121
123
  return False
122
124
 
123
125
  if event.get_platform_name() in [
@@ -185,7 +187,7 @@ class RespondStage(Stage):
185
187
  if isinstance(component, Comp.File) and component.file:
186
188
  # 支持 File 消息段的路径映射。
187
189
  component.file = path_Mapping(mappings, component.file)
188
- event.get_result().chain[idx] = component
190
+ result.chain[idx] = component
189
191
 
190
192
  # 检查消息链是否为空
191
193
  try:
@@ -6,6 +6,7 @@ from collections.abc import AsyncGenerator
6
6
  from astrbot.core import file_token_service, html_renderer, logger
7
7
  from astrbot.core.message.components import At, File, Image, Node, Plain, Record, Reply
8
8
  from astrbot.core.message.message_event_result import ResultContentType
9
+ from astrbot.core.pipeline.content_safety_check.stage import ContentSafetyCheckStage
9
10
  from astrbot.core.platform.astr_message_event import AstrMessageEvent
10
11
  from astrbot.core.platform.message_type import MessageType
11
12
  from astrbot.core.star.session_llm_manager import SessionServiceManager
@@ -53,7 +54,22 @@ class ResultDecorateStage(Stage):
53
54
  self.only_llm_result = ctx.astrbot_config["platform_settings"][
54
55
  "segmented_reply"
55
56
  ]["only_llm_result"]
57
+ self.split_mode = ctx.astrbot_config["platform_settings"][
58
+ "segmented_reply"
59
+ ].get("split_mode", "regex")
56
60
  self.regex = ctx.astrbot_config["platform_settings"]["segmented_reply"]["regex"]
61
+ self.split_words = ctx.astrbot_config["platform_settings"][
62
+ "segmented_reply"
63
+ ].get("split_words", ["。", "?", "!", "~", "…"])
64
+ if self.split_words:
65
+ escaped_words = sorted(
66
+ [re.escape(word) for word in self.split_words], key=len, reverse=True
67
+ )
68
+ self.split_words_pattern = re.compile(
69
+ f"(.*?({'|'.join(escaped_words)})|.+$)", re.DOTALL
70
+ )
71
+ else:
72
+ self.split_words_pattern = None
57
73
  self.content_cleanup_rule = ctx.astrbot_config["platform_settings"][
58
74
  "segmented_reply"
59
75
  ]["content_cleanup_rule"]
@@ -69,6 +85,28 @@ class ResultDecorateStage(Stage):
69
85
  self.content_safe_check_stage = stage_cls()
70
86
  await self.content_safe_check_stage.initialize(ctx)
71
87
 
88
+ def _split_text_by_words(self, text: str) -> list[str]:
89
+ """使用分段词列表分段文本"""
90
+ if not self.split_words_pattern:
91
+ return [text]
92
+
93
+ segments = self.split_words_pattern.findall(text)
94
+ result = []
95
+ for seg in segments:
96
+ if isinstance(seg, tuple):
97
+ content = seg[0]
98
+ if not isinstance(content, str):
99
+ continue
100
+ for word in self.split_words:
101
+ if content.endswith(word):
102
+ content = content[: -len(word)]
103
+ break
104
+ if content.strip():
105
+ result.append(content)
106
+ elif seg and seg.strip():
107
+ result.append(seg)
108
+ return result if result else [text]
109
+
72
110
  async def process(
73
111
  self,
74
112
  event: AstrMessageEvent,
@@ -93,11 +131,13 @@ class ResultDecorateStage(Stage):
93
131
  for comp in result.chain:
94
132
  if isinstance(comp, Plain):
95
133
  text += comp.text
96
- async for _ in self.content_safe_check_stage.process(
97
- event,
98
- check_text=text,
99
- ):
100
- yield
134
+
135
+ if isinstance(self.content_safe_check_stage, ContentSafetyCheckStage):
136
+ async for _ in self.content_safe_check_stage.process(
137
+ event,
138
+ check_text=text,
139
+ ):
140
+ yield
101
141
 
102
142
  # 发送消息前事件钩子
103
143
  handlers = star_handlers_registry.get_handlers_by_event_type(
@@ -114,7 +154,8 @@ class ResultDecorateStage(Stage):
114
154
  "启用流式输出时,依赖发送消息前事件钩子的插件可能无法正常工作",
115
155
  )
116
156
  await handler.handler(event)
117
- if event.get_result() is None or not event.get_result().chain:
157
+
158
+ if (result := event.get_result()) is None or not result.chain:
118
159
  logger.debug(
119
160
  f"hook(on_decorating_result) -> {star_map[handler.handler_module_path].name} - {handler.handler_name} 将消息结果清空。",
120
161
  )
@@ -161,21 +202,27 @@ class ResultDecorateStage(Stage):
161
202
  # 不分段回复
162
203
  new_chain.append(comp)
163
204
  continue
164
- try:
165
- split_response = re.findall(
166
- self.regex,
167
- comp.text,
168
- re.DOTALL | re.MULTILINE,
169
- )
170
- except re.error:
171
- logger.error(
172
- f"分段回复正则表达式错误,使用默认分段方式: {traceback.format_exc()}",
173
- )
174
- split_response = re.findall(
175
- r".*?[。?!~…]+|.+$",
176
- comp.text,
177
- re.DOTALL | re.MULTILINE,
178
- )
205
+
206
+ # 根据 split_mode 选择分段方式
207
+ if self.split_mode == "words":
208
+ split_response = self._split_text_by_words(comp.text)
209
+ else: # regex 模式
210
+ try:
211
+ split_response = re.findall(
212
+ self.regex,
213
+ comp.text,
214
+ re.DOTALL | re.MULTILINE,
215
+ )
216
+ except re.error:
217
+ logger.error(
218
+ f"分段回复正则表达式错误,使用默认分段方式: {traceback.format_exc()}",
219
+ )
220
+ split_response = re.findall(
221
+ r".*?[。?!~…]+|.+$",
222
+ comp.text,
223
+ re.DOTALL | re.MULTILINE,
224
+ )
225
+
179
226
  if not split_response:
180
227
  new_chain.append(comp)
181
228
  continue
@@ -2,6 +2,10 @@ from collections.abc import AsyncGenerator
2
2
 
3
3
  from astrbot.core import logger
4
4
  from astrbot.core.platform import AstrMessageEvent
5
+ from astrbot.core.platform.sources.webchat.webchat_event import WebChatMessageEvent
6
+ from astrbot.core.platform.sources.wecom_ai_bot.wecomai_event import (
7
+ WecomAIBotMessageEvent,
8
+ )
5
9
 
6
10
  from . import STAGES_ORDER
7
11
  from .context import PipelineContext
@@ -78,7 +82,7 @@ class PipelineScheduler:
78
82
  await self._process_stages(event)
79
83
 
80
84
  # 如果没有发送操作, 则发送一个空消息, 以便于后续的处理
81
- if event.get_platform_name() in ["webchat", "wecom_ai_bot"]:
85
+ if isinstance(event, (WebChatMessageEvent, WecomAIBotMessageEvent)):
82
86
  await event.send(None)
83
87
 
84
88
  logger.debug("pipeline 执行完毕。")
@@ -50,6 +50,9 @@ class WakingCheckStage(Stage):
50
50
  "ignore_at_all",
51
51
  False,
52
52
  )
53
+ self.disable_builtin_commands = self.ctx.astrbot_config.get(
54
+ "disable_builtin_commands", False
55
+ )
53
56
 
54
57
  async def process(
55
58
  self,
@@ -131,6 +134,13 @@ class WakingCheckStage(Stage):
131
134
  EventType.AdapterMessageEvent,
132
135
  plugins_name=event.plugins_name,
133
136
  ):
137
+ if (
138
+ self.disable_builtin_commands
139
+ and handler.handler_module_path == "packages.builtin_commands.main"
140
+ ):
141
+ logger.debug("skipping builtin command")
142
+ continue
143
+
134
144
  # filter 需满足 AND 逻辑关系
135
145
  passed = True
136
146
  permission_not_pass = False
@@ -153,7 +153,9 @@ class AstrMessageEvent(abc.ABC):
153
153
 
154
154
  def get_sender_name(self) -> str:
155
155
  """获取消息发送者的名称。(可能会返回空字符串)"""
156
- return self.message_obj.sender.nickname
156
+ if isinstance(self.message_obj.sender.nickname, str):
157
+ return self.message_obj.sender.nickname
158
+ return ""
157
159
 
158
160
  def set_extra(self, key, value):
159
161
  """设置额外的信息。"""
@@ -270,7 +272,7 @@ class AstrMessageEvent(abc.ABC):
270
272
  """
271
273
  self.call_llm = call_llm
272
274
 
273
- def get_result(self) -> MessageEventResult:
275
+ def get_result(self) -> MessageEventResult | None:
274
276
  """获取消息事件的结果。"""
275
277
  return self._result
276
278
 
@@ -320,7 +322,7 @@ class AstrMessageEvent(abc.ABC):
320
322
  self,
321
323
  prompt: str,
322
324
  func_tool_manager=None,
323
- session_id: str = None,
325
+ session_id: str = "",
324
326
  image_urls: list[str] | None = None,
325
327
  contexts: list | None = None,
326
328
  system_prompt: str = "",
@@ -54,7 +54,7 @@ class AstrBotMessage:
54
54
  self_id: str # 机器人的识别id
55
55
  session_id: str # 会话id。取决于 unique_session 的设置。
56
56
  message_id: str # 消息id
57
- group: Group # 群组
57
+ group: Group | None # 群组
58
58
  sender: MessageMember # 发送者
59
59
  message: list[BaseMessageComponent] # 消息链使用 Nakuru 的消息链格式
60
60
  message_str: str # 最直观的纯文本消息字符串
@@ -78,7 +78,7 @@ class AstrBotMessage:
78
78
  return ""
79
79
 
80
80
  @group_id.setter
81
- def group_id(self, value: str):
81
+ def group_id(self, value: str | None):
82
82
  """设置 group_id"""
83
83
  if value:
84
84
  if self.group:
@@ -5,8 +5,9 @@ from asyncio import Queue
5
5
  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
+ from astrbot.core.utils.webhook_utils import ensure_platform_webhook_config
8
9
 
9
- from .platform import Platform
10
+ from .platform import Platform, PlatformStatus
10
11
  from .register import platform_cls_map
11
12
  from .sources.webchat.webchat_adapter import WebChatAdapter
12
13
 
@@ -16,8 +17,9 @@ class PlatformManager:
16
17
  self.platform_insts: list[Platform] = []
17
18
  """加载的 Platform 的实例"""
18
19
 
19
- self._inst_map = {}
20
+ self._inst_map: dict[str, dict] = {}
20
21
 
22
+ self.astrbot_config = config
21
23
  self.platforms_config = config["platform"]
22
24
  self.settings = config["platform_settings"]
23
25
  """NOTE: 这里是 default 的配置文件,以保证最大的兼容性;
@@ -29,6 +31,8 @@ class PlatformManager:
29
31
  """初始化所有平台适配器"""
30
32
  for platform in self.platforms_config:
31
33
  try:
34
+ if ensure_platform_webhook_config(platform):
35
+ self.astrbot_config.save_config()
32
36
  await self.load_platform(platform)
33
37
  except Exception as e:
34
38
  logger.error(f"初始化 {platform} 平台适配器失败: {e}")
@@ -37,7 +41,10 @@ class PlatformManager:
37
41
  webchat_inst = WebChatAdapter({}, self.settings, self.event_queue)
38
42
  self.platform_insts.append(webchat_inst)
39
43
  asyncio.create_task(
40
- self._task_wrapper(asyncio.create_task(webchat_inst.run(), name="webchat")),
44
+ self._task_wrapper(
45
+ asyncio.create_task(webchat_inst.run(), name="webchat"),
46
+ platform=webchat_inst,
47
+ ),
41
48
  )
42
49
 
43
50
  async def load_platform(self, platform_config: dict):
@@ -107,7 +114,7 @@ class PlatformManager:
107
114
  )
108
115
  except (ImportError, ModuleNotFoundError) as e:
109
116
  logger.error(
110
- f"加载平台适配器 {platform_config['type']} 失败,原因:{e}。请检查依赖库是否安装。提示:可以在 管理面板->控制台->安装Pip库 中安装依赖库。",
117
+ f"加载平台适配器 {platform_config['type']} 失败,原因:{e}。请检查依赖库是否安装。提示:可以在 管理面板->平台日志->安装Pip库 中安装依赖库。",
111
118
  )
112
119
  except Exception as e:
113
120
  logger.error(f"加载平台适配器 {platform_config['type']} 失败,原因:{e}。")
@@ -131,6 +138,7 @@ class PlatformManager:
131
138
  inst.run(),
132
139
  name=f"platform_{platform_config['type']}_{platform_config['id']}",
133
140
  ),
141
+ platform=inst,
134
142
  ),
135
143
  )
136
144
  handlers = star_handlers_registry.get_handlers_by_event_type(
@@ -145,17 +153,28 @@ class PlatformManager:
145
153
  except Exception:
146
154
  logger.error(traceback.format_exc())
147
155
 
148
- async def _task_wrapper(self, task: asyncio.Task):
156
+ async def _task_wrapper(self, task: asyncio.Task, platform: Platform | None = None):
157
+ # 设置平台状态为运行中
158
+ if platform:
159
+ platform.status = PlatformStatus.RUNNING
160
+
149
161
  try:
150
162
  await task
151
163
  except asyncio.CancelledError:
152
- pass
164
+ if platform:
165
+ platform.status = PlatformStatus.STOPPED
153
166
  except Exception as e:
167
+ error_msg = str(e)
168
+ tb_str = traceback.format_exc()
154
169
  logger.error(f"------- 任务 {task.get_name()} 发生错误: {e}")
155
- for line in traceback.format_exc().split("\n"):
170
+ for line in tb_str.split("\n"):
156
171
  logger.error(f"| {line}")
157
172
  logger.error("-------")
158
173
 
174
+ # 记录错误到平台实例
175
+ if platform:
176
+ platform.record_error(error_msg, tb_str)
177
+
159
178
  async def reload(self, platform_config: dict):
160
179
  await self.terminate_platform(platform_config["id"])
161
180
  if platform_config["enable"]:
@@ -172,9 +191,9 @@ class PlatformManager:
172
191
  logger.info(f"正在尝试终止 {platform_id} 平台适配器 ...")
173
192
 
174
193
  # client_id = self._inst_map.pop(platform_id, None)
175
- info = self._inst_map.pop(platform_id, None)
194
+ info = self._inst_map.pop(platform_id)
176
195
  client_id = info["client_id"]
177
- inst = info["inst"]
196
+ inst: Platform = info["inst"]
178
197
  try:
179
198
  self.platform_insts.remove(
180
199
  next(
@@ -196,3 +215,46 @@ class PlatformManager:
196
215
 
197
216
  def get_insts(self):
198
217
  return self.platform_insts
218
+
219
+ def get_all_stats(self) -> dict:
220
+ """获取所有平台的统计信息
221
+
222
+ Returns:
223
+ 包含所有平台统计信息的字典
224
+ """
225
+ stats_list = []
226
+ total_errors = 0
227
+ running_count = 0
228
+ error_count = 0
229
+
230
+ for inst in self.platform_insts:
231
+ try:
232
+ stat = inst.get_stats()
233
+ stats_list.append(stat)
234
+ total_errors += stat.get("error_count", 0)
235
+ if stat.get("status") == PlatformStatus.RUNNING.value:
236
+ running_count += 1
237
+ elif stat.get("status") == PlatformStatus.ERROR.value:
238
+ error_count += 1
239
+ except Exception as e:
240
+ # 如果获取统计信息失败,记录基本信息
241
+ logger.warning(f"获取平台统计信息失败: {e}")
242
+ stats_list.append(
243
+ {
244
+ "id": getattr(inst, "config", {}).get("id", "unknown"),
245
+ "type": "unknown",
246
+ "status": "unknown",
247
+ "error_count": 0,
248
+ "last_error": None,
249
+ }
250
+ )
251
+
252
+ return {
253
+ "platforms": stats_list,
254
+ "summary": {
255
+ "total": len(stats_list),
256
+ "running": running_count,
257
+ "error": error_count,
258
+ "total_errors": total_errors,
259
+ },
260
+ }