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/cli/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "4.7.4"
1
+ __version__ = "4.9.0"
@@ -97,7 +97,6 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
97
97
  llm_resp_result = None
98
98
 
99
99
  async for llm_response in self._iter_llm_responses():
100
- assert isinstance(llm_response, LLMResponse)
101
100
  if llm_response.is_chunk:
102
101
  if llm_response.result_chain:
103
102
  yield AgentResponse(
@@ -1,4 +1,4 @@
1
- from collections.abc import Awaitable, Callable
1
+ from collections.abc import AsyncGenerator, Awaitable, Callable
2
2
  from typing import Any, Generic
3
3
 
4
4
  import jsonschema
@@ -7,6 +7,8 @@ from deprecated import deprecated
7
7
  from pydantic import Field, model_validator
8
8
  from pydantic.dataclasses import dataclass
9
9
 
10
+ from astrbot.core.message.message_event_result import MessageEventResult
11
+
10
12
  from .run_context import ContextWrapper, TContext
11
13
 
12
14
  ParametersType = dict[str, Any]
@@ -38,7 +40,10 @@ class ToolSchema:
38
40
  class FunctionTool(ToolSchema, Generic[TContext]):
39
41
  """A callable tool, for function calling."""
40
42
 
41
- handler: Callable[..., Awaitable[Any]] | None = None
43
+ handler: (
44
+ Callable[..., Awaitable[str | None] | AsyncGenerator[MessageEventResult, None]]
45
+ | None
46
+ ) = None
42
47
  """a callable that implements the tool's functionality. It should be an async function."""
43
48
 
44
49
  handler_module_path: str | None = None
@@ -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
- err_msg = f"\n\nAstrBot 请求失败。\n错误类型: {type(e).__name__}\n错误信息: {e!s}\n\n请在控制台查看和分享错误详情。\n"
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:
@@ -185,7 +185,11 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
185
185
 
186
186
  async def call_local_llm_tool(
187
187
  context: ContextWrapper[AstrAgentContext],
188
- handler: T.Callable[..., T.Awaitable[T.Any]],
188
+ handler: T.Callable[
189
+ ...,
190
+ T.Awaitable[MessageEventResult | mcp.types.CallToolResult | str | None]
191
+ | T.AsyncGenerator[MessageEventResult | CommandResult | str | None, None],
192
+ ],
189
193
  method_name: str,
190
194
  *args,
191
195
  **kwargs,
@@ -24,6 +24,10 @@ class AstrBotConfig(dict):
24
24
  - 如果传入了 schema,将会通过 schema 解析出 default_config,此时传入的 default_config 会被忽略。
25
25
  """
26
26
 
27
+ config_path: str
28
+ default_config: dict
29
+ schema: dict | None
30
+
27
31
  def __init__(
28
32
  self,
29
33
  config_path: str = ASTRBOT_CONFIG_PATH,
@@ -4,9 +4,18 @@ import os
4
4
 
5
5
  from astrbot.core.utils.astrbot_path import get_astrbot_data_path
6
6
 
7
- VERSION = "4.7.4"
7
+ VERSION = "4.9.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
+ "lark",
17
+ ]
18
+
10
19
  # 默认配置
11
20
  DEFAULT_CONFIG = {
12
21
  "config_version": 2,
@@ -34,7 +43,15 @@ DEFAULT_CONFIG = {
34
43
  "interval": "1.5,3.5",
35
44
  "log_base": 2.6,
36
45
  "words_count_threshold": 150,
46
+ "split_mode": "regex", # regex 或 words
37
47
  "regex": ".*?[。?!~…]+|.+$",
48
+ "split_words": [
49
+ "。",
50
+ "?",
51
+ "!",
52
+ "~",
53
+ "…",
54
+ ], # 当 split_mode 为 words 时使用
38
55
  "content_cleanup_rule": "",
39
56
  },
40
57
  "no_permission_reply": True,
@@ -149,6 +166,7 @@ DEFAULT_CONFIG = {
149
166
  "kb_fusion_top_k": 20, # 知识库检索融合阶段返回结果数量
150
167
  "kb_final_top_k": 5, # 知识库检索最终返回结果数量
151
168
  "kb_agentic_mode": False,
169
+ "disable_builtin_commands": False,
152
170
  }
153
171
 
154
172
 
@@ -185,6 +203,8 @@ CONFIG_METADATA_2 = {
185
203
  "appid": "",
186
204
  "secret": "",
187
205
  "is_sandbox": False,
206
+ "unified_webhook_mode": True,
207
+ "webhook_uuid": "",
188
208
  "callback_server_host": "0.0.0.0",
189
209
  "port": 6196,
190
210
  },
@@ -215,6 +235,8 @@ CONFIG_METADATA_2 = {
215
235
  "token": "",
216
236
  "encoding_aes_key": "",
217
237
  "api_base_url": "https://api.weixin.qq.com/cgi-bin/",
238
+ "unified_webhook_mode": True,
239
+ "webhook_uuid": "",
218
240
  "callback_server_host": "0.0.0.0",
219
241
  "port": 6194,
220
242
  "active_send_mode": False,
@@ -229,6 +251,8 @@ CONFIG_METADATA_2 = {
229
251
  "encoding_aes_key": "",
230
252
  "kf_name": "",
231
253
  "api_base_url": "https://qyapi.weixin.qq.com/cgi-bin/",
254
+ "unified_webhook_mode": True,
255
+ "webhook_uuid": "",
232
256
  "callback_server_host": "0.0.0.0",
233
257
  "port": 6195,
234
258
  },
@@ -241,6 +265,8 @@ CONFIG_METADATA_2 = {
241
265
  "wecom_ai_bot_name": "",
242
266
  "token": "",
243
267
  "encoding_aes_key": "",
268
+ "unified_webhook_mode": True,
269
+ "webhook_uuid": "",
244
270
  "callback_server_host": "0.0.0.0",
245
271
  "port": 6198,
246
272
  },
@@ -252,6 +278,10 @@ CONFIG_METADATA_2 = {
252
278
  "app_id": "",
253
279
  "app_secret": "",
254
280
  "domain": "https://open.feishu.cn",
281
+ "lark_connection_mode": "socket", # webhook, socket
282
+ "webhook_uuid": "",
283
+ "lark_encrypt_key": "",
284
+ "lark_verification_token": "",
255
285
  },
256
286
  "钉钉(DingTalk)": {
257
287
  "id": "dingtalk",
@@ -308,6 +338,8 @@ CONFIG_METADATA_2 = {
308
338
  "app_token": "",
309
339
  "signing_secret": "",
310
340
  "slack_connection_mode": "socket", # webhook, socket
341
+ "unified_webhook_mode": True,
342
+ "webhook_uuid": "",
311
343
  "slack_webhook_host": "0.0.0.0",
312
344
  "slack_webhook_port": 6197,
313
345
  "slack_webhook_path": "/astrbot-slack-webhook/callback",
@@ -343,6 +375,28 @@ CONFIG_METADATA_2 = {
343
375
  # "type": "string",
344
376
  # "options": ["fullscreen", "embedded"],
345
377
  # },
378
+ "lark_connection_mode": {
379
+ "description": "订阅方式",
380
+ "type": "string",
381
+ "options": ["socket", "webhook"],
382
+ "labels": ["长连接模式", "推送至服务器模式"],
383
+ },
384
+ "lark_encrypt_key": {
385
+ "description": "Encrypt Key",
386
+ "type": "string",
387
+ "hint": "用于解密飞书回调数据的加密密钥",
388
+ "condition": {
389
+ "lark_connection_mode": "webhook",
390
+ },
391
+ },
392
+ "lark_verification_token": {
393
+ "description": "Verification Token",
394
+ "type": "string",
395
+ "hint": "用于验证飞书回调请求的令牌",
396
+ "condition": {
397
+ "lark_connection_mode": "webhook",
398
+ },
399
+ },
346
400
  "is_sandbox": {
347
401
  "description": "沙箱模式",
348
402
  "type": "bool",
@@ -387,16 +441,28 @@ CONFIG_METADATA_2 = {
387
441
  "description": "Slack Webhook Host",
388
442
  "type": "string",
389
443
  "hint": "Only valid when Slack connection mode is `webhook`.",
444
+ "condition": {
445
+ "slack_connection_mode": "webhook",
446
+ "unified_webhook_mode": False,
447
+ },
390
448
  },
391
449
  "slack_webhook_port": {
392
450
  "description": "Slack Webhook Port",
393
451
  "type": "int",
394
452
  "hint": "Only valid when Slack connection mode is `webhook`.",
453
+ "condition": {
454
+ "slack_connection_mode": "webhook",
455
+ "unified_webhook_mode": False,
456
+ },
395
457
  },
396
458
  "slack_webhook_path": {
397
459
  "description": "Slack Webhook Path",
398
460
  "type": "string",
399
461
  "hint": "Only valid when Slack connection mode is `webhook`.",
462
+ "condition": {
463
+ "slack_connection_mode": "webhook",
464
+ "unified_webhook_mode": False,
465
+ },
400
466
  },
401
467
  "active_send_mode": {
402
468
  "description": "是否换用主动发送接口",
@@ -587,6 +653,33 @@ CONFIG_METADATA_2 = {
587
653
  "type": "string",
588
654
  "hint": "可选的 Discord 活动名称。留空则不设置活动。",
589
655
  },
656
+ "port": {
657
+ "description": "回调服务器端口",
658
+ "type": "int",
659
+ "hint": "回调服务器端口。留空则不启用回调服务器。",
660
+ "condition": {
661
+ "unified_webhook_mode": False,
662
+ },
663
+ },
664
+ "callback_server_host": {
665
+ "description": "回调服务器主机",
666
+ "type": "string",
667
+ "hint": "回调服务器主机。留空则不启用回调服务器。",
668
+ "condition": {
669
+ "unified_webhook_mode": False,
670
+ },
671
+ },
672
+ "unified_webhook_mode": {
673
+ "description": "统一 Webhook 模式",
674
+ "type": "bool",
675
+ "hint": "启用后,将使用 AstrBot 统一 Webhook 入口,无需单独开启端口。回调地址为 /api/platform/webhook/{webhook_uuid}。",
676
+ },
677
+ "webhook_uuid": {
678
+ "invisible": True,
679
+ "description": "Webhook UUID",
680
+ "type": "string",
681
+ "hint": "统一 Webhook 模式下的唯一标识符,创建平台时自动生成。",
682
+ },
590
683
  },
591
684
  },
592
685
  "platform_settings": {
@@ -2604,6 +2697,11 @@ CONFIG_METADATA_3 = {
2604
2697
  "description": "只 @ 机器人是否触发等待",
2605
2698
  "type": "bool",
2606
2699
  },
2700
+ "disable_builtin_commands": {
2701
+ "description": "禁用自带指令",
2702
+ "type": "bool",
2703
+ "hint": "禁用所有 AstrBot 的自带指令,如 help, provider, model 等。",
2704
+ },
2607
2705
  },
2608
2706
  },
2609
2707
  "whitelist": {
@@ -2818,9 +2916,26 @@ CONFIG_METADATA_3 = {
2818
2916
  "description": "分段回复字数阈值",
2819
2917
  "type": "int",
2820
2918
  },
2919
+ "platform_settings.segmented_reply.split_mode": {
2920
+ "description": "分段模式",
2921
+ "type": "string",
2922
+ "options": ["regex", "words"],
2923
+ "labels": ["正则表达式", "分段词列表"],
2924
+ },
2821
2925
  "platform_settings.segmented_reply.regex": {
2822
2926
  "description": "分段正则表达式",
2823
2927
  "type": "string",
2928
+ "condition": {
2929
+ "platform_settings.segmented_reply.split_mode": "regex",
2930
+ },
2931
+ },
2932
+ "platform_settings.segmented_reply.split_words": {
2933
+ "description": "分段词列表",
2934
+ "type": "list",
2935
+ "hint": "检测到列表中的任意词时进行分段,如:。、?、!等",
2936
+ "condition": {
2937
+ "platform_settings.segmented_reply.split_mode": "words",
2938
+ },
2824
2939
  },
2825
2940
  "platform_settings.segmented_reply.content_cleanup_rule": {
2826
2941
  "description": "内容过滤正则表达式",
@@ -197,7 +197,7 @@ class AstrBotCoreLifecycle:
197
197
  # 把插件中注册的所有协程函数注册到事件总线中并执行
198
198
  extra_tasks = []
199
199
  for task in self.star_context._register_tasks:
200
- extra_tasks.append(asyncio.create_task(task, name=task.__name__))
200
+ extra_tasks.append(asyncio.create_task(task, name=task.__name__)) # type: ignore
201
201
 
202
202
  tasks_ = [event_bus_task, *extra_tasks]
203
203
  for task in tasks_:
@@ -5,8 +5,7 @@ from contextlib import asynccontextmanager
5
5
  from dataclasses import dataclass
6
6
 
7
7
  from deprecated import deprecated
8
- from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
9
- from sqlalchemy.orm import sessionmaker
8
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
10
9
 
11
10
  from astrbot.core.db.po import (
12
11
  Attachment,
@@ -32,7 +31,7 @@ class BaseDatabase(abc.ABC):
32
31
  echo=False,
33
32
  future=True,
34
33
  )
35
- self.AsyncSessionLocal = sessionmaker(
34
+ self.AsyncSessionLocal = async_sessionmaker(
36
35
  self.engine,
37
36
  class_=AsyncSession,
38
37
  expire_on_commit=False,
@@ -173,7 +172,7 @@ class BaseDatabase(abc.ABC):
173
172
  content: dict,
174
173
  sender_id: str | None = None,
175
174
  sender_name: str | None = None,
176
- ) -> None:
175
+ ) -> PlatformMessageHistory:
177
176
  """Insert a new platform message history record."""
178
177
  ...
179
178
 
@@ -198,6 +197,14 @@ class BaseDatabase(abc.ABC):
198
197
  """Get platform message history for a specific user."""
199
198
  ...
200
199
 
200
+ @abc.abstractmethod
201
+ async def get_platform_message_history_by_id(
202
+ self,
203
+ message_id: int,
204
+ ) -> PlatformMessageHistory | None:
205
+ """Get a platform message history record by its ID."""
206
+ ...
207
+
201
208
  @abc.abstractmethod
202
209
  async def insert_attachment(
203
210
  self,
@@ -213,6 +220,27 @@ class BaseDatabase(abc.ABC):
213
220
  """Get an attachment by its ID."""
214
221
  ...
215
222
 
223
+ @abc.abstractmethod
224
+ async def get_attachments(self, attachment_ids: list[str]) -> list[Attachment]:
225
+ """Get multiple attachments by their IDs."""
226
+ ...
227
+
228
+ @abc.abstractmethod
229
+ async def delete_attachment(self, attachment_id: str) -> bool:
230
+ """Delete an attachment by its ID.
231
+
232
+ Returns True if the attachment was deleted, False if it was not found.
233
+ """
234
+ ...
235
+
236
+ @abc.abstractmethod
237
+ async def delete_attachments(self, attachment_ids: list[str]) -> int:
238
+ """Delete multiple attachments by their IDs.
239
+
240
+ Returns the number of attachments deleted.
241
+ """
242
+ ...
243
+
216
244
  @abc.abstractmethod
217
245
  async def insert_persona(
218
246
  self,
@@ -70,6 +70,7 @@ async def migration_conversation_table(
70
70
  logger.info(
71
71
  f"未找到该条旧会话对应的具体数据: {conversation}, 跳过。",
72
72
  )
73
+ continue
73
74
  if ":" not in conv.user_id:
74
75
  continue
75
76
  session = MessageSesion.from_str(session_str=conv.user_id)
@@ -207,6 +208,7 @@ async def migration_webchat_data(
207
208
  logger.info(
208
209
  f"未找到该条旧会话对应的具体数据: {conversation}, 跳过。",
209
210
  )
211
+ continue
210
212
  if ":" in conv.user_id:
211
213
  continue
212
214
  platform_id = "webchat"
@@ -127,7 +127,7 @@ class SQLiteDatabase:
127
127
  conn.text_factory = str
128
128
  return conn
129
129
 
130
- def _exec_sql(self, sql: str, params: tuple = None):
130
+ def _exec_sql(self, sql: str, params: tuple | None = None):
131
131
  conn = self.conn
132
132
  try:
133
133
  c = self.conn.cursor()
@@ -224,9 +224,11 @@ class SQLiteDatabase:
224
224
 
225
225
  c.close()
226
226
 
227
- return Stats(platform, [], [])
227
+ return Stats(platform)
228
228
 
229
- def get_conversation_by_user_id(self, user_id: str, cid: str) -> Conversation:
229
+ def get_conversation_by_user_id(
230
+ self, user_id: str, cid: str
231
+ ) -> Conversation | None:
230
232
  try:
231
233
  c = self.conn.cursor()
232
234
  except sqlite3.ProgrammingError:
@@ -258,7 +260,7 @@ class SQLiteDatabase:
258
260
  (user_id, cid, history, updated_at, created_at),
259
261
  )
260
262
 
261
- def get_conversations(self, user_id: str) -> tuple:
263
+ def get_conversations(self, user_id: str) -> list[Conversation]:
262
264
  try:
263
265
  c = self.conn.cursor()
264
266
  except sqlite3.ProgrammingError:
astrbot/core/db/po.py CHANGED
@@ -12,7 +12,7 @@ class PlatformStat(SQLModel, table=True):
12
12
  Note: In astrbot v4, we moved `platform` table to here.
13
13
  """
14
14
 
15
- __tablename__ = "platform_stats" # type: ignore
15
+ __tablename__: str = "platform_stats"
16
16
 
17
17
  id: int = Field(primary_key=True, sa_column_kwargs={"autoincrement": True})
18
18
  timestamp: datetime = Field(nullable=False)
@@ -31,9 +31,10 @@ class PlatformStat(SQLModel, table=True):
31
31
 
32
32
 
33
33
  class ConversationV2(SQLModel, table=True):
34
- __tablename__ = "conversations" # type: ignore
34
+ __tablename__: str = "conversations"
35
35
 
36
- inner_conversation_id: int = Field(
36
+ inner_conversation_id: int | None = Field(
37
+ default=None,
37
38
  primary_key=True,
38
39
  sa_column_kwargs={"autoincrement": True},
39
40
  )
@@ -68,7 +69,7 @@ class Persona(SQLModel, table=True):
68
69
  It can be used to customize the behavior of LLMs.
69
70
  """
70
71
 
71
- __tablename__ = "personas" # type: ignore
72
+ __tablename__: str = "personas"
72
73
 
73
74
  id: int | None = Field(
74
75
  primary_key=True,
@@ -98,7 +99,7 @@ class Persona(SQLModel, table=True):
98
99
  class Preference(SQLModel, table=True):
99
100
  """This class represents preferences for bots."""
100
101
 
101
- __tablename__ = "preferences" # type: ignore
102
+ __tablename__: str = "preferences"
102
103
 
103
104
  id: int | None = Field(
104
105
  default=None,
@@ -134,7 +135,7 @@ class PlatformMessageHistory(SQLModel, table=True):
134
135
  or platform-specific messages.
135
136
  """
136
137
 
137
- __tablename__ = "platform_message_history" # type: ignore
138
+ __tablename__: str = "platform_message_history"
138
139
 
139
140
  id: int | None = Field(
140
141
  primary_key=True,
@@ -162,7 +163,7 @@ class PlatformSession(SQLModel, table=True):
162
163
  Each session can have multiple conversations (对话) associated with it.
163
164
  """
164
165
 
165
- __tablename__ = "platform_sessions" # type: ignore
166
+ __tablename__: str = "platform_sessions"
166
167
 
167
168
  inner_id: int | None = Field(
168
169
  primary_key=True,
@@ -203,7 +204,7 @@ class Attachment(SQLModel, table=True):
203
204
  Attachments can be images, files, or other media types.
204
205
  """
205
206
 
206
- __tablename__ = "attachments" # type: ignore
207
+ __tablename__: str = "attachments"
207
208
 
208
209
  inner_attachment_id: int | None = Field(
209
210
  primary_key=True,
@@ -261,17 +262,17 @@ class Personality(TypedDict):
261
262
  在 v4.0.0 版本及之后,推荐使用上面的 Persona 类。并且, mood_imitation_dialogs 字段已被废弃。
262
263
  """
263
264
 
264
- prompt: str = ""
265
- name: str = ""
266
- begin_dialogs: list[str] = []
267
- mood_imitation_dialogs: list[str] = []
265
+ prompt: str
266
+ name: str
267
+ begin_dialogs: list[str]
268
+ mood_imitation_dialogs: list[str]
268
269
  """情感模拟对话预设。在 v4.0.0 版本及之后,已被废弃。"""
269
- tools: list[str] | None = None
270
+ tools: list[str] | None
270
271
  """工具列表。None 表示使用所有工具,空列表表示不使用任何工具"""
271
272
 
272
273
  # cache
273
- _begin_dialogs_processed: list[dict] = []
274
- _mood_imitation_dialogs_processed: str = ""
274
+ _begin_dialogs_processed: list[dict]
275
+ _mood_imitation_dialogs_processed: str
275
276
 
276
277
 
277
278
  # ====
astrbot/core/db/sqlite.py CHANGED
@@ -3,6 +3,7 @@ import threading
3
3
  import typing as T
4
4
  from datetime import datetime, timedelta, timezone
5
5
 
6
+ from sqlalchemy import CursorResult
6
7
  from sqlalchemy.ext.asyncio import AsyncSession
7
8
  from sqlmodel import col, delete, desc, func, or_, select, text, update
8
9
 
@@ -105,8 +106,8 @@ class SQLiteDatabase(BaseDatabase):
105
106
  text("""
106
107
  SELECT * FROM platform_stats
107
108
  WHERE timestamp >= :start_time
108
- ORDER BY timestamp DESC
109
109
  GROUP BY platform_id
110
+ ORDER BY timestamp DESC
110
111
  """),
111
112
  {"start_time": start_time},
112
113
  )
@@ -449,6 +450,18 @@ class SQLiteDatabase(BaseDatabase):
449
450
  result = await session.execute(query.offset(offset).limit(page_size))
450
451
  return result.scalars().all()
451
452
 
453
+ async def get_platform_message_history_by_id(
454
+ self, message_id: int
455
+ ) -> PlatformMessageHistory | None:
456
+ """Get a platform message history record by its ID."""
457
+ async with self.get_db() as session:
458
+ session: AsyncSession
459
+ query = select(PlatformMessageHistory).where(
460
+ PlatformMessageHistory.id == message_id
461
+ )
462
+ result = await session.execute(query)
463
+ return result.scalar_one_or_none()
464
+
452
465
  async def insert_attachment(self, path, type, mime_type):
453
466
  """Insert a new attachment record."""
454
467
  async with self.get_db() as session:
@@ -470,6 +483,48 @@ class SQLiteDatabase(BaseDatabase):
470
483
  result = await session.execute(query)
471
484
  return result.scalar_one_or_none()
472
485
 
486
+ async def get_attachments(self, attachment_ids: list[str]) -> list:
487
+ """Get multiple attachments by their IDs."""
488
+ if not attachment_ids:
489
+ return []
490
+ async with self.get_db() as session:
491
+ session: AsyncSession
492
+ query = select(Attachment).where(
493
+ col(Attachment.attachment_id).in_(attachment_ids)
494
+ )
495
+ result = await session.execute(query)
496
+ return list(result.scalars().all())
497
+
498
+ async def delete_attachment(self, attachment_id: str) -> bool:
499
+ """Delete an attachment by its ID.
500
+
501
+ Returns True if the attachment was deleted, False if it was not found.
502
+ """
503
+ async with self.get_db() as session:
504
+ session: AsyncSession
505
+ async with session.begin():
506
+ query = delete(Attachment).where(
507
+ col(Attachment.attachment_id) == attachment_id
508
+ )
509
+ result = T.cast(CursorResult, await session.execute(query))
510
+ return result.rowcount > 0
511
+
512
+ async def delete_attachments(self, attachment_ids: list[str]) -> int:
513
+ """Delete multiple attachments by their IDs.
514
+
515
+ Returns the number of attachments deleted.
516
+ """
517
+ if not attachment_ids:
518
+ return 0
519
+ async with self.get_db() as session:
520
+ session: AsyncSession
521
+ async with session.begin():
522
+ query = delete(Attachment).where(
523
+ col(Attachment.attachment_id).in_(attachment_ids)
524
+ )
525
+ result = T.cast(CursorResult, await session.execute(query))
526
+ return result.rowcount
527
+
473
528
  async def insert_persona(
474
529
  self,
475
530
  persona_id,
@@ -90,4 +90,6 @@ class EmbeddingStorage:
90
90
  path (str): 保存索引的路径
91
91
 
92
92
  """
93
+ if self.index is None:
94
+ return
93
95
  faiss.write_index(self.index, self.path)
astrbot/core/event_bus.py CHANGED
@@ -27,7 +27,7 @@ class EventBus:
27
27
  self,
28
28
  event_queue: Queue,
29
29
  pipeline_scheduler_mapping: dict[str, PipelineScheduler],
30
- astrbot_config_mgr: AstrBotConfigManager = None,
30
+ astrbot_config_mgr: AstrBotConfigManager,
31
31
  ):
32
32
  self.event_queue = event_queue # 事件队列
33
33
  # abconf uuid -> scheduler
@@ -40,6 +40,11 @@ class EventBus:
40
40
  conf_info = self.astrbot_config_mgr.get_conf_info(event.unified_msg_origin)
41
41
  self._print_event(event, conf_info["name"])
42
42
  scheduler = self.pipeline_scheduler_mapping.get(conf_info["id"])
43
+ if not scheduler:
44
+ logger.error(
45
+ f"PipelineScheduler not found for id: {conf_info['id']}, event ignored."
46
+ )
47
+ continue
43
48
  asyncio.create_task(scheduler.execute(event))
44
49
 
45
50
  def _print_event(self, event: AstrMessageEvent, conf_name: str):
@@ -166,7 +166,11 @@ class RetrievalManager:
166
166
  # 5. Rerank
167
167
  first_rerank = None
168
168
  for kb_id in kb_ids:
169
- vec_db: FaissVecDB = kb_options[kb_id]["vec_db"]
169
+ vec_db = kb_options[kb_id]["vec_db"]
170
+ if not isinstance(vec_db, FaissVecDB):
171
+ logger.warning(f"vec_db for kb_id {kb_id} is not FaissVecDB")
172
+ continue
173
+
170
174
  rerank_pi = kb_options[kb_id]["rerank_provider_id"]
171
175
  if (
172
176
  vec_db