AstrBot 4.9.1__py3-none-any.whl → 4.10.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 (45) hide show
  1. astrbot/cli/__init__.py +1 -1
  2. astrbot/core/agent/message.py +6 -4
  3. astrbot/core/agent/response.py +22 -1
  4. astrbot/core/agent/run_context.py +1 -1
  5. astrbot/core/agent/runners/tool_loop_agent_runner.py +99 -20
  6. astrbot/core/astr_agent_context.py +3 -1
  7. astrbot/core/astr_agent_run_util.py +42 -3
  8. astrbot/core/astr_agent_tool_exec.py +34 -4
  9. astrbot/core/config/default.py +127 -184
  10. astrbot/core/core_lifecycle.py +3 -0
  11. astrbot/core/db/__init__.py +72 -0
  12. astrbot/core/db/po.py +59 -0
  13. astrbot/core/db/sqlite.py +240 -0
  14. astrbot/core/message/components.py +4 -5
  15. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +6 -1
  16. astrbot/core/pipeline/respond/stage.py +1 -1
  17. astrbot/core/platform/sources/telegram/tg_event.py +9 -0
  18. astrbot/core/platform/sources/webchat/webchat_event.py +22 -18
  19. astrbot/core/provider/entities.py +41 -0
  20. astrbot/core/provider/manager.py +203 -93
  21. astrbot/core/provider/sources/anthropic_source.py +55 -11
  22. astrbot/core/provider/sources/gemini_source.py +84 -33
  23. astrbot/core/provider/sources/openai_source.py +21 -6
  24. astrbot/core/star/__init__.py +5 -1
  25. astrbot/core/star/command_management.py +449 -0
  26. astrbot/core/star/context.py +4 -0
  27. astrbot/core/star/filter/command.py +1 -0
  28. astrbot/core/star/filter/command_group.py +1 -0
  29. astrbot/core/star/star_handler.py +4 -0
  30. astrbot/core/star/star_manager.py +14 -0
  31. astrbot/core/utils/llm_metadata.py +63 -0
  32. astrbot/core/utils/migra_helper.py +93 -0
  33. astrbot/core/utils/plugin_kv_store.py +28 -0
  34. astrbot/dashboard/routes/__init__.py +2 -0
  35. astrbot/dashboard/routes/chat.py +56 -13
  36. astrbot/dashboard/routes/command.py +82 -0
  37. astrbot/dashboard/routes/config.py +291 -33
  38. astrbot/dashboard/routes/stat.py +96 -0
  39. astrbot/dashboard/routes/tools.py +20 -4
  40. astrbot/dashboard/server.py +1 -0
  41. {astrbot-4.9.1.dist-info → astrbot-4.10.0.dist-info}/METADATA +2 -2
  42. {astrbot-4.9.1.dist-info → astrbot-4.10.0.dist-info}/RECORD +45 -41
  43. {astrbot-4.9.1.dist-info → astrbot-4.10.0.dist-info}/WHEEL +0 -0
  44. {astrbot-4.9.1.dist-info → astrbot-4.10.0.dist-info}/entry_points.txt +0 -0
  45. {astrbot-4.9.1.dist-info → astrbot-4.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,7 @@ from typing import Any
6
6
 
7
7
  from quart import request
8
8
 
9
- from astrbot.core import file_token_service, logger
9
+ from astrbot.core import astrbot_config, file_token_service, logger
10
10
  from astrbot.core.config.astrbot_config import AstrBotConfig
11
11
  from astrbot.core.config.default import (
12
12
  CONFIG_METADATA_2,
@@ -21,6 +21,7 @@ from astrbot.core.platform.register import platform_cls_map, platform_registry
21
21
  from astrbot.core.provider import Provider
22
22
  from astrbot.core.provider.register import provider_registry
23
23
  from astrbot.core.star.star import star_registry
24
+ from astrbot.core.utils.llm_metadata import LLM_METADATAS
24
25
  from astrbot.core.utils.webhook_utils import ensure_platform_webhook_config
25
26
 
26
27
  from .route import Response, Route, RouteContext
@@ -179,13 +180,149 @@ class ConfigRoute(Route):
179
180
  "/config/provider/new": ("POST", self.post_new_provider),
180
181
  "/config/provider/update": ("POST", self.post_update_provider),
181
182
  "/config/provider/delete": ("POST", self.post_delete_provider),
183
+ "/config/provider/template": ("GET", self.get_provider_template),
182
184
  "/config/provider/check_one": ("GET", self.check_one_provider_status),
183
185
  "/config/provider/list": ("GET", self.get_provider_config_list),
184
186
  "/config/provider/model_list": ("GET", self.get_provider_model_list),
185
187
  "/config/provider/get_embedding_dim": ("POST", self.get_embedding_dim),
188
+ "/config/provider_sources/<provider_source_id>/models": (
189
+ "GET",
190
+ self.get_provider_source_models,
191
+ ),
192
+ "/config/provider_sources/<provider_source_id>/update": (
193
+ "POST",
194
+ self.update_provider_source,
195
+ ),
196
+ "/config/provider_sources/<provider_source_id>/delete": (
197
+ "POST",
198
+ self.delete_provider_source,
199
+ ),
186
200
  }
187
201
  self.register_routes()
188
202
 
203
+ async def delete_provider_source(self, provider_source_id: str):
204
+ """删除 provider_source,并更新关联的 providers"""
205
+
206
+ provider_sources = self.config.get("provider_sources", [])
207
+ target_idx = next(
208
+ (
209
+ i
210
+ for i, ps in enumerate(provider_sources)
211
+ if ps.get("id") == provider_source_id
212
+ ),
213
+ -1,
214
+ )
215
+
216
+ if target_idx == -1:
217
+ return Response().error("未找到对应的 provider source").__dict__
218
+
219
+ # 删除 provider_source
220
+ del provider_sources[target_idx]
221
+
222
+ # 写回配置
223
+ self.config["provider_sources"] = provider_sources
224
+
225
+ # 删除引用了该 provider_source 的 providers
226
+ await self.core_lifecycle.provider_manager.delete_provider(
227
+ provider_source_id=provider_source_id
228
+ )
229
+
230
+ try:
231
+ save_config(self.config, self.config, is_core=True)
232
+ except Exception as e:
233
+ logger.error(traceback.format_exc())
234
+ return Response().error(str(e)).__dict__
235
+
236
+ return Response().ok(message="删除 provider source 成功").__dict__
237
+
238
+ async def update_provider_source(self, provider_source_id: str):
239
+ """更新或新增 provider_source,并重载关联的 providers"""
240
+
241
+ post_data = await request.json
242
+ if not post_data:
243
+ return Response().error("缺少配置数据").__dict__
244
+
245
+ new_source_config = post_data.get("config") or post_data
246
+ original_id = provider_source_id
247
+
248
+ if not isinstance(new_source_config, dict):
249
+ return Response().error("缺少或错误的配置数据").__dict__
250
+
251
+ # 确保配置中有 id 字段
252
+ if not new_source_config.get("id"):
253
+ new_source_config["id"] = original_id
254
+
255
+ provider_sources = self.config.get("provider_sources", [])
256
+
257
+ for ps in provider_sources:
258
+ if ps.get("id") == new_source_config["id"] and ps.get("id") != original_id:
259
+ return (
260
+ Response()
261
+ .error(
262
+ f"Provider source ID '{new_source_config['id']}' exists already, please try another ID.",
263
+ )
264
+ .__dict__
265
+ )
266
+
267
+ # 查找旧的 provider_source,若不存在则追加为新配置
268
+ target_idx = next(
269
+ (i for i, ps in enumerate(provider_sources) if ps.get("id") == original_id),
270
+ -1,
271
+ )
272
+
273
+ old_id = original_id
274
+ if target_idx == -1:
275
+ provider_sources.append(new_source_config)
276
+ else:
277
+ old_id = provider_sources[target_idx].get("id")
278
+ provider_sources[target_idx] = new_source_config
279
+
280
+ # 更新引用了该 provider_source 的 providers
281
+ affected_providers = []
282
+ for provider in self.config.get("provider", []):
283
+ if provider.get("provider_source_id") == old_id:
284
+ provider["provider_source_id"] = new_source_config["id"]
285
+ affected_providers.append(provider)
286
+
287
+ # 写回配置
288
+ self.config["provider_sources"] = provider_sources
289
+
290
+ try:
291
+ save_config(self.config, self.config, is_core=True)
292
+ except Exception as e:
293
+ logger.error(traceback.format_exc())
294
+ return Response().error(str(e)).__dict__
295
+
296
+ # 重载受影响的 providers,使新的 source 配置生效
297
+ reload_errors = []
298
+ prov_mgr = self.core_lifecycle.provider_manager
299
+ for provider in affected_providers:
300
+ try:
301
+ await prov_mgr.reload(provider)
302
+ except Exception as e:
303
+ logger.error(traceback.format_exc())
304
+ reload_errors.append(f"{provider.get('id')}: {e}")
305
+
306
+ if reload_errors:
307
+ return (
308
+ Response()
309
+ .error("更新成功,但部分提供商重载失败: " + ", ".join(reload_errors))
310
+ .__dict__
311
+ )
312
+
313
+ return Response().ok(message="更新 provider source 成功").__dict__
314
+
315
+ async def get_provider_template(self):
316
+ config_schema = {
317
+ "provider": CONFIG_METADATA_2["provider_group"]["metadata"]["provider"]
318
+ }
319
+ data = {
320
+ "config_schema": config_schema,
321
+ "providers": astrbot_config["provider"],
322
+ "provider_sources": astrbot_config["provider_sources"],
323
+ }
324
+ return Response().ok(data=data).__dict__
325
+
189
326
  async def get_uc_table(self):
190
327
  """获取 UMOP 配置路由表"""
191
328
  return Response().ok({"routing": self.ucr.umop_to_conf_id}).__dict__
@@ -433,9 +570,25 @@ class ConfigRoute(Route):
433
570
  return Response().error("缺少参数 provider_type").__dict__
434
571
  provider_type_ls = provider_type.split(",")
435
572
  provider_list = []
436
- astrbot_config = self.core_lifecycle.astrbot_config
437
- for provider in astrbot_config["provider"]:
438
- if provider.get("provider_type", None) in provider_type_ls:
573
+ ps = self.core_lifecycle.provider_manager.providers_config
574
+ p_source_pt = {
575
+ psrc["id"]: psrc["provider_type"]
576
+ for psrc in self.core_lifecycle.provider_manager.provider_sources_config
577
+ }
578
+ for provider in ps:
579
+ ps_id = provider.get("provider_source_id", None)
580
+ if (
581
+ ps_id
582
+ and ps_id in p_source_pt
583
+ and p_source_pt[ps_id] in provider_type_ls
584
+ ):
585
+ # chat
586
+ prov = self.core_lifecycle.provider_manager.get_merged_provider_config(
587
+ provider
588
+ )
589
+ provider_list.append(prov)
590
+ elif not ps_id and provider.get("provider_type", None) in provider_type_ls:
591
+ # agent runner, embedding, etc
439
592
  provider_list.append(provider)
440
593
  return Response().ok(provider_list).__dict__
441
594
 
@@ -458,9 +611,18 @@ class ConfigRoute(Route):
458
611
 
459
612
  try:
460
613
  models = await provider.get_models()
614
+ models = models or []
615
+
616
+ metadata_map = {}
617
+ for model_id in models:
618
+ meta = LLM_METADATAS.get(model_id)
619
+ if meta:
620
+ metadata_map[model_id] = meta
621
+
461
622
  ret = {
462
623
  "models": models,
463
624
  "provider_id": provider_id,
625
+ "model_metadata": metadata_map,
464
626
  }
465
627
  return Response().ok(ret).__dict__
466
628
  except Exception as e:
@@ -522,6 +684,100 @@ class ConfigRoute(Route):
522
684
  logger.error(traceback.format_exc())
523
685
  return Response().error(f"获取嵌入维度失败: {e!s}").__dict__
524
686
 
687
+ async def get_provider_source_models(self, provider_source_id: str):
688
+ """获取指定 provider_source 支持的模型列表
689
+
690
+ 本质上会临时初始化一个 Provider 实例,调用 get_models() 获取模型列表,然后销毁实例
691
+ """
692
+ try:
693
+ from astrbot.core.provider.register import provider_cls_map
694
+
695
+ # 从配置中查找对应的 provider_source
696
+ provider_sources = self.config.get("provider_sources", [])
697
+ provider_source = None
698
+ for ps in provider_sources:
699
+ if ps.get("id") == provider_source_id:
700
+ provider_source = ps
701
+ break
702
+
703
+ if not provider_source:
704
+ return (
705
+ Response()
706
+ .error(f"未找到 ID 为 {provider_source_id} 的 provider_source")
707
+ .__dict__
708
+ )
709
+
710
+ # 获取 provider 类型
711
+ provider_type = provider_source.get("type", None)
712
+ if not provider_type:
713
+ return Response().error("provider_source 缺少 type 字段").__dict__
714
+
715
+ try:
716
+ self.core_lifecycle.provider_manager.dynamic_import_provider(
717
+ provider_type
718
+ )
719
+ except ImportError as e:
720
+ logger.error(traceback.format_exc())
721
+ return Response().error(f"动态导入提供商适配器失败: {e!s}").__dict__
722
+
723
+ # 获取对应的 provider 类
724
+ if provider_type not in provider_cls_map:
725
+ return (
726
+ Response()
727
+ .error(f"未找到适用于 {provider_type} 的提供商适配器")
728
+ .__dict__
729
+ )
730
+
731
+ provider_metadata = provider_cls_map[provider_type]
732
+ cls_type = provider_metadata.cls_type
733
+
734
+ if not cls_type:
735
+ return Response().error(f"无法找到 {provider_type} 的类").__dict__
736
+
737
+ # 检查是否是 Provider 类型
738
+ if not issubclass(cls_type, Provider):
739
+ return (
740
+ Response()
741
+ .error(f"提供商 {provider_type} 不支持获取模型列表")
742
+ .__dict__
743
+ )
744
+
745
+ # 临时实例化 provider
746
+ inst = cls_type(provider_source, {})
747
+
748
+ # 如果有 initialize 方法,调用它
749
+ init_fn = getattr(inst, "initialize", None)
750
+ if inspect.iscoroutinefunction(init_fn):
751
+ await init_fn()
752
+
753
+ # 获取模型列表
754
+ models = await inst.get_models()
755
+ models = models or []
756
+
757
+ metadata_map = {}
758
+ for model_id in models:
759
+ meta = LLM_METADATAS.get(model_id)
760
+ if meta:
761
+ metadata_map[model_id] = meta
762
+
763
+ # 销毁实例(如果有 terminate 方法)
764
+ terminate_fn = getattr(inst, "terminate", None)
765
+ if inspect.iscoroutinefunction(terminate_fn):
766
+ await terminate_fn()
767
+
768
+ logger.info(
769
+ f"获取到 provider_source {provider_source_id} 的模型列表: {models}",
770
+ )
771
+
772
+ return (
773
+ Response()
774
+ .ok({"models": models, "model_metadata": metadata_map})
775
+ .__dict__
776
+ )
777
+ except Exception as e:
778
+ logger.error(traceback.format_exc())
779
+ return Response().error(f"获取模型列表失败: {e!s}").__dict__
780
+
525
781
  async def get_platform_list(self):
526
782
  """获取所有平台的列表"""
527
783
  platform_list = []
@@ -533,7 +789,15 @@ class ConfigRoute(Route):
533
789
  data = await request.json
534
790
  config = data.get("config", None)
535
791
  conf_id = data.get("conf_id", None)
792
+
536
793
  try:
794
+ # 不更新 provider_sources, provider, platform
795
+ # 这些配置有单独的接口进行更新
796
+ if conf_id == "default":
797
+ no_update_keys = ["provider_sources", "provider", "platform"]
798
+ for key in no_update_keys:
799
+ config[key] = self.acm.default_conf[key]
800
+
537
801
  await self._save_astrbot_configs(config, conf_id)
538
802
  await self.core_lifecycle.reload_pipeline_scheduler(conf_id)
539
803
  return Response().ok(None, "保存成功~").__dict__
@@ -573,28 +837,30 @@ class ConfigRoute(Route):
573
837
 
574
838
  async def post_new_provider(self):
575
839
  new_provider_config = await request.json
576
- self.config["provider"].append(new_provider_config)
840
+
577
841
  try:
578
- save_config(self.config, self.config, is_core=True)
579
- await self.core_lifecycle.provider_manager.load_provider(
580
- new_provider_config,
842
+ await self.core_lifecycle.provider_manager.create_provider(
843
+ new_provider_config
581
844
  )
582
845
  except Exception as e:
583
846
  return Response().error(str(e)).__dict__
584
- return Response().ok(None, "新增服务提供商配置成功~").__dict__
847
+ return Response().ok(None, "新增服务提供商配置成功").__dict__
585
848
 
586
849
  async def post_update_platform(self):
587
850
  update_platform_config = await request.json
588
- platform_id = update_platform_config.get("id", None)
851
+ origin_platform_id = update_platform_config.get("id", None)
589
852
  new_config = update_platform_config.get("config", None)
590
- if not platform_id or not new_config:
853
+ if not origin_platform_id or not new_config:
591
854
  return Response().error("参数错误").__dict__
592
855
 
856
+ if origin_platform_id != new_config.get("id", None):
857
+ return Response().error("机器人名称不允许修改").__dict__
858
+
593
859
  # 如果是支持统一 webhook 模式的平台,且启用了统一 webhook 模式,确保有 webhook_uuid
594
860
  ensure_platform_webhook_config(new_config)
595
861
 
596
862
  for i, platform in enumerate(self.config["platform"]):
597
- if platform["id"] == platform_id:
863
+ if platform["id"] == origin_platform_id:
598
864
  self.config["platform"][i] = new_config
599
865
  break
600
866
  else:
@@ -609,21 +875,15 @@ class ConfigRoute(Route):
609
875
 
610
876
  async def post_update_provider(self):
611
877
  update_provider_config = await request.json
612
- provider_id = update_provider_config.get("id", None)
878
+ origin_provider_id = update_provider_config.get("id", None)
613
879
  new_config = update_provider_config.get("config", None)
614
- if not provider_id or not new_config:
880
+ if not origin_provider_id or not new_config:
615
881
  return Response().error("参数错误").__dict__
616
882
 
617
- for i, provider in enumerate(self.config["provider"]):
618
- if provider["id"] == provider_id:
619
- self.config["provider"][i] = new_config
620
- break
621
- else:
622
- return Response().error("未找到对应服务提供商").__dict__
623
-
624
883
  try:
625
- save_config(self.config, self.config, is_core=True)
626
- await self.core_lifecycle.provider_manager.reload(new_config)
884
+ await self.core_lifecycle.provider_manager.update_provider(
885
+ origin_provider_id, new_config
886
+ )
627
887
  except Exception as e:
628
888
  return Response().error(str(e)).__dict__
629
889
  return Response().ok(None, "更新成功,已经实时生效~").__dict__
@@ -646,19 +906,17 @@ class ConfigRoute(Route):
646
906
 
647
907
  async def post_delete_provider(self):
648
908
  provider_id = await request.json
649
- provider_id = provider_id.get("id")
650
- for i, provider in enumerate(self.config["provider"]):
651
- if provider["id"] == provider_id:
652
- del self.config["provider"][i]
653
- break
654
- else:
655
- return Response().error("未找到对应服务提供商").__dict__
909
+ provider_id = provider_id.get("id", "")
910
+ if not provider_id:
911
+ return Response().error("缺少参数 id").__dict__
912
+
656
913
  try:
657
- save_config(self.config, self.config, is_core=True)
658
- await self.core_lifecycle.provider_manager.terminate_provider(provider_id)
914
+ await self.core_lifecycle.provider_manager.delete_provider(
915
+ provider_id=provider_id
916
+ )
659
917
  except Exception as e:
660
918
  return Response().error(str(e)).__dict__
661
- return Response().ok(None, "删除成功,已经实时生效~").__dict__
919
+ return Response().ok(None, "删除成功,已经实时生效。").__dict__
662
920
 
663
921
  async def get_llm_tools(self):
664
922
  """获取函数调用工具。包含了本地加载的以及 MCP 服务的工具"""
@@ -1,6 +1,9 @@
1
+ import os
2
+ import re
1
3
  import threading
2
4
  import time
3
5
  import traceback
6
+ from functools import cmp_to_key
4
7
 
5
8
  import aiohttp
6
9
  import psutil
@@ -11,7 +14,9 @@ from astrbot.core.config import VERSION
11
14
  from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
12
15
  from astrbot.core.db import BaseDatabase
13
16
  from astrbot.core.db.migration.helper import check_migration_needed_v4
17
+ from astrbot.core.utils.astrbot_path import get_astrbot_path
14
18
  from astrbot.core.utils.io import get_dashboard_version
19
+ from astrbot.core.utils.version_comparator import VersionComparator
15
20
 
16
21
  from .route import Response, Route, RouteContext
17
22
 
@@ -30,6 +35,8 @@ class StatRoute(Route):
30
35
  "/stat/start-time": ("GET", self.get_start_time),
31
36
  "/stat/restart-core": ("POST", self.restart_core),
32
37
  "/stat/test-ghproxy-connection": ("POST", self.test_ghproxy_connection),
38
+ "/stat/changelog": ("GET", self.get_changelog),
39
+ "/stat/changelog/list": ("GET", self.list_changelog_versions),
33
40
  }
34
41
  self.db_helper = db_helper
35
42
  self.register_routes()
@@ -183,3 +190,92 @@ class StatRoute(Route):
183
190
  except Exception as e:
184
191
  logger.error(traceback.format_exc())
185
192
  return Response().error(f"Error: {e!s}").__dict__
193
+
194
+ async def get_changelog(self):
195
+ """获取指定版本的更新日志"""
196
+ try:
197
+ version = request.args.get("version")
198
+ if not version:
199
+ return Response().error("version parameter is required").__dict__
200
+
201
+ version = version.lstrip("v")
202
+
203
+ # 防止路径遍历攻击
204
+ if not re.match(r"^[a-zA-Z0-9._-]+$", version):
205
+ return Response().error("Invalid version format").__dict__
206
+ if ".." in version or "/" in version or "\\" in version:
207
+ return Response().error("Invalid version format").__dict__
208
+
209
+ filename = f"v{version}.md"
210
+ project_path = get_astrbot_path()
211
+ changelogs_dir = os.path.join(project_path, "changelogs")
212
+ changelog_path = os.path.join(changelogs_dir, filename)
213
+
214
+ # 规范化路径,防止符号链接攻击
215
+ changelog_path = os.path.realpath(changelog_path)
216
+ changelogs_dir = os.path.realpath(changelogs_dir)
217
+
218
+ # 验证最终路径在预期的 changelogs 目录内(防止路径遍历)
219
+ # 确保规范化后的路径以 changelogs_dir 开头,且是目录内的文件
220
+ changelog_path_normalized = os.path.normpath(changelog_path)
221
+ changelogs_dir_normalized = os.path.normpath(changelogs_dir)
222
+
223
+ # 检查路径是否在预期目录内(必须是目录的子文件,不能是目录本身)
224
+ expected_prefix = changelogs_dir_normalized + os.sep
225
+ if not changelog_path_normalized.startswith(expected_prefix):
226
+ logger.warning(
227
+ f"Path traversal attempt detected: {version} -> {changelog_path}",
228
+ )
229
+ return Response().error("Invalid version format").__dict__
230
+
231
+ if not os.path.exists(changelog_path):
232
+ return (
233
+ Response()
234
+ .error(f"Changelog for version {version} not found")
235
+ .__dict__
236
+ )
237
+ if not os.path.isfile(changelog_path):
238
+ return (
239
+ Response()
240
+ .error(f"Changelog for version {version} not found")
241
+ .__dict__
242
+ )
243
+
244
+ with open(changelog_path, encoding="utf-8") as f:
245
+ content = f.read()
246
+
247
+ return Response().ok({"content": content, "version": version}).__dict__
248
+ except Exception as e:
249
+ logger.error(traceback.format_exc())
250
+ return Response().error(f"Error: {e!s}").__dict__
251
+
252
+ async def list_changelog_versions(self):
253
+ """获取所有可用的更新日志版本列表"""
254
+ try:
255
+ project_path = get_astrbot_path()
256
+ changelogs_dir = os.path.join(project_path, "changelogs")
257
+
258
+ if not os.path.exists(changelogs_dir):
259
+ return Response().ok({"versions": []}).__dict__
260
+
261
+ versions = []
262
+ for filename in os.listdir(changelogs_dir):
263
+ if filename.endswith(".md") and filename.startswith("v"):
264
+ # 提取版本号(去除 v 前缀和 .md 后缀)
265
+ version = filename[1:-3] # 去掉 "v" 和 ".md"
266
+ # 验证版本号格式
267
+ if re.match(r"^[a-zA-Z0-9._-]+$", version):
268
+ versions.append(version)
269
+
270
+ # 按版本号排序(降序,最新的在前)
271
+ # 使用项目中的 VersionComparator 进行语义化版本号排序
272
+ versions.sort(
273
+ key=cmp_to_key(
274
+ lambda v1, v2: VersionComparator.compare_version(v2, v1),
275
+ ),
276
+ )
277
+
278
+ return Response().ok({"versions": versions}).__dict__
279
+ except Exception as e:
280
+ logger.error(traceback.format_exc())
281
+ return Response().error(f"Error: {e!s}").__dict__
@@ -3,6 +3,7 @@ import traceback
3
3
  from quart import request
4
4
 
5
5
  from astrbot.core import logger
6
+ from astrbot.core.agent.mcp_client import MCPTool
6
7
  from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
7
8
  from astrbot.core.star import star_map
8
9
 
@@ -296,15 +297,30 @@ class ToolsRoute(Route):
296
297
  """获取所有注册的工具列表"""
297
298
  try:
298
299
  tools = self.tool_mgr.func_list
299
- tools_dict = [
300
- {
300
+ tools_dict = []
301
+ for tool in tools:
302
+ if isinstance(tool, MCPTool):
303
+ origin = "mcp"
304
+ origin_name = tool.mcp_server_name
305
+ elif tool.handler_module_path and star_map.get(
306
+ tool.handler_module_path
307
+ ):
308
+ star = star_map[tool.handler_module_path]
309
+ origin = "plugin"
310
+ origin_name = star.name
311
+ else:
312
+ origin = "unknown"
313
+ origin_name = "unknown"
314
+
315
+ tool_info = {
301
316
  "name": tool.name,
302
317
  "description": tool.description,
303
318
  "parameters": tool.parameters,
304
319
  "active": tool.active,
320
+ "origin": origin,
321
+ "origin_name": origin_name,
305
322
  }
306
- for tool in tools
307
- ]
323
+ tools_dict.append(tool_info)
308
324
  return Response().ok(data=tools_dict).__dict__
309
325
  except Exception as e:
310
326
  logger.error(traceback.format_exc())
@@ -67,6 +67,7 @@ class AstrBotDashboard:
67
67
  core_lifecycle,
68
68
  core_lifecycle.plugin_manager,
69
69
  )
70
+ self.command_route = CommandRoute(self.context)
70
71
  self.cr = ConfigRoute(self.context, core_lifecycle)
71
72
  self.lr = LogRoute(self.context, core_lifecycle.log_broker)
72
73
  self.sfr = StaticFileRoute(self.context)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: AstrBot
3
- Version: 4.9.1
3
+ Version: 4.10.0
4
4
  Summary: Easy-to-use multi-platform LLM chatbot and development framework
5
5
  License-File: LICENSE
6
6
  Keywords: Astrbot,Astrbot Module,Astrbot Plugin
@@ -26,7 +26,7 @@ Requires-Dist: dingtalk-stream>=0.22.1
26
26
  Requires-Dist: docstring-parser>=0.16
27
27
  Requires-Dist: faiss-cpu==1.10.0
28
28
  Requires-Dist: filelock>=3.18.0
29
- Requires-Dist: google-genai<1.51.0,>=1.14.0
29
+ Requires-Dist: google-genai>=1.56.0
30
30
  Requires-Dist: jieba>=0.42.1
31
31
  Requires-Dist: lark-oapi>=1.4.15
32
32
  Requires-Dist: lxml-html-clean>=0.4.2