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
@@ -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
  """获取知识库列表
@@ -274,7 +396,7 @@ class KnowledgeBaseRoute(Route):
274
396
  except Exception as e:
275
397
  return (
276
398
  Response()
277
- .error(f"测试重排序模型失败: {e!s},请检查控制台日志输出。")
399
+ .error(f"测试重排序模型失败: {e!s},请检查平台日志输出。")
278
400
  .__dict__
279
401
  )
280
402
 
@@ -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
@@ -0,0 +1,100 @@
1
+ """统一 Webhook 路由
2
+
3
+ 提供统一的 webhook 回调入口,支持多个平台使用同一端口接收回调。
4
+ """
5
+
6
+ from quart import request
7
+
8
+ from astrbot.core import logger
9
+ from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
10
+ from astrbot.core.platform import Platform
11
+
12
+ from .route import Response, Route, RouteContext
13
+
14
+
15
+ class PlatformRoute(Route):
16
+ """统一 Webhook 路由"""
17
+
18
+ def __init__(
19
+ self,
20
+ context: RouteContext,
21
+ core_lifecycle: AstrBotCoreLifecycle,
22
+ ) -> None:
23
+ super().__init__(context)
24
+ self.core_lifecycle = core_lifecycle
25
+ self.platform_manager = core_lifecycle.platform_manager
26
+
27
+ self._register_webhook_routes()
28
+
29
+ def _register_webhook_routes(self):
30
+ """注册 webhook 路由"""
31
+ # 统一 webhook 入口,支持 GET 和 POST
32
+ self.app.add_url_rule(
33
+ "/api/platform/webhook/<webhook_uuid>",
34
+ view_func=self.unified_webhook_callback,
35
+ methods=["GET", "POST"],
36
+ )
37
+
38
+ # 平台统计信息接口
39
+ self.app.add_url_rule(
40
+ "/api/platform/stats",
41
+ view_func=self.get_platform_stats,
42
+ methods=["GET"],
43
+ )
44
+
45
+ async def unified_webhook_callback(self, webhook_uuid: str):
46
+ """统一 webhook 回调入口
47
+
48
+ Args:
49
+ webhook_uuid: 平台配置中的 webhook_uuid
50
+
51
+ Returns:
52
+ 根据平台适配器返回相应的响应
53
+ """
54
+ # 根据 webhook_uuid 查找对应的平台
55
+ platform_adapter = self._find_platform_by_uuid(webhook_uuid)
56
+
57
+ if not platform_adapter:
58
+ logger.warning(f"未找到 webhook_uuid 为 {webhook_uuid} 的平台")
59
+ return Response().error("未找到对应平台").__dict__, 404
60
+
61
+ # 调用平台适配器的 webhook_callback 方法
62
+ try:
63
+ result = await platform_adapter.webhook_callback(request)
64
+ return result
65
+ except NotImplementedError:
66
+ logger.error(
67
+ f"平台 {platform_adapter.meta().name} 未实现 webhook_callback 方法"
68
+ )
69
+ return Response().error("平台未支持统一 Webhook 模式").__dict__, 500
70
+ except Exception as e:
71
+ logger.error(f"处理 webhook 回调时发生错误: {e}", exc_info=True)
72
+ return Response().error("处理回调失败").__dict__, 500
73
+
74
+ def _find_platform_by_uuid(self, webhook_uuid: str) -> Platform | None:
75
+ """根据 webhook_uuid 查找对应的平台适配器
76
+
77
+ Args:
78
+ webhook_uuid: webhook UUID
79
+
80
+ Returns:
81
+ 平台适配器实例,未找到则返回 None
82
+ """
83
+ for platform in self.platform_manager.platform_insts:
84
+ if platform.config.get("webhook_uuid") == webhook_uuid:
85
+ if platform.unified_webhook():
86
+ return platform
87
+ return None
88
+
89
+ async def get_platform_stats(self):
90
+ """获取所有平台的统计信息
91
+
92
+ Returns:
93
+ 包含平台统计信息的响应
94
+ """
95
+ try:
96
+ stats = self.platform_manager.get_all_stats()
97
+ return Response().ok(stats).__dict__
98
+ except Exception as e:
99
+ logger.error(f"获取平台统计信息失败: {e}", exc_info=True)
100
+ return Response().error(f"获取统计信息失败: {e}").__dict__, 500