AstrBot 4.7.3__py3-none-any.whl → 4.8.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 +21 -5
- astrbot/core/astr_agent_run_util.py +15 -1
- astrbot/core/config/default.py +113 -1
- astrbot/core/db/__init__.py +30 -1
- astrbot/core/db/sqlite.py +55 -1
- astrbot/core/message/components.py +6 -1
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +64 -5
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +1 -1
- astrbot/core/platform/manager.py +67 -9
- astrbot/core/platform/platform.py +99 -2
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +19 -5
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +5 -7
- astrbot/core/platform/sources/discord/discord_platform_adapter.py +1 -2
- astrbot/core/platform/sources/lark/lark_adapter.py +1 -3
- astrbot/core/platform/sources/misskey/misskey_adapter.py +1 -2
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +2 -0
- astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +1 -3
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +32 -9
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +13 -1
- astrbot/core/platform/sources/satori/satori_adapter.py +1 -2
- astrbot/core/platform/sources/slack/client.py +50 -39
- astrbot/core/platform/sources/slack/slack_adapter.py +21 -7
- astrbot/core/platform/sources/slack/slack_event.py +3 -3
- astrbot/core/platform/sources/telegram/tg_adapter.py +4 -3
- astrbot/core/platform/sources/webchat/webchat_adapter.py +95 -29
- astrbot/core/platform/sources/webchat/webchat_event.py +33 -33
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +1 -2
- astrbot/core/platform/sources/wecom/wecom_adapter.py +51 -9
- astrbot/core/platform/sources/wecom/wecom_event.py +1 -1
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +26 -9
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +27 -5
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +52 -11
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +1 -1
- astrbot/core/platform_message_history_mgr.py +3 -3
- astrbot/core/provider/provider.py +35 -0
- astrbot/core/provider/sources/whisper_api_source.py +43 -11
- astrbot/core/utils/file_extract.py +23 -0
- astrbot/core/utils/tencent_record_helper.py +1 -1
- astrbot/core/utils/webhook_utils.py +47 -0
- astrbot/dashboard/routes/__init__.py +2 -0
- astrbot/dashboard/routes/chat.py +300 -70
- astrbot/dashboard/routes/config.py +32 -165
- astrbot/dashboard/routes/knowledge_base.py +1 -1
- astrbot/dashboard/routes/platform.py +100 -0
- astrbot/dashboard/routes/plugin.py +65 -6
- astrbot/dashboard/server.py +3 -1
- {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/METADATA +48 -37
- {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/RECORD +52 -49
- {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/WHEEL +0 -0
- {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/entry_points.txt +0 -0
- {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,6 +2,7 @@ import asyncio
|
|
|
2
2
|
import inspect
|
|
3
3
|
import os
|
|
4
4
|
import traceback
|
|
5
|
+
import uuid
|
|
5
6
|
|
|
6
7
|
from quart import request
|
|
7
8
|
|
|
@@ -13,16 +14,14 @@ from astrbot.core.config.default import (
|
|
|
13
14
|
CONFIG_METADATA_3_SYSTEM,
|
|
14
15
|
DEFAULT_CONFIG,
|
|
15
16
|
DEFAULT_VALUE_MAP,
|
|
17
|
+
WEBHOOK_SUPPORTED_PLATFORMS,
|
|
16
18
|
)
|
|
17
19
|
from astrbot.core.config.i18n_utils import ConfigMetadataI18n
|
|
18
20
|
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
|
19
21
|
from astrbot.core.platform.register import platform_cls_map, platform_registry
|
|
20
22
|
from astrbot.core.provider import Provider
|
|
21
|
-
from astrbot.core.provider.entities import ProviderType
|
|
22
|
-
from astrbot.core.provider.provider import RerankProvider
|
|
23
23
|
from astrbot.core.provider.register import provider_registry
|
|
24
24
|
from astrbot.core.star.star import star_registry
|
|
25
|
-
from astrbot.core.utils.astrbot_path import get_astrbot_path
|
|
26
25
|
|
|
27
26
|
from .route import Response, Route, RouteContext
|
|
28
27
|
|
|
@@ -356,169 +355,20 @@ class ConfigRoute(Route):
|
|
|
356
355
|
f"Attempting to check provider: {status_info['name']} (ID: {status_info['id']}, Type: {status_info['type']}, Model: {status_info['model']})",
|
|
357
356
|
)
|
|
358
357
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
logger.debug(f"Sending 'Ping' to provider: {status_info['name']}")
|
|
362
|
-
response = await asyncio.wait_for(
|
|
363
|
-
provider.text_chat(prompt="REPLY `PONG` ONLY"),
|
|
364
|
-
timeout=45.0,
|
|
365
|
-
)
|
|
366
|
-
logger.debug(
|
|
367
|
-
f"Received response from {status_info['name']}: {response}",
|
|
368
|
-
)
|
|
369
|
-
if response is not None:
|
|
370
|
-
status_info["status"] = "available"
|
|
371
|
-
response_text_snippet = ""
|
|
372
|
-
if (
|
|
373
|
-
hasattr(response, "completion_text")
|
|
374
|
-
and response.completion_text
|
|
375
|
-
):
|
|
376
|
-
response_text_snippet = (
|
|
377
|
-
response.completion_text[:70] + "..."
|
|
378
|
-
if len(response.completion_text) > 70
|
|
379
|
-
else response.completion_text
|
|
380
|
-
)
|
|
381
|
-
elif hasattr(response, "result_chain") and response.result_chain:
|
|
382
|
-
try:
|
|
383
|
-
response_text_snippet = (
|
|
384
|
-
response.result_chain.get_plain_text()[:70] + "..."
|
|
385
|
-
if len(response.result_chain.get_plain_text()) > 70
|
|
386
|
-
else response.result_chain.get_plain_text()
|
|
387
|
-
)
|
|
388
|
-
except Exception as _:
|
|
389
|
-
pass
|
|
390
|
-
logger.info(
|
|
391
|
-
f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{response_text_snippet}'",
|
|
392
|
-
)
|
|
393
|
-
else:
|
|
394
|
-
status_info["error"] = (
|
|
395
|
-
"Test call returned None, but expected an LLMResponse object."
|
|
396
|
-
)
|
|
397
|
-
logger.warning(
|
|
398
|
-
f"Provider {status_info['name']} (ID: {status_info['id']}) test call returned None.",
|
|
399
|
-
)
|
|
400
|
-
|
|
401
|
-
except asyncio.TimeoutError:
|
|
402
|
-
status_info["error"] = (
|
|
403
|
-
"Connection timed out after 45 seconds during test call."
|
|
404
|
-
)
|
|
405
|
-
logger.warning(
|
|
406
|
-
f"Provider {status_info['name']} (ID: {status_info['id']}) timed out.",
|
|
407
|
-
)
|
|
408
|
-
except Exception as e:
|
|
409
|
-
error_message = str(e)
|
|
410
|
-
status_info["error"] = error_message
|
|
411
|
-
logger.warning(
|
|
412
|
-
f"Provider {status_info['name']} (ID: {status_info['id']}) is unavailable. Error: {error_message}",
|
|
413
|
-
)
|
|
414
|
-
logger.debug(
|
|
415
|
-
f"Traceback for {status_info['name']}:\n{traceback.format_exc()}",
|
|
416
|
-
)
|
|
417
|
-
|
|
418
|
-
elif provider_capability_type == ProviderType.EMBEDDING:
|
|
419
|
-
try:
|
|
420
|
-
# For embedding, we can call the get_embedding method with a short prompt.
|
|
421
|
-
embedding_result = await provider.get_embedding("health_check")
|
|
422
|
-
if isinstance(embedding_result, list) and (
|
|
423
|
-
not embedding_result or isinstance(embedding_result[0], float)
|
|
424
|
-
):
|
|
425
|
-
status_info["status"] = "available"
|
|
426
|
-
else:
|
|
427
|
-
status_info["status"] = "unavailable"
|
|
428
|
-
status_info["error"] = (
|
|
429
|
-
f"Embedding test failed: unexpected result type {type(embedding_result)}"
|
|
430
|
-
)
|
|
431
|
-
except Exception as e:
|
|
432
|
-
logger.error(
|
|
433
|
-
f"Error testing embedding provider {provider_name}: {e}",
|
|
434
|
-
exc_info=True,
|
|
435
|
-
)
|
|
436
|
-
status_info["status"] = "unavailable"
|
|
437
|
-
status_info["error"] = f"Embedding test failed: {e!s}"
|
|
438
|
-
|
|
439
|
-
elif provider_capability_type == ProviderType.TEXT_TO_SPEECH:
|
|
440
|
-
try:
|
|
441
|
-
# For TTS, we can call the get_audio method with a short prompt.
|
|
442
|
-
audio_result = await provider.get_audio("你好")
|
|
443
|
-
if isinstance(audio_result, str) and audio_result:
|
|
444
|
-
status_info["status"] = "available"
|
|
445
|
-
else:
|
|
446
|
-
status_info["status"] = "unavailable"
|
|
447
|
-
status_info["error"] = (
|
|
448
|
-
f"TTS test failed: unexpected result type {type(audio_result)}"
|
|
449
|
-
)
|
|
450
|
-
except Exception as e:
|
|
451
|
-
logger.error(
|
|
452
|
-
f"Error testing TTS provider {provider_name}: {e}",
|
|
453
|
-
exc_info=True,
|
|
454
|
-
)
|
|
455
|
-
status_info["status"] = "unavailable"
|
|
456
|
-
status_info["error"] = f"TTS test failed: {e!s}"
|
|
457
|
-
elif provider_capability_type == ProviderType.SPEECH_TO_TEXT:
|
|
458
|
-
try:
|
|
459
|
-
logger.debug(
|
|
460
|
-
f"Sending health check audio to provider: {status_info['name']}",
|
|
461
|
-
)
|
|
462
|
-
sample_audio_path = os.path.join(
|
|
463
|
-
get_astrbot_path(),
|
|
464
|
-
"samples",
|
|
465
|
-
"stt_health_check.wav",
|
|
466
|
-
)
|
|
467
|
-
if not os.path.exists(sample_audio_path):
|
|
468
|
-
status_info["status"] = "unavailable"
|
|
469
|
-
status_info["error"] = (
|
|
470
|
-
"STT test failed: sample audio file not found."
|
|
471
|
-
)
|
|
472
|
-
logger.warning(
|
|
473
|
-
f"STT test for {status_info['name']} failed: sample audio file not found at {sample_audio_path}",
|
|
474
|
-
)
|
|
475
|
-
else:
|
|
476
|
-
text_result = await provider.get_text(sample_audio_path)
|
|
477
|
-
if isinstance(text_result, str) and text_result:
|
|
478
|
-
status_info["status"] = "available"
|
|
479
|
-
snippet = (
|
|
480
|
-
text_result[:70] + "..."
|
|
481
|
-
if len(text_result) > 70
|
|
482
|
-
else text_result
|
|
483
|
-
)
|
|
484
|
-
logger.info(
|
|
485
|
-
f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{snippet}'",
|
|
486
|
-
)
|
|
487
|
-
else:
|
|
488
|
-
status_info["status"] = "unavailable"
|
|
489
|
-
status_info["error"] = (
|
|
490
|
-
f"STT test failed: unexpected result type {type(text_result)}"
|
|
491
|
-
)
|
|
492
|
-
logger.warning(
|
|
493
|
-
f"STT test for {status_info['name']} failed: unexpected result type {type(text_result)}",
|
|
494
|
-
)
|
|
495
|
-
except Exception as e:
|
|
496
|
-
logger.error(
|
|
497
|
-
f"Error testing STT provider {provider_name}: {e}",
|
|
498
|
-
exc_info=True,
|
|
499
|
-
)
|
|
500
|
-
status_info["status"] = "unavailable"
|
|
501
|
-
status_info["error"] = f"STT test failed: {e!s}"
|
|
502
|
-
elif provider_capability_type == ProviderType.RERANK:
|
|
503
|
-
try:
|
|
504
|
-
assert isinstance(provider, RerankProvider)
|
|
505
|
-
await provider.rerank("Apple", documents=["apple", "banana"])
|
|
506
|
-
status_info["status"] = "available"
|
|
507
|
-
except Exception as e:
|
|
508
|
-
logger.error(
|
|
509
|
-
f"Error testing rerank provider {provider_name}: {e}",
|
|
510
|
-
exc_info=True,
|
|
511
|
-
)
|
|
512
|
-
status_info["status"] = "unavailable"
|
|
513
|
-
status_info["error"] = f"Rerank test failed: {e!s}"
|
|
514
|
-
|
|
515
|
-
else:
|
|
516
|
-
logger.debug(
|
|
517
|
-
f"Provider {provider_name} is not a Chat Completion or Embedding provider. Marking as available without test. Meta: {meta}",
|
|
518
|
-
)
|
|
358
|
+
try:
|
|
359
|
+
await provider.test()
|
|
519
360
|
status_info["status"] = "available"
|
|
520
|
-
|
|
521
|
-
"
|
|
361
|
+
logger.info(
|
|
362
|
+
f"Provider {status_info['name']} (ID: {status_info['id']}) is available.",
|
|
363
|
+
)
|
|
364
|
+
except Exception as e:
|
|
365
|
+
error_message = str(e)
|
|
366
|
+
status_info["error"] = error_message
|
|
367
|
+
logger.warning(
|
|
368
|
+
f"Provider {status_info['name']} (ID: {status_info['id']}) is unavailable. Error: {error_message}",
|
|
369
|
+
)
|
|
370
|
+
logger.debug(
|
|
371
|
+
f"Traceback for {status_info['name']}:\n{traceback.format_exc()}",
|
|
522
372
|
)
|
|
523
373
|
|
|
524
374
|
return status_info
|
|
@@ -707,6 +557,15 @@ class ConfigRoute(Route):
|
|
|
707
557
|
|
|
708
558
|
async def post_new_platform(self):
|
|
709
559
|
new_platform_config = await request.json
|
|
560
|
+
|
|
561
|
+
# 如果是支持统一 webhook 模式的平台,且启用了统一 webhook 模式,自动生成 webhook_uuid
|
|
562
|
+
platform_type = new_platform_config.get("type", "")
|
|
563
|
+
if platform_type in WEBHOOK_SUPPORTED_PLATFORMS:
|
|
564
|
+
if new_platform_config.get("unified_webhook_mode", False):
|
|
565
|
+
# 如果没有 webhook_uuid 或为空,自动生成
|
|
566
|
+
if not new_platform_config.get("webhook_uuid"):
|
|
567
|
+
new_platform_config["webhook_uuid"] = uuid.uuid4().hex[:16]
|
|
568
|
+
|
|
710
569
|
self.config["platform"].append(new_platform_config)
|
|
711
570
|
try:
|
|
712
571
|
save_config(self.config, self.config, is_core=True)
|
|
@@ -736,6 +595,14 @@ class ConfigRoute(Route):
|
|
|
736
595
|
if not platform_id or not new_config:
|
|
737
596
|
return Response().error("参数错误").__dict__
|
|
738
597
|
|
|
598
|
+
# 如果是支持统一 webhook 模式的平台,且启用了统一 webhook 模式,确保有 webhook_uuid
|
|
599
|
+
platform_type = new_config.get("type", "")
|
|
600
|
+
if platform_type in WEBHOOK_SUPPORTED_PLATFORMS:
|
|
601
|
+
if new_config.get("unified_webhook_mode", False):
|
|
602
|
+
# 如果没有 webhook_uuid 或为空,自动生成
|
|
603
|
+
if not new_config.get("webhook_uuid"):
|
|
604
|
+
new_config["webhook_uuid"] = uuid.uuid4().hex
|
|
605
|
+
|
|
739
606
|
for i, platform in enumerate(self.config["platform"]):
|
|
740
607
|
if platform["id"] == platform_id:
|
|
741
608
|
self.config["platform"][i] = new_config
|
|
@@ -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.config.get("unified_webhook_mode", False):
|
|
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
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import json
|
|
2
3
|
import os
|
|
3
4
|
import ssl
|
|
@@ -19,6 +20,10 @@ from astrbot.core.star.star_manager import PluginManager
|
|
|
19
20
|
|
|
20
21
|
from .route import Response, Route, RouteContext
|
|
21
22
|
|
|
23
|
+
PLUGIN_UPDATE_CONCURRENCY = (
|
|
24
|
+
3 # limit concurrent updates to avoid overwhelming plugin sources
|
|
25
|
+
)
|
|
26
|
+
|
|
22
27
|
|
|
23
28
|
class PluginRoute(Route):
|
|
24
29
|
def __init__(
|
|
@@ -33,6 +38,7 @@ class PluginRoute(Route):
|
|
|
33
38
|
"/plugin/install": ("POST", self.install_plugin),
|
|
34
39
|
"/plugin/install-upload": ("POST", self.install_plugin_upload),
|
|
35
40
|
"/plugin/update": ("POST", self.update_plugin),
|
|
41
|
+
"/plugin/update-all": ("POST", self.update_all_plugins),
|
|
36
42
|
"/plugin/uninstall": ("POST", self.uninstall_plugin),
|
|
37
43
|
"/plugin/market_list": ("GET", self.get_online_plugins),
|
|
38
44
|
"/plugin/off": ("POST", self.off_plugin),
|
|
@@ -63,7 +69,7 @@ class PluginRoute(Route):
|
|
|
63
69
|
.__dict__
|
|
64
70
|
)
|
|
65
71
|
|
|
66
|
-
data = await request.
|
|
72
|
+
data = await request.get_json()
|
|
67
73
|
plugin_name = data.get("name", None)
|
|
68
74
|
try:
|
|
69
75
|
success, message = await self.plugin_manager.reload(plugin_name)
|
|
@@ -346,7 +352,7 @@ class PluginRoute(Route):
|
|
|
346
352
|
.__dict__
|
|
347
353
|
)
|
|
348
354
|
|
|
349
|
-
post_data = await request.
|
|
355
|
+
post_data = await request.get_json()
|
|
350
356
|
repo_url = post_data["url"]
|
|
351
357
|
|
|
352
358
|
proxy: str = post_data.get("proxy", None)
|
|
@@ -393,7 +399,7 @@ class PluginRoute(Route):
|
|
|
393
399
|
.__dict__
|
|
394
400
|
)
|
|
395
401
|
|
|
396
|
-
post_data = await request.
|
|
402
|
+
post_data = await request.get_json()
|
|
397
403
|
plugin_name = post_data["name"]
|
|
398
404
|
delete_config = post_data.get("delete_config", False)
|
|
399
405
|
delete_data = post_data.get("delete_data", False)
|
|
@@ -418,7 +424,7 @@ class PluginRoute(Route):
|
|
|
418
424
|
.__dict__
|
|
419
425
|
)
|
|
420
426
|
|
|
421
|
-
post_data = await request.
|
|
427
|
+
post_data = await request.get_json()
|
|
422
428
|
plugin_name = post_data["name"]
|
|
423
429
|
proxy: str = post_data.get("proxy", None)
|
|
424
430
|
try:
|
|
@@ -432,6 +438,59 @@ class PluginRoute(Route):
|
|
|
432
438
|
logger.error(f"/api/plugin/update: {traceback.format_exc()}")
|
|
433
439
|
return Response().error(str(e)).__dict__
|
|
434
440
|
|
|
441
|
+
async def update_all_plugins(self):
|
|
442
|
+
if DEMO_MODE:
|
|
443
|
+
return (
|
|
444
|
+
Response()
|
|
445
|
+
.error("You are not permitted to do this operation in demo mode")
|
|
446
|
+
.__dict__
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
post_data = await request.get_json()
|
|
450
|
+
plugin_names: list[str] = post_data.get("names") or []
|
|
451
|
+
proxy: str = post_data.get("proxy", "")
|
|
452
|
+
|
|
453
|
+
if not isinstance(plugin_names, list) or not plugin_names:
|
|
454
|
+
return Response().error("插件列表不能为空").__dict__
|
|
455
|
+
|
|
456
|
+
results = []
|
|
457
|
+
sem = asyncio.Semaphore(PLUGIN_UPDATE_CONCURRENCY)
|
|
458
|
+
|
|
459
|
+
async def _update_one(name: str):
|
|
460
|
+
async with sem:
|
|
461
|
+
try:
|
|
462
|
+
logger.info(f"批量更新插件 {name}")
|
|
463
|
+
await self.plugin_manager.update_plugin(name, proxy)
|
|
464
|
+
return {"name": name, "status": "ok", "message": "更新成功"}
|
|
465
|
+
except Exception as e:
|
|
466
|
+
logger.error(
|
|
467
|
+
f"/api/plugin/update-all: 更新插件 {name} 失败: {traceback.format_exc()}",
|
|
468
|
+
)
|
|
469
|
+
return {"name": name, "status": "error", "message": str(e)}
|
|
470
|
+
|
|
471
|
+
raw_results = await asyncio.gather(
|
|
472
|
+
*(_update_one(name) for name in plugin_names),
|
|
473
|
+
return_exceptions=True,
|
|
474
|
+
)
|
|
475
|
+
for name, result in zip(plugin_names, raw_results):
|
|
476
|
+
if isinstance(result, asyncio.CancelledError):
|
|
477
|
+
raise result
|
|
478
|
+
if isinstance(result, BaseException):
|
|
479
|
+
results.append(
|
|
480
|
+
{"name": name, "status": "error", "message": str(result)}
|
|
481
|
+
)
|
|
482
|
+
else:
|
|
483
|
+
results.append(result)
|
|
484
|
+
|
|
485
|
+
failed = [r for r in results if r["status"] == "error"]
|
|
486
|
+
message = (
|
|
487
|
+
"批量更新完成,全部成功。"
|
|
488
|
+
if not failed
|
|
489
|
+
else f"批量更新完成,其中 {len(failed)}/{len(results)} 个插件失败。"
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
return Response().ok({"results": results}, message).__dict__
|
|
493
|
+
|
|
435
494
|
async def off_plugin(self):
|
|
436
495
|
if DEMO_MODE:
|
|
437
496
|
return (
|
|
@@ -440,7 +499,7 @@ class PluginRoute(Route):
|
|
|
440
499
|
.__dict__
|
|
441
500
|
)
|
|
442
501
|
|
|
443
|
-
post_data = await request.
|
|
502
|
+
post_data = await request.get_json()
|
|
444
503
|
plugin_name = post_data["name"]
|
|
445
504
|
try:
|
|
446
505
|
await self.plugin_manager.turn_off_plugin(plugin_name)
|
|
@@ -458,7 +517,7 @@ class PluginRoute(Route):
|
|
|
458
517
|
.__dict__
|
|
459
518
|
)
|
|
460
519
|
|
|
461
|
-
post_data = await request.
|
|
520
|
+
post_data = await request.get_json()
|
|
462
521
|
plugin_name = post_data["name"]
|
|
463
522
|
try:
|
|
464
523
|
await self.plugin_manager.turn_on_plugin(plugin_name)
|
astrbot/dashboard/server.py
CHANGED
|
@@ -16,6 +16,7 @@ from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
|
|
16
16
|
from astrbot.core.utils.io import get_local_ip_addresses
|
|
17
17
|
|
|
18
18
|
from .routes import *
|
|
19
|
+
from .routes.platform import PlatformRoute
|
|
19
20
|
from .routes.route import Response, RouteContext
|
|
20
21
|
from .routes.session_management import SessionManagementRoute
|
|
21
22
|
from .routes.t2i import T2iRoute
|
|
@@ -79,6 +80,7 @@ class AstrBotDashboard:
|
|
|
79
80
|
self.persona_route = PersonaRoute(self.context, db, core_lifecycle)
|
|
80
81
|
self.t2i_route = T2iRoute(self.context, core_lifecycle)
|
|
81
82
|
self.kb_route = KnowledgeBaseRoute(self.context, core_lifecycle)
|
|
83
|
+
self.platform_route = PlatformRoute(self.context, core_lifecycle)
|
|
82
84
|
|
|
83
85
|
self.app.add_url_rule(
|
|
84
86
|
"/api/plug/<path:subpath>",
|
|
@@ -102,7 +104,7 @@ class AstrBotDashboard:
|
|
|
102
104
|
async def auth_middleware(self):
|
|
103
105
|
if not request.path.startswith("/api"):
|
|
104
106
|
return None
|
|
105
|
-
allowed_endpoints = ["/api/auth/login", "/api/file"]
|
|
107
|
+
allowed_endpoints = ["/api/auth/login", "/api/file", "/api/platform/webhook"]
|
|
106
108
|
if any(request.path.startswith(prefix) for prefix in allowed_endpoints):
|
|
107
109
|
return None
|
|
108
110
|
# 声明 JWT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AstrBot
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.8.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
|
|
@@ -61,11 +61,14 @@ Description-Content-Type: text/markdown
|
|
|
61
61
|
|
|
62
62
|

|
|
63
63
|
|
|
64
|
-
</p>
|
|
65
|
-
|
|
66
64
|
<div align="center">
|
|
67
65
|
|
|
68
|
-
|
|
66
|
+
|
|
67
|
+
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a> |
|
|
68
|
+
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a> |
|
|
69
|
+
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a> |
|
|
70
|
+
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a> |
|
|
71
|
+
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ru.md">Русский</a>
|
|
69
72
|
|
|
70
73
|
<div>
|
|
71
74
|
<a href="https://trendshift.io/repositories/12875" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12875" alt="Soulter%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
|
@@ -75,35 +78,37 @@ Description-Content-Type: text/markdown
|
|
|
75
78
|
<br>
|
|
76
79
|
|
|
77
80
|
<div>
|
|
78
|
-
<img src="https://img.shields.io/github/v/release/AstrBotDevs/AstrBot?
|
|
79
|
-
<img src="https://img.shields.io/badge/python-3.10+-blue.svg
|
|
80
|
-
<
|
|
81
|
-
<a href="https://
|
|
82
|
-
<
|
|
83
|
-
<img src="https://
|
|
81
|
+
<img src="https://img.shields.io/github/v/release/AstrBotDevs/AstrBot?color=76bad9" href="https://github.com/AstrBotDevs/AstrBot/releases/latest">
|
|
82
|
+
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python">
|
|
83
|
+
<img src="https://deepwiki.com/badge.svg" href="https://deepwiki.com/AstrBotDevs/AstrBot">
|
|
84
|
+
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg?color=76bad9"/></a>
|
|
85
|
+
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%E4%B8%AA&label=%E6%8F%92%E4%BB%B6%E5%B8%82%E5%9C%BA&cacheSeconds=3600">
|
|
86
|
+
<img src="https://gitcode.com/Soulter/AstrBot/star/badge.svg" href="https://gitcode.com/Soulter/AstrBot">
|
|
84
87
|
</div>
|
|
85
88
|
|
|
86
89
|
<br>
|
|
87
90
|
|
|
88
|
-
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a> |
|
|
89
|
-
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a> |
|
|
90
91
|
<a href="https://astrbot.app/">文档</a> |
|
|
91
92
|
<a href="https://blog.astrbot.app/">Blog</a> |
|
|
92
93
|
<a href="https://astrbot.featurebase.app/roadmap">路线图</a> |
|
|
93
94
|
<a href="https://github.com/AstrBotDevs/AstrBot/issues">问题提交</a>
|
|
94
95
|
</div>
|
|
95
96
|
|
|
96
|
-
AstrBot 是一个开源的一站式 Agent
|
|
97
|
+
AstrBot 是一个开源的一站式 Agent 聊天机器人平台,可接入主流即时通讯软件,为个人、开发者和团队打造可靠、可扩展的对话式智能基础设施。无论是个人 AI 伙伴、智能客服、自动化助手,还是企业知识库,AstrBot 都能在你的即时通讯软件平台的工作流中快速构建生产可用的 AI 应用。
|
|
98
|
+
|
|
99
|
+
<img width="1776" height="1080" alt="image" src="https://github.com/user-attachments/assets/00782c4c-4437-4d97-aabc-605e3738da5c" />
|
|
97
100
|
|
|
98
101
|
## 主要功能
|
|
99
102
|
|
|
100
|
-
1.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
1. 💯 免费 & 开源。
|
|
104
|
+
1. ✨ AI 大模型对话,多模态,Agent,MCP,知识库,人格设定。
|
|
105
|
+
2. 🤖 支持接入 Dify、阿里云百炼、Coze 等智能体平台。
|
|
106
|
+
2. 🌐 多平台,支持 QQ、企业微信、飞书、钉钉、微信公众号、Telegram、Slack 以及[更多](#支持的消息平台)。
|
|
107
|
+
3. 📦 插件扩展,已有近 800 个插件可一键安装。
|
|
108
|
+
5. 💻 WebUI 支持。
|
|
109
|
+
6. 🌐 国际化(i18n)支持。
|
|
105
110
|
|
|
106
|
-
##
|
|
111
|
+
## 快速开始
|
|
107
112
|
|
|
108
113
|
#### Docker 部署(推荐 🥳)
|
|
109
114
|
|
|
@@ -111,6 +116,12 @@ AstrBot 是一个开源的一站式 Agent 聊天机器人平台,可无缝接
|
|
|
111
116
|
|
|
112
117
|
请参阅官方文档 [使用 Docker 部署 AstrBot](https://astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot) 。
|
|
113
118
|
|
|
119
|
+
#### uv 部署
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
uvx astrbot
|
|
123
|
+
```
|
|
124
|
+
|
|
114
125
|
#### 宝塔面板部署
|
|
115
126
|
|
|
116
127
|
AstrBot 与宝塔面板合作,已上架至宝塔面板。
|
|
@@ -162,24 +173,6 @@ uv run main.py
|
|
|
162
173
|
|
|
163
174
|
或者请参阅官方文档 [通过源码部署 AstrBot](https://astrbot.app/deploy/astrbot/cli.html) 。
|
|
164
175
|
|
|
165
|
-
## 🌍 社区
|
|
166
|
-
|
|
167
|
-
### QQ 群组
|
|
168
|
-
|
|
169
|
-
- 1 群:322154837
|
|
170
|
-
- 3 群:630166526
|
|
171
|
-
- 5 群:822130018
|
|
172
|
-
- 6 群:753075035
|
|
173
|
-
- 开发者群:975206796
|
|
174
|
-
|
|
175
|
-
### Telegram 群组
|
|
176
|
-
|
|
177
|
-
<a href="https://t.me/+hAsD2Ebl5as3NmY1"><img alt="Telegram_community" src="https://img.shields.io/badge/Telegram-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
|
|
178
|
-
|
|
179
|
-
### Discord 群组
|
|
180
|
-
|
|
181
|
-
<a href="https://discord.gg/hAVk6tgV36"><img alt="Discord_community" src="https://img.shields.io/badge/Discord-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
|
|
182
|
-
|
|
183
176
|
## 支持的消息平台
|
|
184
177
|
|
|
185
178
|
**官方维护**
|
|
@@ -266,6 +259,24 @@ pip install pre-commit
|
|
|
266
259
|
pre-commit install
|
|
267
260
|
```
|
|
268
261
|
|
|
262
|
+
## 🌍 社区
|
|
263
|
+
|
|
264
|
+
### QQ 群组
|
|
265
|
+
|
|
266
|
+
- 1 群:322154837
|
|
267
|
+
- 3 群:630166526
|
|
268
|
+
- 5 群:822130018
|
|
269
|
+
- 6 群:753075035
|
|
270
|
+
- 开发者群:975206796
|
|
271
|
+
|
|
272
|
+
### Telegram 群组
|
|
273
|
+
|
|
274
|
+
<a href="https://t.me/+hAsD2Ebl5as3NmY1"><img alt="Telegram_community" src="https://img.shields.io/badge/Telegram-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
|
|
275
|
+
|
|
276
|
+
### Discord 群组
|
|
277
|
+
|
|
278
|
+
<a href="https://discord.gg/hAVk6tgV36"><img alt="Discord_community" src="https://img.shields.io/badge/Discord-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
|
|
279
|
+
|
|
269
280
|
## ❤️ Special Thanks
|
|
270
281
|
|
|
271
282
|
特别感谢所有 Contributors 和插件开发者对 AstrBot 的贡献 ❤️
|