AstrBot 4.8.0__py3-none-any.whl → 4.9.1__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 (106) 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_tool_exec.py +5 -1
  5. astrbot/core/config/astrbot_config.py +4 -0
  6. astrbot/core/config/default.py +72 -1
  7. astrbot/core/config/i18n_utils.py +1 -0
  8. astrbot/core/core_lifecycle.py +1 -1
  9. astrbot/core/db/__init__.py +2 -3
  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 +4 -3
  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/star_request.py +1 -2
  23. astrbot/core/pipeline/process_stage/stage.py +1 -1
  24. astrbot/core/pipeline/respond/stage.py +8 -2
  25. astrbot/core/pipeline/result_decorate/stage.py +89 -22
  26. astrbot/core/pipeline/scheduler.py +5 -1
  27. astrbot/core/pipeline/waking_check/stage.py +10 -0
  28. astrbot/core/platform/astr_message_event.py +5 -3
  29. astrbot/core/platform/astrbot_message.py +2 -2
  30. astrbot/core/platform/manager.py +4 -0
  31. astrbot/core/platform/platform.py +11 -3
  32. astrbot/core/platform/platform_metadata.py +1 -1
  33. astrbot/core/platform/register.py +1 -0
  34. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +8 -6
  35. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +9 -5
  36. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +24 -16
  37. astrbot/core/platform/sources/dingtalk/dingtalk_event.py +5 -2
  38. astrbot/core/platform/sources/discord/client.py +16 -4
  39. astrbot/core/platform/sources/discord/components.py +2 -2
  40. astrbot/core/platform/sources/discord/discord_platform_adapter.py +52 -24
  41. astrbot/core/platform/sources/discord/discord_platform_event.py +29 -8
  42. astrbot/core/platform/sources/lark/lark_adapter.py +183 -20
  43. astrbot/core/platform/sources/lark/lark_event.py +39 -4
  44. astrbot/core/platform/sources/lark/server.py +206 -0
  45. astrbot/core/platform/sources/misskey/misskey_adapter.py +2 -3
  46. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +62 -18
  47. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +13 -7
  48. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +5 -3
  49. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +2 -1
  50. astrbot/core/platform/sources/slack/client.py +9 -2
  51. astrbot/core/platform/sources/slack/slack_adapter.py +15 -9
  52. astrbot/core/platform/sources/slack/slack_event.py +8 -7
  53. astrbot/core/platform/sources/telegram/tg_adapter.py +1 -1
  54. astrbot/core/platform/sources/telegram/tg_event.py +23 -27
  55. astrbot/core/platform/sources/webchat/webchat_adapter.py +2 -2
  56. astrbot/core/platform/sources/webchat/webchat_event.py +2 -2
  57. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +26 -9
  58. astrbot/core/platform/sources/wecom/wecom_adapter.py +25 -28
  59. astrbot/core/platform/sources/wecom/wecom_event.py +2 -2
  60. astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +3 -3
  61. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +30 -25
  62. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +10 -7
  63. astrbot/core/provider/func_tool_manager.py +3 -3
  64. astrbot/core/provider/manager.py +130 -74
  65. astrbot/core/provider/provider.py +12 -1
  66. astrbot/core/provider/sources/azure_tts_source.py +31 -9
  67. astrbot/core/provider/sources/bailian_rerank_source.py +4 -0
  68. astrbot/core/provider/sources/dashscope_tts.py +3 -2
  69. astrbot/core/provider/sources/edge_tts_source.py +1 -1
  70. astrbot/core/provider/sources/fishaudio_tts_api_source.py +5 -4
  71. astrbot/core/provider/sources/gemini_embedding_source.py +15 -5
  72. astrbot/core/provider/sources/gemini_source.py +12 -10
  73. astrbot/core/provider/sources/minimax_tts_api_source.py +4 -2
  74. astrbot/core/provider/sources/openai_embedding_source.py +2 -2
  75. astrbot/core/provider/sources/openai_source.py +4 -0
  76. astrbot/core/provider/sources/sensevoice_selfhosted_source.py +5 -2
  77. astrbot/core/provider/sources/vllm_rerank_source.py +1 -0
  78. astrbot/core/provider/sources/whisper_api_source.py +1 -1
  79. astrbot/core/provider/sources/whisper_selfhosted_source.py +6 -2
  80. astrbot/core/provider/sources/xinference_rerank_source.py +10 -2
  81. astrbot/core/star/context.py +2 -2
  82. astrbot/core/star/register/star_handler.py +22 -5
  83. astrbot/core/star/star_handler.py +85 -4
  84. astrbot/core/updator.py +3 -3
  85. astrbot/core/utils/io.py +1 -1
  86. astrbot/core/utils/session_waiter.py +17 -10
  87. astrbot/core/utils/shared_preferences.py +32 -0
  88. astrbot/core/utils/t2i/__init__.py +2 -2
  89. astrbot/core/utils/t2i/local_strategy.py +25 -31
  90. astrbot/core/utils/tencent_record_helper.py +1 -1
  91. astrbot/core/utils/version_comparator.py +6 -3
  92. astrbot/core/utils/webhook_utils.py +19 -0
  93. astrbot/dashboard/routes/chat.py +14 -9
  94. astrbot/dashboard/routes/config.py +10 -20
  95. astrbot/dashboard/routes/conversation.py +91 -1
  96. astrbot/dashboard/routes/knowledge_base.py +253 -78
  97. astrbot/dashboard/routes/log.py +13 -8
  98. astrbot/dashboard/routes/platform.py +1 -1
  99. astrbot/dashboard/routes/plugin.py +113 -52
  100. astrbot/dashboard/routes/route.py +2 -0
  101. astrbot/dashboard/server.py +6 -3
  102. {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/METADATA +9 -1
  103. {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/RECORD +106 -105
  104. {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/WHEEL +0 -0
  105. {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/entry_points.txt +0 -0
  106. {astrbot-4.8.0.dist-info → astrbot-4.9.1.dist-info}/licenses/LICENSE +0 -0
astrbot/cli/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "4.8.0"
1
+ __version__ = "4.9.1"
@@ -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
@@ -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,7 +4,7 @@ import os
4
4
 
5
5
  from astrbot.core.utils.astrbot_path import get_astrbot_data_path
6
6
 
7
- VERSION = "4.8.0"
7
+ VERSION = "4.9.1"
8
8
  DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
9
9
 
10
10
  WEBHOOK_SUPPORTED_PLATFORMS = [
@@ -13,6 +13,7 @@ WEBHOOK_SUPPORTED_PLATFORMS = [
13
13
  "wecom",
14
14
  "wecom_ai_bot",
15
15
  "slack",
16
+ "lark",
16
17
  ]
17
18
 
18
19
  # 默认配置
@@ -42,7 +43,15 @@ DEFAULT_CONFIG = {
42
43
  "interval": "1.5,3.5",
43
44
  "log_base": 2.6,
44
45
  "words_count_threshold": 150,
46
+ "split_mode": "regex", # regex 或 words
45
47
  "regex": ".*?[。?!~…]+|.+$",
48
+ "split_words": [
49
+ "。",
50
+ "?",
51
+ "!",
52
+ "~",
53
+ "…",
54
+ ], # 当 split_mode 为 words 时使用
46
55
  "content_cleanup_rule": "",
47
56
  },
48
57
  "no_permission_reply": True,
@@ -99,6 +108,7 @@ DEFAULT_CONFIG = {
99
108
  "provider_id": "",
100
109
  "dual_output": False,
101
110
  "use_file_service": False,
111
+ "trigger_probability": 1.0,
102
112
  },
103
113
  "provider_ltm_settings": {
104
114
  "group_icl_enable": False,
@@ -157,6 +167,7 @@ DEFAULT_CONFIG = {
157
167
  "kb_fusion_top_k": 20, # 知识库检索融合阶段返回结果数量
158
168
  "kb_final_top_k": 5, # 知识库检索最终返回结果数量
159
169
  "kb_agentic_mode": False,
170
+ "disable_builtin_commands": False,
160
171
  }
161
172
 
162
173
 
@@ -268,6 +279,10 @@ CONFIG_METADATA_2 = {
268
279
  "app_id": "",
269
280
  "app_secret": "",
270
281
  "domain": "https://open.feishu.cn",
282
+ "lark_connection_mode": "socket", # webhook, socket
283
+ "webhook_uuid": "",
284
+ "lark_encrypt_key": "",
285
+ "lark_verification_token": "",
271
286
  },
272
287
  "钉钉(DingTalk)": {
273
288
  "id": "dingtalk",
@@ -361,6 +376,28 @@ CONFIG_METADATA_2 = {
361
376
  # "type": "string",
362
377
  # "options": ["fullscreen", "embedded"],
363
378
  # },
379
+ "lark_connection_mode": {
380
+ "description": "订阅方式",
381
+ "type": "string",
382
+ "options": ["socket", "webhook"],
383
+ "labels": ["长连接模式", "推送至服务器模式"],
384
+ },
385
+ "lark_encrypt_key": {
386
+ "description": "Encrypt Key",
387
+ "type": "string",
388
+ "hint": "用于解密飞书回调数据的加密密钥",
389
+ "condition": {
390
+ "lark_connection_mode": "webhook",
391
+ },
392
+ },
393
+ "lark_verification_token": {
394
+ "description": "Verification Token",
395
+ "type": "string",
396
+ "hint": "用于验证飞书回调请求的令牌",
397
+ "condition": {
398
+ "lark_connection_mode": "webhook",
399
+ },
400
+ },
364
401
  "is_sandbox": {
365
402
  "description": "沙箱模式",
366
403
  "type": "bool",
@@ -2173,6 +2210,9 @@ CONFIG_METADATA_2 = {
2173
2210
  "use_file_service": {
2174
2211
  "type": "bool",
2175
2212
  },
2213
+ "trigger_probability": {
2214
+ "type": "float",
2215
+ },
2176
2216
  },
2177
2217
  },
2178
2218
  "provider_ltm_settings": {
@@ -2383,6 +2423,14 @@ CONFIG_METADATA_3 = {
2383
2423
  "provider_tts_settings.enable": True,
2384
2424
  },
2385
2425
  },
2426
+ "provider_tts_settings.trigger_probability": {
2427
+ "description": "TTS 触发概率",
2428
+ "type": "float",
2429
+ "slider": {"min": 0, "max": 1, "step": 0.05},
2430
+ "condition": {
2431
+ "provider_tts_settings.enable": True,
2432
+ },
2433
+ },
2386
2434
  "provider_settings.image_caption_prompt": {
2387
2435
  "description": "图片转述提示词",
2388
2436
  "type": "text",
@@ -2661,6 +2709,11 @@ CONFIG_METADATA_3 = {
2661
2709
  "description": "只 @ 机器人是否触发等待",
2662
2710
  "type": "bool",
2663
2711
  },
2712
+ "disable_builtin_commands": {
2713
+ "description": "禁用自带指令",
2714
+ "type": "bool",
2715
+ "hint": "禁用所有 AstrBot 的自带指令,如 help, provider, model 等。",
2716
+ },
2664
2717
  },
2665
2718
  },
2666
2719
  "whitelist": {
@@ -2875,9 +2928,26 @@ CONFIG_METADATA_3 = {
2875
2928
  "description": "分段回复字数阈值",
2876
2929
  "type": "int",
2877
2930
  },
2931
+ "platform_settings.segmented_reply.split_mode": {
2932
+ "description": "分段模式",
2933
+ "type": "string",
2934
+ "options": ["regex", "words"],
2935
+ "labels": ["正则表达式", "分段词列表"],
2936
+ },
2878
2937
  "platform_settings.segmented_reply.regex": {
2879
2938
  "description": "分段正则表达式",
2880
2939
  "type": "string",
2940
+ "condition": {
2941
+ "platform_settings.segmented_reply.split_mode": "regex",
2942
+ },
2943
+ },
2944
+ "platform_settings.segmented_reply.split_words": {
2945
+ "description": "分段词列表",
2946
+ "type": "list",
2947
+ "hint": "检测到列表中的任意词时进行分段,如:。、?、!等",
2948
+ "condition": {
2949
+ "platform_settings.segmented_reply.split_mode": "words",
2950
+ },
2881
2951
  },
2882
2952
  "platform_settings.segmented_reply.content_cleanup_rule": {
2883
2953
  "description": "内容过滤正则表达式",
@@ -2928,6 +2998,7 @@ CONFIG_METADATA_3 = {
2928
2998
  "description": "回复概率",
2929
2999
  "type": "float",
2930
3000
  "hint": "0.0-1.0 之间的数值",
3001
+ "slider": {"min": 0, "max": 1, "step": 0.05},
2931
3002
  "condition": {
2932
3003
  "provider_ltm_settings.active_reply.enable": True,
2933
3004
  },
@@ -79,6 +79,7 @@ class ConfigMetadataI18n:
79
79
  "_special",
80
80
  "invisible",
81
81
  "options",
82
+ "slider",
82
83
  ]:
83
84
  if attr in field_data:
84
85
  field_result[attr] = field_data[attr]
@@ -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,
@@ -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
 
@@ -489,7 +490,7 @@ class SQLiteDatabase(BaseDatabase):
489
490
  async with self.get_db() as session:
490
491
  session: AsyncSession
491
492
  query = select(Attachment).where(
492
- Attachment.attachment_id.in_(attachment_ids)
493
+ col(Attachment.attachment_id).in_(attachment_ids)
493
494
  )
494
495
  result = await session.execute(query)
495
496
  return list(result.scalars().all())
@@ -505,7 +506,7 @@ class SQLiteDatabase(BaseDatabase):
505
506
  query = delete(Attachment).where(
506
507
  col(Attachment.attachment_id) == attachment_id
507
508
  )
508
- result = await session.execute(query)
509
+ result = T.cast(CursorResult, await session.execute(query))
509
510
  return result.rowcount > 0
510
511
 
511
512
  async def delete_attachments(self, attachment_ids: list[str]) -> int:
@@ -521,7 +522,7 @@ class SQLiteDatabase(BaseDatabase):
521
522
  query = delete(Attachment).where(
522
523
  col(Attachment.attachment_id).in_(attachment_ids)
523
524
  )
524
- result = await session.execute(query)
525
+ result = T.cast(CursorResult, await session.execute(query))
525
526
  return result.rowcount
526
527
 
527
528
  async def insert_persona(
@@ -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
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
  )
@@ -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 [
@@ -156,7 +158,11 @@ class RespondStage(Stage):
156
158
  result = event.get_result()
157
159
  if result is None:
158
160
  return
161
+ if event.get_extra("_streaming_finished", False):
162
+ # prevent some plugin make result content type to LLM_RESULT after streaming finished, lead to send again
163
+ return
159
164
  if result.result_content_type == ResultContentType.STREAMING_FINISH:
165
+ event.set_extra("_streaming_finished", True)
160
166
  return
161
167
 
162
168
  logger.info(
@@ -185,7 +191,7 @@ class RespondStage(Stage):
185
191
  if isinstance(component, Comp.File) and component.file:
186
192
  # 支持 File 消息段的路径映射。
187
193
  component.file = path_Mapping(mappings, component.file)
188
- event.get_result().chain[idx] = component
194
+ result.chain[idx] = component
189
195
 
190
196
  # 检查消息链是否为空
191
197
  try: