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.
- astrbot/cli/__init__.py +1 -1
- astrbot/core/agent/message.py +6 -4
- astrbot/core/agent/response.py +22 -1
- astrbot/core/agent/run_context.py +1 -1
- astrbot/core/agent/runners/tool_loop_agent_runner.py +99 -20
- astrbot/core/astr_agent_context.py +3 -1
- astrbot/core/astr_agent_run_util.py +42 -3
- astrbot/core/astr_agent_tool_exec.py +34 -4
- astrbot/core/config/default.py +127 -184
- astrbot/core/core_lifecycle.py +3 -0
- astrbot/core/db/__init__.py +72 -0
- astrbot/core/db/po.py +59 -0
- astrbot/core/db/sqlite.py +240 -0
- astrbot/core/message/components.py +4 -5
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +6 -1
- astrbot/core/pipeline/respond/stage.py +1 -1
- astrbot/core/platform/sources/telegram/tg_event.py +9 -0
- astrbot/core/platform/sources/webchat/webchat_event.py +22 -18
- astrbot/core/provider/entities.py +41 -0
- astrbot/core/provider/manager.py +203 -93
- astrbot/core/provider/sources/anthropic_source.py +55 -11
- astrbot/core/provider/sources/gemini_source.py +84 -33
- astrbot/core/provider/sources/openai_source.py +21 -6
- astrbot/core/star/__init__.py +5 -1
- astrbot/core/star/command_management.py +449 -0
- astrbot/core/star/context.py +4 -0
- astrbot/core/star/filter/command.py +1 -0
- astrbot/core/star/filter/command_group.py +1 -0
- astrbot/core/star/star_handler.py +4 -0
- astrbot/core/star/star_manager.py +14 -0
- astrbot/core/utils/llm_metadata.py +63 -0
- astrbot/core/utils/migra_helper.py +93 -0
- astrbot/core/utils/plugin_kv_store.py +28 -0
- astrbot/dashboard/routes/__init__.py +2 -0
- astrbot/dashboard/routes/chat.py +56 -13
- astrbot/dashboard/routes/command.py +82 -0
- astrbot/dashboard/routes/config.py +291 -33
- astrbot/dashboard/routes/stat.py +96 -0
- astrbot/dashboard/routes/tools.py +20 -4
- astrbot/dashboard/server.py +1 -0
- {astrbot-4.9.1.dist-info → astrbot-4.10.0.dist-info}/METADATA +2 -2
- {astrbot-4.9.1.dist-info → astrbot-4.10.0.dist-info}/RECORD +45 -41
- {astrbot-4.9.1.dist-info → astrbot-4.10.0.dist-info}/WHEEL +0 -0
- {astrbot-4.9.1.dist-info → astrbot-4.10.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
840
|
+
|
|
577
841
|
try:
|
|
578
|
-
|
|
579
|
-
|
|
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, "
|
|
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
|
-
|
|
851
|
+
origin_platform_id = update_platform_config.get("id", None)
|
|
589
852
|
new_config = update_platform_config.get("config", None)
|
|
590
|
-
if not
|
|
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"] ==
|
|
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
|
-
|
|
878
|
+
origin_provider_id = update_provider_config.get("id", None)
|
|
613
879
|
new_config = update_provider_config.get("config", None)
|
|
614
|
-
if not
|
|
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
|
-
|
|
626
|
-
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
658
|
-
|
|
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, "
|
|
919
|
+
return Response().ok(None, "删除成功,已经实时生效。").__dict__
|
|
662
920
|
|
|
663
921
|
async def get_llm_tools(self):
|
|
664
922
|
"""获取函数调用工具。包含了本地加载的以及 MCP 服务的工具"""
|
astrbot/dashboard/routes/stat.py
CHANGED
|
@@ -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
|
-
|
|
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())
|
astrbot/dashboard/server.py
CHANGED
|
@@ -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.
|
|
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
|
|
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
|