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
@@ -48,6 +48,7 @@ class KnowledgeBaseRoute(Route):
48
48
  # 文档管理
49
49
  "/kb/document/list": ("GET", self.list_documents),
50
50
  "/kb/document/upload": ("POST", self.upload_document),
51
+ "/kb/document/import": ("POST", self.import_documents),
51
52
  "/kb/document/upload/url": ("POST", self.upload_document_from_url),
52
53
  "/kb/document/upload/progress": ("GET", self.get_upload_progress),
53
54
  "/kb/document/get": ("GET", self.get_document),
@@ -66,6 +67,65 @@ class KnowledgeBaseRoute(Route):
66
67
  def _get_kb_manager(self):
67
68
  return self.core_lifecycle.kb_manager
68
69
 
70
+ def _init_task(self, task_id: str, status: str = "pending") -> None:
71
+ self.upload_tasks[task_id] = {
72
+ "status": status,
73
+ "result": None,
74
+ "error": None,
75
+ }
76
+
77
+ def _set_task_result(
78
+ self, task_id: str, status: str, result: any = None, error: str | None = None
79
+ ) -> None:
80
+ self.upload_tasks[task_id] = {
81
+ "status": status,
82
+ "result": result,
83
+ "error": error,
84
+ }
85
+ if task_id in self.upload_progress:
86
+ self.upload_progress[task_id]["status"] = status
87
+
88
+ def _update_progress(
89
+ self,
90
+ task_id: str,
91
+ *,
92
+ status: str | None = None,
93
+ file_index: int | None = None,
94
+ file_name: str | None = None,
95
+ stage: str | None = None,
96
+ current: int | None = None,
97
+ total: int | None = None,
98
+ ) -> None:
99
+ if task_id not in self.upload_progress:
100
+ return
101
+ p = self.upload_progress[task_id]
102
+ if status is not None:
103
+ p["status"] = status
104
+ if file_index is not None:
105
+ p["file_index"] = file_index
106
+ if file_name is not None:
107
+ p["file_name"] = file_name
108
+ if stage is not None:
109
+ p["stage"] = stage
110
+ if current is not None:
111
+ p["current"] = current
112
+ if total is not None:
113
+ p["total"] = total
114
+
115
+ def _make_progress_callback(self, task_id: str, file_idx: int, file_name: str):
116
+ async def _callback(stage: str, current: int, total: int):
117
+ self._update_progress(
118
+ task_id,
119
+ status="processing",
120
+ file_index=file_idx,
121
+ file_name=file_name,
122
+ stage=stage,
123
+ current=current,
124
+ total=total,
125
+ )
126
+
127
+ return _callback
128
+
69
129
  async def _background_upload_task(
70
130
  self,
71
131
  task_id: str,
@@ -80,11 +140,7 @@ class KnowledgeBaseRoute(Route):
80
140
  """后台上传任务"""
81
141
  try:
82
142
  # 初始化任务状态
83
- self.upload_tasks[task_id] = {
84
- "status": "processing",
85
- "result": None,
86
- "error": None,
87
- }
143
+ self._init_task(task_id, status="processing")
88
144
  self.upload_progress[task_id] = {
89
145
  "status": "processing",
90
146
  "file_index": 0,
@@ -100,30 +156,20 @@ class KnowledgeBaseRoute(Route):
100
156
  for file_idx, file_info in enumerate(files_to_upload):
101
157
  try:
102
158
  # 更新整体进度
103
- self.upload_progress[task_id].update(
104
- {
105
- "status": "processing",
106
- "file_index": file_idx,
107
- "file_name": file_info["file_name"],
108
- "stage": "parsing",
109
- "current": 0,
110
- "total": 100,
111
- },
159
+ self._update_progress(
160
+ task_id,
161
+ status="processing",
162
+ file_index=file_idx,
163
+ file_name=file_info["file_name"],
164
+ stage="parsing",
165
+ current=0,
166
+ total=100,
112
167
  )
113
168
 
114
169
  # 创建进度回调函数
115
- async def progress_callback(stage, current, total):
116
- if task_id in self.upload_progress:
117
- self.upload_progress[task_id].update(
118
- {
119
- "status": "processing",
120
- "file_index": file_idx,
121
- "file_name": file_info["file_name"],
122
- "stage": stage,
123
- "current": current,
124
- "total": total,
125
- },
126
- )
170
+ progress_callback = self._make_progress_callback(
171
+ task_id, file_idx, file_info["file_name"]
172
+ )
127
173
 
128
174
  doc = await kb_helper.upload_document(
129
175
  file_name=file_info["file_name"],
@@ -154,23 +200,99 @@ class KnowledgeBaseRoute(Route):
154
200
  "failed_count": len(failed_docs),
155
201
  }
156
202
 
157
- self.upload_tasks[task_id] = {
158
- "status": "completed",
159
- "result": result,
160
- "error": None,
161
- }
162
- self.upload_progress[task_id]["status"] = "completed"
203
+ self._set_task_result(task_id, "completed", result=result)
163
204
 
164
205
  except Exception as e:
165
206
  logger.error(f"后台上传任务 {task_id} 失败: {e}")
166
207
  logger.error(traceback.format_exc())
167
- self.upload_tasks[task_id] = {
168
- "status": "failed",
169
- "result": None,
170
- "error": str(e),
208
+ self._set_task_result(task_id, "failed", error=str(e))
209
+
210
+ async def _background_import_task(
211
+ self,
212
+ task_id: str,
213
+ kb_helper,
214
+ documents: list,
215
+ batch_size: int,
216
+ tasks_limit: int,
217
+ max_retries: int,
218
+ ):
219
+ """后台导入预切片文档任务"""
220
+ try:
221
+ # 初始化任务状态
222
+ self._init_task(task_id, status="processing")
223
+ self.upload_progress[task_id] = {
224
+ "status": "processing",
225
+ "file_index": 0,
226
+ "file_total": len(documents),
227
+ "stage": "waiting",
228
+ "current": 0,
229
+ "total": 100,
230
+ }
231
+
232
+ uploaded_docs = []
233
+ failed_docs = []
234
+
235
+ for file_idx, doc_info in enumerate(documents):
236
+ file_name = doc_info.get("file_name", f"imported_doc_{file_idx}")
237
+ chunks = doc_info.get("chunks", [])
238
+
239
+ try:
240
+ # 更新整体进度
241
+ self._update_progress(
242
+ task_id,
243
+ status="processing",
244
+ file_index=file_idx,
245
+ file_name=file_name,
246
+ stage="importing",
247
+ current=0,
248
+ total=100,
249
+ )
250
+
251
+ # 创建进度回调函数
252
+ progress_callback = self._make_progress_callback(
253
+ task_id, file_idx, file_name
254
+ )
255
+
256
+ # 调用 upload_document,传入 pre_chunked_text
257
+ doc = await kb_helper.upload_document(
258
+ file_name=file_name,
259
+ file_content=None, # 预切片模式下不需要原始内容
260
+ file_type=doc_info.get("file_type")
261
+ or (
262
+ file_name.rsplit(".", 1)[-1].lower()
263
+ if "." in file_name
264
+ else "txt"
265
+ ),
266
+ batch_size=batch_size,
267
+ tasks_limit=tasks_limit,
268
+ max_retries=max_retries,
269
+ progress_callback=progress_callback,
270
+ pre_chunked_text=chunks,
271
+ )
272
+
273
+ uploaded_docs.append(doc.model_dump())
274
+ except Exception as e:
275
+ logger.error(f"导入文档 {file_name} 失败: {e}")
276
+ failed_docs.append(
277
+ {"file_name": file_name, "error": str(e)},
278
+ )
279
+
280
+ # 更新任务完成状态
281
+ result = {
282
+ "task_id": task_id,
283
+ "uploaded": uploaded_docs,
284
+ "failed": failed_docs,
285
+ "total": len(documents),
286
+ "success_count": len(uploaded_docs),
287
+ "failed_count": len(failed_docs),
171
288
  }
172
- if task_id in self.upload_progress:
173
- self.upload_progress[task_id]["status"] = "failed"
289
+
290
+ self._set_task_result(task_id, "completed", result=result)
291
+
292
+ except Exception as e:
293
+ logger.error(f"后台导入任务 {task_id} 失败: {e}")
294
+ logger.error(traceback.format_exc())
295
+ self._set_task_result(task_id, "failed", error=str(e))
174
296
 
175
297
  async def list_kbs(self):
176
298
  """获取知识库列表
@@ -614,11 +736,7 @@ class KnowledgeBaseRoute(Route):
614
736
  task_id = str(uuid.uuid4())
615
737
 
616
738
  # 初始化任务状态
617
- self.upload_tasks[task_id] = {
618
- "status": "pending",
619
- "result": None,
620
- "error": None,
621
- }
739
+ self._init_task(task_id, status="pending")
622
740
 
623
741
  # 启动后台任务
624
742
  asyncio.create_task(
@@ -653,6 +771,93 @@ class KnowledgeBaseRoute(Route):
653
771
  logger.error(traceback.format_exc())
654
772
  return Response().error(f"上传文档失败: {e!s}").__dict__
655
773
 
774
+ def _validate_import_request(self, data: dict):
775
+ kb_id = data.get("kb_id")
776
+ if not kb_id:
777
+ raise ValueError("缺少参数 kb_id")
778
+
779
+ documents = data.get("documents")
780
+ if not documents or not isinstance(documents, list):
781
+ raise ValueError("缺少参数 documents 或格式错误")
782
+
783
+ for doc in documents:
784
+ if "file_name" not in doc or "chunks" not in doc:
785
+ raise ValueError("文档格式错误,必须包含 file_name 和 chunks")
786
+ if not isinstance(doc["chunks"], list):
787
+ raise ValueError("chunks 必须是列表")
788
+ if not all(
789
+ isinstance(chunk, str) and chunk.strip() for chunk in doc["chunks"]
790
+ ):
791
+ raise ValueError("chunks 必须是非空字符串列表")
792
+
793
+ batch_size = data.get("batch_size", 32)
794
+ tasks_limit = data.get("tasks_limit", 3)
795
+ max_retries = data.get("max_retries", 3)
796
+ return kb_id, documents, batch_size, tasks_limit, max_retries
797
+
798
+ async def import_documents(self):
799
+ """导入预切片文档
800
+
801
+ Body:
802
+ - kb_id: 知识库 ID (必填)
803
+ - documents: 文档列表 (必填)
804
+ - file_name: 文件名 (必填)
805
+ - chunks: 切片列表 (必填, list[str])
806
+ - file_type: 文件类型 (可选, 默认从文件名推断或为 txt)
807
+ - batch_size: 批处理大小 (可选, 默认32)
808
+ - tasks_limit: 并发任务限制 (可选, 默认3)
809
+ - max_retries: 最大重试次数 (可选, 默认3)
810
+ """
811
+ try:
812
+ kb_manager = self._get_kb_manager()
813
+ data = await request.json
814
+
815
+ kb_id, documents, batch_size, tasks_limit, max_retries = (
816
+ self._validate_import_request(data)
817
+ )
818
+
819
+ # 获取知识库
820
+ kb_helper = await kb_manager.get_kb(kb_id)
821
+ if not kb_helper:
822
+ return Response().error("知识库不存在").__dict__
823
+
824
+ # 生成任务ID
825
+ task_id = str(uuid.uuid4())
826
+
827
+ # 初始化任务状态
828
+ self._init_task(task_id, status="pending")
829
+
830
+ # 启动后台任务
831
+ asyncio.create_task(
832
+ self._background_import_task(
833
+ task_id=task_id,
834
+ kb_helper=kb_helper,
835
+ documents=documents,
836
+ batch_size=batch_size,
837
+ tasks_limit=tasks_limit,
838
+ max_retries=max_retries,
839
+ ),
840
+ )
841
+
842
+ return (
843
+ Response()
844
+ .ok(
845
+ {
846
+ "task_id": task_id,
847
+ "doc_count": len(documents),
848
+ "message": "import task created, processing in background",
849
+ },
850
+ )
851
+ .__dict__
852
+ )
853
+
854
+ except ValueError as e:
855
+ return Response().error(str(e)).__dict__
856
+ except Exception as e:
857
+ logger.error(f"导入文档失败: {e}")
858
+ logger.error(traceback.format_exc())
859
+ return Response().error(f"导入文档失败: {e!s}").__dict__
860
+
656
861
  async def get_upload_progress(self):
657
862
  """获取上传进度和结果
658
863
 
@@ -960,11 +1165,7 @@ class KnowledgeBaseRoute(Route):
960
1165
  task_id = str(uuid.uuid4())
961
1166
 
962
1167
  # 初始化任务状态
963
- self.upload_tasks[task_id] = {
964
- "status": "pending",
965
- "result": None,
966
- "error": None,
967
- }
1168
+ self._init_task(task_id, status="pending")
968
1169
 
969
1170
  # 启动后台任务
970
1171
  asyncio.create_task(
@@ -1017,11 +1218,7 @@ class KnowledgeBaseRoute(Route):
1017
1218
  """后台上传URL任务"""
1018
1219
  try:
1019
1220
  # 初始化任务状态
1020
- self.upload_tasks[task_id] = {
1021
- "status": "processing",
1022
- "result": None,
1023
- "error": None,
1024
- }
1221
+ self._init_task(task_id, status="processing")
1025
1222
  self.upload_progress[task_id] = {
1026
1223
  "status": "processing",
1027
1224
  "file_index": 0,
@@ -1033,18 +1230,7 @@ class KnowledgeBaseRoute(Route):
1033
1230
  }
1034
1231
 
1035
1232
  # 创建进度回调函数
1036
- async def progress_callback(stage, current, total):
1037
- if task_id in self.upload_progress:
1038
- self.upload_progress[task_id].update(
1039
- {
1040
- "status": "processing",
1041
- "file_index": 0,
1042
- "file_name": f"URL: {url}",
1043
- "stage": stage,
1044
- "current": current,
1045
- "total": total,
1046
- },
1047
- )
1233
+ progress_callback = self._make_progress_callback(task_id, 0, f"URL: {url}")
1048
1234
 
1049
1235
  # 上传文档
1050
1236
  doc = await kb_helper.upload_from_url(
@@ -1069,20 +1255,9 @@ class KnowledgeBaseRoute(Route):
1069
1255
  "failed_count": 0,
1070
1256
  }
1071
1257
 
1072
- self.upload_tasks[task_id] = {
1073
- "status": "completed",
1074
- "result": result,
1075
- "error": None,
1076
- }
1077
- self.upload_progress[task_id]["status"] = "completed"
1258
+ self._set_task_result(task_id, "completed", result=result)
1078
1259
 
1079
1260
  except Exception as e:
1080
1261
  logger.error(f"后台上传URL任务 {task_id} 失败: {e}")
1081
1262
  logger.error(traceback.format_exc())
1082
- self.upload_tasks[task_id] = {
1083
- "status": "failed",
1084
- "result": None,
1085
- "error": str(e),
1086
- }
1087
- if task_id in self.upload_progress:
1088
- self.upload_progress[task_id]["status"] = "failed"
1263
+ self._set_task_result(task_id, "failed", error=str(e))
@@ -1,6 +1,8 @@
1
1
  import asyncio
2
2
  import json
3
+ from typing import cast
3
4
 
5
+ from quart import Response as QuartResponse
4
6
  from quart import make_response
5
7
 
6
8
  from astrbot.core import LogBroker, logger
@@ -39,14 +41,17 @@ class LogRoute(Route):
39
41
  if queue:
40
42
  self.log_broker.unregister(queue)
41
43
 
42
- response = await make_response(
43
- stream(),
44
- {
45
- "Content-Type": "text/event-stream",
46
- "Cache-Control": "no-cache",
47
- "Connection": "keep-alive",
48
- "Transfer-Encoding": "chunked",
49
- },
44
+ response = cast(
45
+ QuartResponse,
46
+ await make_response(
47
+ stream(),
48
+ {
49
+ "Content-Type": "text/event-stream",
50
+ "Cache-Control": "no-cache",
51
+ "Connection": "keep-alive",
52
+ "Transfer-Encoding": "chunked",
53
+ },
54
+ ),
50
55
  )
51
56
  response.timeout = None
52
57
  return response
@@ -82,7 +82,7 @@ class PlatformRoute(Route):
82
82
  """
83
83
  for platform in self.platform_manager.platform_insts:
84
84
  if platform.config.get("webhook_uuid") == webhook_uuid:
85
- if platform.config.get("unified_webhook_mode", False):
85
+ if platform.unified_webhook():
86
86
  return platform
87
87
  return None
88
88