AstrBot 3.5.6__py3-none-any.whl → 4.7.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/api/__init__.py +16 -4
- astrbot/api/all.py +2 -1
- astrbot/api/event/__init__.py +5 -6
- astrbot/api/event/filter/__init__.py +37 -34
- astrbot/api/platform/__init__.py +7 -8
- astrbot/api/provider/__init__.py +8 -7
- astrbot/api/star/__init__.py +3 -4
- astrbot/api/util/__init__.py +2 -2
- astrbot/cli/__init__.py +1 -0
- astrbot/cli/__main__.py +18 -197
- astrbot/cli/commands/__init__.py +6 -0
- astrbot/cli/commands/cmd_conf.py +209 -0
- astrbot/cli/commands/cmd_init.py +56 -0
- astrbot/cli/commands/cmd_plug.py +245 -0
- astrbot/cli/commands/cmd_run.py +62 -0
- astrbot/cli/utils/__init__.py +18 -0
- astrbot/cli/utils/basic.py +76 -0
- astrbot/cli/utils/plugin.py +246 -0
- astrbot/cli/utils/version_comparator.py +90 -0
- astrbot/core/__init__.py +17 -19
- astrbot/core/agent/agent.py +14 -0
- astrbot/core/agent/handoff.py +38 -0
- astrbot/core/agent/hooks.py +30 -0
- astrbot/core/agent/mcp_client.py +385 -0
- astrbot/core/agent/message.py +175 -0
- astrbot/core/agent/response.py +14 -0
- astrbot/core/agent/run_context.py +22 -0
- astrbot/core/agent/runners/__init__.py +3 -0
- astrbot/core/agent/runners/base.py +65 -0
- astrbot/core/agent/runners/coze/coze_agent_runner.py +367 -0
- astrbot/core/agent/runners/coze/coze_api_client.py +324 -0
- astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py +403 -0
- astrbot/core/agent/runners/dify/dify_agent_runner.py +336 -0
- astrbot/core/agent/runners/dify/dify_api_client.py +195 -0
- astrbot/core/agent/runners/tool_loop_agent_runner.py +400 -0
- astrbot/core/agent/tool.py +285 -0
- astrbot/core/agent/tool_executor.py +17 -0
- astrbot/core/astr_agent_context.py +19 -0
- astrbot/core/astr_agent_hooks.py +36 -0
- astrbot/core/astr_agent_run_util.py +80 -0
- astrbot/core/astr_agent_tool_exec.py +246 -0
- astrbot/core/astrbot_config_mgr.py +275 -0
- astrbot/core/config/__init__.py +2 -2
- astrbot/core/config/astrbot_config.py +60 -20
- astrbot/core/config/default.py +1972 -453
- astrbot/core/config/i18n_utils.py +110 -0
- astrbot/core/conversation_mgr.py +285 -75
- astrbot/core/core_lifecycle.py +167 -62
- astrbot/core/db/__init__.py +305 -102
- astrbot/core/db/migration/helper.py +69 -0
- astrbot/core/db/migration/migra_3_to_4.py +357 -0
- astrbot/core/db/migration/migra_45_to_46.py +44 -0
- astrbot/core/db/migration/migra_webchat_session.py +131 -0
- astrbot/core/db/migration/shared_preferences_v3.py +48 -0
- astrbot/core/db/migration/sqlite_v3.py +497 -0
- astrbot/core/db/po.py +259 -55
- astrbot/core/db/sqlite.py +773 -528
- astrbot/core/db/vec_db/base.py +73 -0
- astrbot/core/db/vec_db/faiss_impl/__init__.py +3 -0
- astrbot/core/db/vec_db/faiss_impl/document_storage.py +392 -0
- astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +93 -0
- astrbot/core/db/vec_db/faiss_impl/sqlite_init.sql +17 -0
- astrbot/core/db/vec_db/faiss_impl/vec_db.py +204 -0
- astrbot/core/event_bus.py +26 -22
- astrbot/core/exceptions.py +9 -0
- astrbot/core/file_token_service.py +98 -0
- astrbot/core/initial_loader.py +19 -10
- astrbot/core/knowledge_base/chunking/__init__.py +9 -0
- astrbot/core/knowledge_base/chunking/base.py +25 -0
- astrbot/core/knowledge_base/chunking/fixed_size.py +59 -0
- astrbot/core/knowledge_base/chunking/recursive.py +161 -0
- astrbot/core/knowledge_base/kb_db_sqlite.py +301 -0
- astrbot/core/knowledge_base/kb_helper.py +642 -0
- astrbot/core/knowledge_base/kb_mgr.py +330 -0
- astrbot/core/knowledge_base/models.py +120 -0
- astrbot/core/knowledge_base/parsers/__init__.py +13 -0
- astrbot/core/knowledge_base/parsers/base.py +51 -0
- astrbot/core/knowledge_base/parsers/markitdown_parser.py +26 -0
- astrbot/core/knowledge_base/parsers/pdf_parser.py +101 -0
- astrbot/core/knowledge_base/parsers/text_parser.py +42 -0
- astrbot/core/knowledge_base/parsers/url_parser.py +103 -0
- astrbot/core/knowledge_base/parsers/util.py +13 -0
- astrbot/core/knowledge_base/prompts.py +65 -0
- astrbot/core/knowledge_base/retrieval/__init__.py +14 -0
- astrbot/core/knowledge_base/retrieval/hit_stopwords.txt +767 -0
- astrbot/core/knowledge_base/retrieval/manager.py +276 -0
- astrbot/core/knowledge_base/retrieval/rank_fusion.py +142 -0
- astrbot/core/knowledge_base/retrieval/sparse_retriever.py +136 -0
- astrbot/core/log.py +21 -15
- astrbot/core/message/components.py +413 -287
- astrbot/core/message/message_event_result.py +35 -24
- astrbot/core/persona_mgr.py +192 -0
- astrbot/core/pipeline/__init__.py +14 -14
- astrbot/core/pipeline/content_safety_check/stage.py +13 -9
- astrbot/core/pipeline/content_safety_check/strategies/__init__.py +1 -2
- astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py +13 -14
- astrbot/core/pipeline/content_safety_check/strategies/keywords.py +2 -1
- astrbot/core/pipeline/content_safety_check/strategies/strategy.py +6 -6
- astrbot/core/pipeline/context.py +7 -1
- astrbot/core/pipeline/context_utils.py +107 -0
- astrbot/core/pipeline/preprocess_stage/stage.py +63 -36
- astrbot/core/pipeline/process_stage/method/agent_request.py +48 -0
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +464 -0
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +202 -0
- astrbot/core/pipeline/process_stage/method/star_request.py +26 -32
- astrbot/core/pipeline/process_stage/stage.py +21 -15
- astrbot/core/pipeline/process_stage/utils.py +125 -0
- astrbot/core/pipeline/rate_limit_check/stage.py +34 -36
- astrbot/core/pipeline/respond/stage.py +142 -101
- astrbot/core/pipeline/result_decorate/stage.py +124 -57
- astrbot/core/pipeline/scheduler.py +21 -16
- astrbot/core/pipeline/session_status_check/stage.py +37 -0
- astrbot/core/pipeline/stage.py +11 -76
- astrbot/core/pipeline/waking_check/stage.py +69 -33
- astrbot/core/pipeline/whitelist_check/stage.py +10 -7
- astrbot/core/platform/__init__.py +6 -6
- astrbot/core/platform/astr_message_event.py +107 -129
- astrbot/core/platform/astrbot_message.py +32 -12
- astrbot/core/platform/manager.py +62 -18
- astrbot/core/platform/message_session.py +30 -0
- astrbot/core/platform/platform.py +16 -24
- astrbot/core/platform/platform_metadata.py +9 -4
- astrbot/core/platform/register.py +12 -7
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +136 -60
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +126 -46
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +63 -31
- astrbot/core/platform/sources/dingtalk/dingtalk_event.py +30 -26
- astrbot/core/platform/sources/discord/client.py +129 -0
- astrbot/core/platform/sources/discord/components.py +139 -0
- astrbot/core/platform/sources/discord/discord_platform_adapter.py +473 -0
- astrbot/core/platform/sources/discord/discord_platform_event.py +313 -0
- astrbot/core/platform/sources/lark/lark_adapter.py +27 -18
- astrbot/core/platform/sources/lark/lark_event.py +39 -13
- astrbot/core/platform/sources/misskey/misskey_adapter.py +770 -0
- astrbot/core/platform/sources/misskey/misskey_api.py +964 -0
- astrbot/core/platform/sources/misskey/misskey_event.py +163 -0
- astrbot/core/platform/sources/misskey/misskey_utils.py +550 -0
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +149 -33
- astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +41 -26
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -17
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_event.py +3 -1
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +14 -8
- astrbot/core/platform/sources/satori/satori_adapter.py +792 -0
- astrbot/core/platform/sources/satori/satori_event.py +432 -0
- astrbot/core/platform/sources/slack/client.py +164 -0
- astrbot/core/platform/sources/slack/slack_adapter.py +416 -0
- astrbot/core/platform/sources/slack/slack_event.py +253 -0
- astrbot/core/platform/sources/telegram/tg_adapter.py +100 -43
- astrbot/core/platform/sources/telegram/tg_event.py +136 -36
- astrbot/core/platform/sources/webchat/webchat_adapter.py +72 -22
- astrbot/core/platform/sources/webchat/webchat_event.py +46 -22
- astrbot/core/platform/sources/webchat/webchat_queue_mgr.py +35 -0
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +926 -0
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +178 -0
- astrbot/core/platform/sources/wechatpadpro/xml_data_parser.py +159 -0
- astrbot/core/platform/sources/wecom/wecom_adapter.py +169 -27
- astrbot/core/platform/sources/wecom/wecom_event.py +162 -77
- astrbot/core/platform/sources/wecom/wecom_kf.py +279 -0
- astrbot/core/platform/sources/wecom/wecom_kf_message.py +196 -0
- astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py +297 -0
- astrbot/core/platform/sources/wecom_ai_bot/__init__.py +15 -0
- astrbot/core/platform/sources/wecom_ai_bot/ierror.py +19 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +472 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py +417 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +152 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py +153 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +168 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_utils.py +209 -0
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +306 -0
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +186 -0
- astrbot/core/platform_message_history_mgr.py +49 -0
- astrbot/core/provider/__init__.py +2 -3
- astrbot/core/provider/entites.py +8 -8
- astrbot/core/provider/entities.py +154 -98
- astrbot/core/provider/func_tool_manager.py +446 -458
- astrbot/core/provider/manager.py +345 -207
- astrbot/core/provider/provider.py +188 -73
- astrbot/core/provider/register.py +9 -7
- astrbot/core/provider/sources/anthropic_source.py +295 -115
- astrbot/core/provider/sources/azure_tts_source.py +224 -0
- astrbot/core/provider/sources/bailian_rerank_source.py +236 -0
- astrbot/core/provider/sources/dashscope_tts.py +138 -14
- astrbot/core/provider/sources/edge_tts_source.py +24 -19
- astrbot/core/provider/sources/fishaudio_tts_api_source.py +58 -13
- astrbot/core/provider/sources/gemini_embedding_source.py +61 -0
- astrbot/core/provider/sources/gemini_source.py +310 -132
- astrbot/core/provider/sources/gemini_tts_source.py +81 -0
- astrbot/core/provider/sources/groq_source.py +15 -0
- astrbot/core/provider/sources/gsv_selfhosted_source.py +151 -0
- astrbot/core/provider/sources/gsvi_tts_source.py +14 -7
- astrbot/core/provider/sources/minimax_tts_api_source.py +159 -0
- astrbot/core/provider/sources/openai_embedding_source.py +40 -0
- astrbot/core/provider/sources/openai_source.py +241 -145
- astrbot/core/provider/sources/openai_tts_api_source.py +18 -7
- astrbot/core/provider/sources/sensevoice_selfhosted_source.py +13 -11
- astrbot/core/provider/sources/vllm_rerank_source.py +71 -0
- astrbot/core/provider/sources/volcengine_tts.py +115 -0
- astrbot/core/provider/sources/whisper_api_source.py +18 -13
- astrbot/core/provider/sources/whisper_selfhosted_source.py +19 -12
- astrbot/core/provider/sources/xinference_rerank_source.py +116 -0
- astrbot/core/provider/sources/xinference_stt_provider.py +197 -0
- astrbot/core/provider/sources/zhipu_source.py +6 -73
- astrbot/core/star/__init__.py +43 -11
- astrbot/core/star/config.py +17 -18
- astrbot/core/star/context.py +362 -138
- astrbot/core/star/filter/__init__.py +4 -3
- astrbot/core/star/filter/command.py +111 -35
- astrbot/core/star/filter/command_group.py +46 -34
- astrbot/core/star/filter/custom_filter.py +6 -5
- astrbot/core/star/filter/event_message_type.py +4 -2
- astrbot/core/star/filter/permission.py +4 -2
- astrbot/core/star/filter/platform_adapter_type.py +45 -12
- astrbot/core/star/filter/regex.py +4 -2
- astrbot/core/star/register/__init__.py +19 -15
- astrbot/core/star/register/star.py +41 -13
- astrbot/core/star/register/star_handler.py +236 -86
- astrbot/core/star/session_llm_manager.py +280 -0
- astrbot/core/star/session_plugin_manager.py +170 -0
- astrbot/core/star/star.py +36 -43
- astrbot/core/star/star_handler.py +47 -85
- astrbot/core/star/star_manager.py +442 -260
- astrbot/core/star/star_tools.py +167 -45
- astrbot/core/star/updator.py +17 -20
- astrbot/core/umop_config_router.py +106 -0
- astrbot/core/updator.py +38 -13
- astrbot/core/utils/astrbot_path.py +39 -0
- astrbot/core/utils/command_parser.py +1 -1
- astrbot/core/utils/io.py +119 -60
- astrbot/core/utils/log_pipe.py +1 -1
- astrbot/core/utils/metrics.py +11 -10
- astrbot/core/utils/migra_helper.py +73 -0
- astrbot/core/utils/path_util.py +63 -62
- astrbot/core/utils/pip_installer.py +37 -15
- astrbot/core/utils/session_lock.py +29 -0
- astrbot/core/utils/session_waiter.py +19 -20
- astrbot/core/utils/shared_preferences.py +174 -34
- astrbot/core/utils/t2i/__init__.py +4 -1
- astrbot/core/utils/t2i/local_strategy.py +386 -238
- astrbot/core/utils/t2i/network_strategy.py +109 -49
- astrbot/core/utils/t2i/renderer.py +29 -14
- astrbot/core/utils/t2i/template/astrbot_powershell.html +184 -0
- astrbot/core/utils/t2i/template_manager.py +111 -0
- astrbot/core/utils/tencent_record_helper.py +115 -1
- astrbot/core/utils/version_comparator.py +10 -13
- astrbot/core/zip_updator.py +112 -65
- astrbot/dashboard/routes/__init__.py +20 -13
- astrbot/dashboard/routes/auth.py +20 -9
- astrbot/dashboard/routes/chat.py +297 -141
- astrbot/dashboard/routes/config.py +652 -55
- astrbot/dashboard/routes/conversation.py +107 -37
- astrbot/dashboard/routes/file.py +26 -0
- astrbot/dashboard/routes/knowledge_base.py +1244 -0
- astrbot/dashboard/routes/log.py +27 -2
- astrbot/dashboard/routes/persona.py +202 -0
- astrbot/dashboard/routes/plugin.py +197 -139
- astrbot/dashboard/routes/route.py +27 -7
- astrbot/dashboard/routes/session_management.py +354 -0
- astrbot/dashboard/routes/stat.py +85 -18
- astrbot/dashboard/routes/static_file.py +5 -2
- astrbot/dashboard/routes/t2i.py +233 -0
- astrbot/dashboard/routes/tools.py +184 -120
- astrbot/dashboard/routes/update.py +59 -36
- astrbot/dashboard/server.py +96 -36
- astrbot/dashboard/utils.py +165 -0
- astrbot-4.7.0.dist-info/METADATA +294 -0
- astrbot-4.7.0.dist-info/RECORD +274 -0
- {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/WHEEL +1 -1
- astrbot/core/db/plugin/sqlite_impl.py +0 -112
- astrbot/core/db/sqlite_init.sql +0 -50
- astrbot/core/pipeline/platform_compatibility/stage.py +0 -56
- astrbot/core/pipeline/process_stage/method/llm_request.py +0 -606
- astrbot/core/platform/sources/gewechat/client.py +0 -806
- astrbot/core/platform/sources/gewechat/downloader.py +0 -55
- astrbot/core/platform/sources/gewechat/gewechat_event.py +0 -255
- astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py +0 -103
- astrbot/core/platform/sources/gewechat/xml_data_parser.py +0 -110
- astrbot/core/provider/sources/dashscope_source.py +0 -203
- astrbot/core/provider/sources/dify_source.py +0 -281
- astrbot/core/provider/sources/llmtuner_source.py +0 -132
- astrbot/core/rag/embedding/openai_source.py +0 -20
- astrbot/core/rag/knowledge_db_mgr.py +0 -94
- astrbot/core/rag/store/__init__.py +0 -9
- astrbot/core/rag/store/chroma_db.py +0 -42
- astrbot/core/utils/dify_api_client.py +0 -152
- astrbot-3.5.6.dist-info/METADATA +0 -249
- astrbot-3.5.6.dist-info/RECORD +0 -158
- {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/entry_points.txt +0 -0
- {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/licenses/LICENSE +0 -0
astrbot/core/utils/io.py
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import logging
|
|
1
3
|
import os
|
|
2
|
-
import ssl
|
|
3
4
|
import shutil
|
|
4
5
|
import socket
|
|
6
|
+
import ssl
|
|
5
7
|
import time
|
|
6
|
-
import aiohttp
|
|
7
|
-
import base64
|
|
8
|
-
import zipfile
|
|
9
8
|
import uuid
|
|
10
|
-
import
|
|
9
|
+
import zipfile
|
|
10
|
+
from pathlib import Path
|
|
11
11
|
|
|
12
|
+
import aiohttp
|
|
12
13
|
import certifi
|
|
14
|
+
import psutil
|
|
15
|
+
from PIL import Image
|
|
13
16
|
|
|
14
|
-
from
|
|
17
|
+
from .astrbot_path import get_astrbot_data_path
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
logger = logging.getLogger("astrbot")
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
def on_error(func, path, exc_info):
|
|
20
|
-
"""
|
|
21
|
-
a callback of the rmtree function.
|
|
22
|
-
"""
|
|
23
|
+
"""A callback of the rmtree function."""
|
|
23
24
|
import stat
|
|
24
25
|
|
|
25
26
|
if not os.access(path, os.W_OK):
|
|
@@ -29,7 +30,7 @@ def on_error(func, path, exc_info):
|
|
|
29
30
|
raise exc_info[1]
|
|
30
31
|
|
|
31
32
|
|
|
32
|
-
def remove_dir(file_path) -> bool:
|
|
33
|
+
def remove_dir(file_path: str) -> bool:
|
|
33
34
|
if not os.path.exists(file_path):
|
|
34
35
|
return True
|
|
35
36
|
shutil.rmtree(file_path, onerror=on_error)
|
|
@@ -48,12 +49,12 @@ def port_checker(port: int, host: str = "localhost"):
|
|
|
48
49
|
return False
|
|
49
50
|
|
|
50
51
|
|
|
51
|
-
def save_temp_img(img:
|
|
52
|
-
os.
|
|
52
|
+
def save_temp_img(img: Image.Image | str) -> str:
|
|
53
|
+
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
|
53
54
|
# 获得文件创建时间,清除超过 12 小时的
|
|
54
55
|
try:
|
|
55
|
-
for f in os.listdir(
|
|
56
|
-
path = os.path.join(
|
|
56
|
+
for f in os.listdir(temp_dir):
|
|
57
|
+
path = os.path.join(temp_dir, f)
|
|
57
58
|
if os.path.isfile(path):
|
|
58
59
|
ctime = os.path.getctime(path)
|
|
59
60
|
if time.time() - ctime > 3600 * 12:
|
|
@@ -63,7 +64,7 @@ def save_temp_img(img: Union[Image.Image, str]) -> str:
|
|
|
63
64
|
|
|
64
65
|
# 获得时间戳
|
|
65
66
|
timestamp = f"{int(time.time())}_{uuid.uuid4().hex[:8]}"
|
|
66
|
-
p = f"
|
|
67
|
+
p = os.path.join(temp_dir, f"{timestamp}.jpg")
|
|
67
68
|
|
|
68
69
|
if isinstance(img, Image.Image):
|
|
69
70
|
img.save(p)
|
|
@@ -74,61 +75,75 @@ def save_temp_img(img: Union[Image.Image, str]) -> str:
|
|
|
74
75
|
|
|
75
76
|
|
|
76
77
|
async def download_image_by_url(
|
|
77
|
-
url: str,
|
|
78
|
+
url: str,
|
|
79
|
+
post: bool = False,
|
|
80
|
+
post_data: dict | None = None,
|
|
81
|
+
path: str | None = None,
|
|
78
82
|
) -> str:
|
|
79
|
-
"""
|
|
80
|
-
下载图片, 返回 path
|
|
81
|
-
"""
|
|
83
|
+
"""下载图片, 返回 path"""
|
|
82
84
|
try:
|
|
83
85
|
ssl_context = ssl.create_default_context(
|
|
84
|
-
cafile=certifi.where()
|
|
86
|
+
cafile=certifi.where(),
|
|
85
87
|
) # 使用 certifi 提供的 CA 证书
|
|
86
88
|
connector = aiohttp.TCPConnector(ssl=ssl_context) # 使用 certifi 的根证书
|
|
87
89
|
async with aiohttp.ClientSession(
|
|
88
|
-
trust_env=True,
|
|
90
|
+
trust_env=True,
|
|
91
|
+
connector=connector,
|
|
89
92
|
) as session:
|
|
90
93
|
if post:
|
|
91
94
|
async with session.post(url, json=post_data) as resp:
|
|
92
95
|
if not path:
|
|
93
96
|
return save_temp_img(await resp.read())
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return path
|
|
97
|
+
with open(path, "wb") as f:
|
|
98
|
+
f.write(await resp.read())
|
|
99
|
+
return path
|
|
98
100
|
else:
|
|
99
101
|
async with session.get(url) as resp:
|
|
100
102
|
if not path:
|
|
101
103
|
return save_temp_img(await resp.read())
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return path
|
|
104
|
+
with open(path, "wb") as f:
|
|
105
|
+
f.write(await resp.read())
|
|
106
|
+
return path
|
|
106
107
|
except (aiohttp.ClientConnectorSSLError, aiohttp.ClientConnectorCertificateError):
|
|
107
|
-
# 关闭SSL
|
|
108
|
+
# 关闭SSL验证(仅在证书验证失败时作为fallback)
|
|
109
|
+
logger.warning(
|
|
110
|
+
f"SSL certificate verification failed for {url}. "
|
|
111
|
+
"Disabling SSL verification (CERT_NONE) as a fallback. "
|
|
112
|
+
"This is insecure and exposes the application to man-in-the-middle attacks. "
|
|
113
|
+
"Please investigate and resolve certificate issues."
|
|
114
|
+
)
|
|
108
115
|
ssl_context = ssl.create_default_context()
|
|
109
|
-
ssl_context.
|
|
116
|
+
ssl_context.check_hostname = False
|
|
117
|
+
ssl_context.verify_mode = ssl.CERT_NONE
|
|
110
118
|
async with aiohttp.ClientSession() as session:
|
|
111
119
|
if post:
|
|
112
|
-
async with session.
|
|
113
|
-
|
|
120
|
+
async with session.post(url, json=post_data, ssl=ssl_context) as resp:
|
|
121
|
+
if not path:
|
|
122
|
+
return save_temp_img(await resp.read())
|
|
123
|
+
with open(path, "wb") as f:
|
|
124
|
+
f.write(await resp.read())
|
|
125
|
+
return path
|
|
114
126
|
else:
|
|
115
127
|
async with session.get(url, ssl=ssl_context) as resp:
|
|
116
|
-
|
|
128
|
+
if not path:
|
|
129
|
+
return save_temp_img(await resp.read())
|
|
130
|
+
with open(path, "wb") as f:
|
|
131
|
+
f.write(await resp.read())
|
|
132
|
+
return path
|
|
117
133
|
except Exception as e:
|
|
118
134
|
raise e
|
|
119
135
|
|
|
120
136
|
|
|
121
137
|
async def download_file(url: str, path: str, show_progress: bool = False):
|
|
122
|
-
"""
|
|
123
|
-
从指定 url 下载文件到指定路径 path
|
|
124
|
-
"""
|
|
138
|
+
"""从指定 url 下载文件到指定路径 path"""
|
|
125
139
|
try:
|
|
126
140
|
ssl_context = ssl.create_default_context(
|
|
127
|
-
cafile=certifi.where()
|
|
141
|
+
cafile=certifi.where(),
|
|
128
142
|
) # 使用 certifi 提供的 CA 证书
|
|
129
143
|
connector = aiohttp.TCPConnector(ssl=ssl_context)
|
|
130
144
|
async with aiohttp.ClientSession(
|
|
131
|
-
trust_env=True,
|
|
145
|
+
trust_env=True,
|
|
146
|
+
connector=connector,
|
|
132
147
|
) as session:
|
|
133
148
|
async with session.get(url, timeout=1800) as resp:
|
|
134
149
|
if resp.status != 200:
|
|
@@ -146,16 +161,30 @@ async def download_file(url: str, path: str, show_progress: bool = False):
|
|
|
146
161
|
f.write(chunk)
|
|
147
162
|
downloaded_size += len(chunk)
|
|
148
163
|
if show_progress:
|
|
149
|
-
elapsed_time =
|
|
164
|
+
elapsed_time = (
|
|
165
|
+
time.time() - start_time
|
|
166
|
+
if time.time() - start_time > 0
|
|
167
|
+
else 1
|
|
168
|
+
)
|
|
150
169
|
speed = downloaded_size / 1024 / elapsed_time # KB/s
|
|
151
170
|
print(
|
|
152
171
|
f"\r下载进度: {downloaded_size / total_size:.2%} 速度: {speed:.2f} KB/s",
|
|
153
172
|
end="",
|
|
154
173
|
)
|
|
155
174
|
except (aiohttp.ClientConnectorSSLError, aiohttp.ClientConnectorCertificateError):
|
|
156
|
-
# 关闭SSL
|
|
175
|
+
# 关闭SSL验证(仅在证书验证失败时作为fallback)
|
|
176
|
+
logger.warning(
|
|
177
|
+
"SSL 证书验证失败,已关闭 SSL 验证(不安全,仅用于临时下载)。请检查目标服务器的证书配置。"
|
|
178
|
+
)
|
|
179
|
+
logger.warning(
|
|
180
|
+
f"SSL certificate verification failed for {url}. "
|
|
181
|
+
"Falling back to unverified connection (CERT_NONE). "
|
|
182
|
+
"This is insecure and exposes the application to man-in-the-middle attacks. "
|
|
183
|
+
"Please investigate certificate issues with the remote server."
|
|
184
|
+
)
|
|
157
185
|
ssl_context = ssl.create_default_context()
|
|
158
|
-
ssl_context.
|
|
186
|
+
ssl_context.check_hostname = False
|
|
187
|
+
ssl_context.verify_mode = ssl.CERT_NONE
|
|
159
188
|
async with aiohttp.ClientSession() as session:
|
|
160
189
|
async with session.get(url, ssl=ssl_context, timeout=120) as resp:
|
|
161
190
|
total_size = int(resp.headers.get("content-length", 0))
|
|
@@ -201,28 +230,58 @@ def get_local_ip_addresses():
|
|
|
201
230
|
|
|
202
231
|
|
|
203
232
|
async def get_dashboard_version():
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
233
|
+
dist_dir = os.path.join(get_astrbot_data_path(), "dist")
|
|
234
|
+
if os.path.exists(dist_dir):
|
|
235
|
+
version_file = os.path.join(dist_dir, "assets", "version")
|
|
236
|
+
if os.path.exists(version_file):
|
|
237
|
+
with open(version_file, encoding="utf-8") as f:
|
|
207
238
|
v = f.read().strip()
|
|
208
239
|
return v
|
|
209
240
|
return None
|
|
210
241
|
|
|
211
242
|
|
|
212
|
-
async def download_dashboard(
|
|
243
|
+
async def download_dashboard(
|
|
244
|
+
path: str | None = None,
|
|
245
|
+
extract_path: str = "data",
|
|
246
|
+
latest: bool = True,
|
|
247
|
+
version: str | None = None,
|
|
248
|
+
proxy: str | None = None,
|
|
249
|
+
) -> None:
|
|
213
250
|
"""下载管理面板文件"""
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
dashboard_release_url, path, show_progress=True
|
|
251
|
+
if path is None:
|
|
252
|
+
zip_path = Path(get_astrbot_data_path()).absolute() / "dashboard.zip"
|
|
253
|
+
else:
|
|
254
|
+
zip_path = Path(path).absolute()
|
|
255
|
+
|
|
256
|
+
if latest or len(str(version)) != 40:
|
|
257
|
+
ver_name = "latest" if latest else version
|
|
258
|
+
dashboard_release_url = f"https://astrbot-registry.soulter.top/download/astrbot-dashboard/{ver_name}/dist.zip"
|
|
259
|
+
logger.info(
|
|
260
|
+
f"准备下载指定发行版本的 AstrBot WebUI 文件: {dashboard_release_url}",
|
|
225
261
|
)
|
|
226
|
-
|
|
227
|
-
|
|
262
|
+
try:
|
|
263
|
+
await download_file(
|
|
264
|
+
dashboard_release_url,
|
|
265
|
+
str(zip_path),
|
|
266
|
+
show_progress=True,
|
|
267
|
+
)
|
|
268
|
+
except BaseException as _:
|
|
269
|
+
if latest:
|
|
270
|
+
dashboard_release_url = "https://github.com/AstrBotDevs/AstrBot/releases/latest/download/dist.zip"
|
|
271
|
+
else:
|
|
272
|
+
dashboard_release_url = f"https://github.com/AstrBotDevs/AstrBot/releases/download/{version}/dist.zip"
|
|
273
|
+
if proxy:
|
|
274
|
+
dashboard_release_url = f"{proxy}/{dashboard_release_url}"
|
|
275
|
+
await download_file(
|
|
276
|
+
dashboard_release_url,
|
|
277
|
+
str(zip_path),
|
|
278
|
+
show_progress=True,
|
|
279
|
+
)
|
|
280
|
+
else:
|
|
281
|
+
url = f"https://github.com/AstrBotDevs/astrbot-release-harbour/releases/download/release-{version}/dist.zip"
|
|
282
|
+
logger.info(f"准备下载指定版本的 AstrBot WebUI: {url}")
|
|
283
|
+
if proxy:
|
|
284
|
+
url = f"{proxy}/{url}"
|
|
285
|
+
await download_file(url, str(zip_path), show_progress=True)
|
|
286
|
+
with zipfile.ZipFile(zip_path, "r") as z:
|
|
228
287
|
z.extractall(extract_path)
|
astrbot/core/utils/log_pipe.py
CHANGED
astrbot/core/utils/metrics.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import aiohttp
|
|
2
|
-
import sys
|
|
3
1
|
import os
|
|
4
2
|
import socket
|
|
3
|
+
import sys
|
|
5
4
|
import uuid
|
|
6
|
-
|
|
5
|
+
|
|
6
|
+
import aiohttp
|
|
7
|
+
|
|
7
8
|
from astrbot.core import db_helper, logger
|
|
9
|
+
from astrbot.core.config import VERSION
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class Metric:
|
|
@@ -21,7 +23,7 @@ class Metric:
|
|
|
21
23
|
|
|
22
24
|
if os.path.exists(id_file):
|
|
23
25
|
try:
|
|
24
|
-
with open(id_file
|
|
26
|
+
with open(id_file) as f:
|
|
25
27
|
Metric._iid_cache = f.read().strip()
|
|
26
28
|
return Metric._iid_cache
|
|
27
29
|
except Exception:
|
|
@@ -39,8 +41,7 @@ class Metric:
|
|
|
39
41
|
|
|
40
42
|
@staticmethod
|
|
41
43
|
async def upload(**kwargs):
|
|
42
|
-
"""
|
|
43
|
-
上传相关非敏感的指标以更好地了解 AstrBot 的使用情况。上传的指标不会包含任何有关消息文本、用户信息等敏感信息。
|
|
44
|
+
"""上传相关非敏感的指标以更好地了解 AstrBot 的使用情况。上传的指标不会包含任何有关消息文本、用户信息等敏感信息。
|
|
44
45
|
|
|
45
46
|
Powered by TickStats.
|
|
46
47
|
"""
|
|
@@ -58,12 +59,12 @@ class Metric:
|
|
|
58
59
|
pass
|
|
59
60
|
try:
|
|
60
61
|
if "adapter_name" in kwargs:
|
|
61
|
-
db_helper.
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
await db_helper.insert_platform_stats(
|
|
63
|
+
platform_id=kwargs["adapter_name"],
|
|
64
|
+
platform_type=kwargs.get("adapter_type", "unknown"),
|
|
65
|
+
)
|
|
64
66
|
except Exception as e:
|
|
65
67
|
logger.error(f"保存指标到数据库失败: {e}")
|
|
66
|
-
pass
|
|
67
68
|
|
|
68
69
|
try:
|
|
69
70
|
async with aiohttp.ClientSession(trust_env=True) as session:
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
|
|
3
|
+
from astrbot.core import astrbot_config, logger
|
|
4
|
+
from astrbot.core.astrbot_config_mgr import AstrBotConfig, AstrBotConfigManager
|
|
5
|
+
from astrbot.core.db.migration.migra_45_to_46 import migrate_45_to_46
|
|
6
|
+
from astrbot.core.db.migration.migra_webchat_session import migrate_webchat_session
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _migra_agent_runner_configs(conf: AstrBotConfig, ids_map: dict) -> None:
|
|
10
|
+
"""
|
|
11
|
+
Migra agent runner configs from provider configs.
|
|
12
|
+
"""
|
|
13
|
+
try:
|
|
14
|
+
default_prov_id = conf["provider_settings"]["default_provider_id"]
|
|
15
|
+
if default_prov_id in ids_map:
|
|
16
|
+
conf["provider_settings"]["default_provider_id"] = ""
|
|
17
|
+
p = ids_map[default_prov_id]
|
|
18
|
+
if p["type"] == "dify":
|
|
19
|
+
conf["provider_settings"]["dify_agent_runner_provider_id"] = p["id"]
|
|
20
|
+
conf["provider_settings"]["agent_runner_type"] = "dify"
|
|
21
|
+
elif p["type"] == "coze":
|
|
22
|
+
conf["provider_settings"]["coze_agent_runner_provider_id"] = p["id"]
|
|
23
|
+
conf["provider_settings"]["agent_runner_type"] = "coze"
|
|
24
|
+
elif p["type"] == "dashscope":
|
|
25
|
+
conf["provider_settings"]["dashscope_agent_runner_provider_id"] = p[
|
|
26
|
+
"id"
|
|
27
|
+
]
|
|
28
|
+
conf["provider_settings"]["agent_runner_type"] = "dashscope"
|
|
29
|
+
conf.save_config()
|
|
30
|
+
except Exception as e:
|
|
31
|
+
logger.error(f"Migration for third party agent runner configs failed: {e!s}")
|
|
32
|
+
logger.error(traceback.format_exc())
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def migra(
|
|
36
|
+
db, astrbot_config_mgr, umop_config_router, acm: AstrBotConfigManager
|
|
37
|
+
) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Stores the migration logic here.
|
|
40
|
+
btw, i really don't like migration :(
|
|
41
|
+
"""
|
|
42
|
+
# 4.5 to 4.6 migration for umop_config_router
|
|
43
|
+
try:
|
|
44
|
+
await migrate_45_to_46(astrbot_config_mgr, umop_config_router)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
logger.error(f"Migration from version 4.5 to 4.6 failed: {e!s}")
|
|
47
|
+
logger.error(traceback.format_exc())
|
|
48
|
+
|
|
49
|
+
# migration for webchat session
|
|
50
|
+
try:
|
|
51
|
+
await migrate_webchat_session(db)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.error(f"Migration for webchat session failed: {e!s}")
|
|
54
|
+
logger.error(traceback.format_exc())
|
|
55
|
+
|
|
56
|
+
# migra third party agent runner configs
|
|
57
|
+
_c = False
|
|
58
|
+
providers = astrbot_config["provider"]
|
|
59
|
+
ids_map = {}
|
|
60
|
+
for prov in providers:
|
|
61
|
+
type_ = prov.get("type")
|
|
62
|
+
if type_ in ["dify", "coze", "dashscope"]:
|
|
63
|
+
prov["provider_type"] = "agent_runner"
|
|
64
|
+
ids_map[prov["id"]] = {
|
|
65
|
+
"type": type_,
|
|
66
|
+
"id": prov["id"],
|
|
67
|
+
}
|
|
68
|
+
_c = True
|
|
69
|
+
if _c:
|
|
70
|
+
astrbot_config.save_config()
|
|
71
|
+
|
|
72
|
+
for conf in acm.confs.values():
|
|
73
|
+
_migra_agent_runner_configs(conf, ids_map)
|
astrbot/core/utils/path_util.py
CHANGED
|
@@ -1,70 +1,71 @@
|
|
|
1
1
|
import os
|
|
2
|
+
|
|
2
3
|
from astrbot.core import logger
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
5
|
+
|
|
6
|
+
def path_Mapping(mappings, srcPath: str) -> str:
|
|
7
|
+
"""路径映射处理函数。尝试支援 Windows 和 Linux 的路径映射。
|
|
8
|
+
Args:
|
|
9
|
+
mappings: 映射规则列表
|
|
10
|
+
srcPath: 原路径
|
|
11
|
+
Returns:
|
|
12
|
+
str: 处理后的路径
|
|
13
|
+
"""
|
|
14
|
+
for mapping in mappings:
|
|
15
|
+
rule = mapping.split(":")
|
|
16
|
+
if len(rule) == 2:
|
|
17
|
+
from_, to_ = mapping.split(":")
|
|
18
|
+
elif len(rule) > 4 or len(rule) == 1:
|
|
19
|
+
# 切割后大于4个项目,或者只有1个项目,那肯定是错误的,只能是2,3,4个项目
|
|
20
|
+
logger.warning(f"路径映射规则错误: {mapping}")
|
|
21
|
+
continue
|
|
22
|
+
# rule.len == 3 or 4
|
|
23
|
+
elif os.path.exists(rule[0] + ":" + rule[1]):
|
|
24
|
+
# 前面两个项目合并路径存在,说明是本地Window路径。后面一个或两个项目组成的路径本地大概率无法解析,直接拼接
|
|
25
|
+
from_ = rule[0] + ":" + rule[1]
|
|
26
|
+
if len(rule) == 3:
|
|
27
|
+
to_ = rule[2]
|
|
28
|
+
else:
|
|
29
|
+
to_ = rule[2] + ":" + rule[3]
|
|
30
|
+
else:
|
|
31
|
+
# 前面两个项目合并路径不存在,说明第一个项目是本地Linux路径,后面一个或两个项目直接拼接。
|
|
32
|
+
from_ = rule[0]
|
|
33
|
+
if len(rule) == 3:
|
|
34
|
+
to_ = rule[1] + ":" + rule[2]
|
|
35
|
+
else:
|
|
36
|
+
# 这种情况下存在四个项目,说明规则也是错误的
|
|
18
37
|
logger.warning(f"路径映射规则错误: {mapping}")
|
|
19
38
|
continue
|
|
20
|
-
else:
|
|
21
|
-
# rule.len == 3 or 4
|
|
22
|
-
if(os.path.exists(rule[0]+":"+rule[1])):
|
|
23
|
-
# 前面两个项目合并路径存在,说明是本地Window路径。后面一个或两个项目组成的路径本地大概率无法解析,直接拼接
|
|
24
|
-
from_ = rule[0] + ":" + rule[1]
|
|
25
|
-
if len(rule) == 3:
|
|
26
|
-
to_ = rule[2]
|
|
27
|
-
else:
|
|
28
|
-
to_ = rule[2] + ":" + rule[3]
|
|
29
|
-
else:
|
|
30
|
-
# 前面两个项目合并路径不存在,说明第一个项目是本地Linux路径,后面一个或两个项目直接拼接。
|
|
31
|
-
from_ = rule[0]
|
|
32
|
-
if len(rule) == 3:
|
|
33
|
-
to_ = rule[1] + ":" + rule[2]
|
|
34
|
-
else:
|
|
35
|
-
# 这种情况下存在四个项目,说明规则也是错误的
|
|
36
|
-
logger.warning(f"路径映射规则错误: {mapping}")
|
|
37
|
-
continue
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
from_ = from_.removesuffix("/")
|
|
41
|
+
from_ = from_.removesuffix("\\")
|
|
42
|
+
to_ = to_.removesuffix("/")
|
|
43
|
+
to_ = to_.removesuffix("\\")
|
|
44
|
+
# logger.debug(f"\t路径映射-规则(处理): {from_} -> {to_}")
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
srcPath = srcPath.replace("\\", "/")
|
|
61
|
-
has_replaced_processed = True
|
|
62
|
-
elif sign == "\\":
|
|
63
|
-
srcPath = srcPath.replace("/", "\\")
|
|
64
|
-
has_replaced_processed = True
|
|
65
|
-
if has_replaced_processed == False:
|
|
66
|
-
# 如果不是相对路径或不能处理,默认按照Linux路径处理
|
|
46
|
+
url = srcPath.removeprefix("file://")
|
|
47
|
+
if url.startswith(from_):
|
|
48
|
+
srcPath = url.replace(from_, to_, 1)
|
|
49
|
+
if ":" in srcPath:
|
|
50
|
+
# Windows路径处理
|
|
51
|
+
srcPath = srcPath.replace("/", "\\")
|
|
52
|
+
else:
|
|
53
|
+
has_replaced_processed = False
|
|
54
|
+
if srcPath.startswith("."):
|
|
55
|
+
# 相对路径处理。如果是相对路径,可能是Linux路径,也可能是Windows路径
|
|
56
|
+
sign = srcPath[1]
|
|
57
|
+
# 处理两个点的情况
|
|
58
|
+
if sign == ".":
|
|
59
|
+
sign = srcPath[2]
|
|
60
|
+
if sign == "/":
|
|
67
61
|
srcPath = srcPath.replace("\\", "/")
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
has_replaced_processed = True
|
|
63
|
+
elif sign == "\\":
|
|
64
|
+
srcPath = srcPath.replace("/", "\\")
|
|
65
|
+
has_replaced_processed = True
|
|
66
|
+
if not has_replaced_processed:
|
|
67
|
+
# 如果不是相对路径或不能处理,默认按照Linux路径处理
|
|
68
|
+
srcPath = srcPath.replace("\\", "/")
|
|
69
|
+
logger.info(f"路径映射: {url} -> {srcPath}")
|
|
70
|
+
return srcPath
|
|
71
|
+
return srcPath
|
|
@@ -1,19 +1,20 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import logging
|
|
2
|
-
|
|
3
|
+
import sys
|
|
3
4
|
|
|
4
5
|
logger = logging.getLogger("astrbot")
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class PipInstaller:
|
|
8
|
-
def __init__(self, pip_install_arg: str, pypi_index_url: str = None):
|
|
9
|
+
def __init__(self, pip_install_arg: str, pypi_index_url: str | None = None):
|
|
9
10
|
self.pip_install_arg = pip_install_arg
|
|
10
11
|
self.pypi_index_url = pypi_index_url
|
|
11
12
|
|
|
12
|
-
def install(
|
|
13
|
+
async def install(
|
|
13
14
|
self,
|
|
14
|
-
package_name: str = None,
|
|
15
|
-
requirements_path: str = None,
|
|
16
|
-
mirror: str = None,
|
|
15
|
+
package_name: str | None = None,
|
|
16
|
+
requirements_path: str | None = None,
|
|
17
|
+
mirror: str | None = None,
|
|
17
18
|
):
|
|
18
19
|
args = ["install"]
|
|
19
20
|
if package_name:
|
|
@@ -29,12 +30,33 @@ class PipInstaller:
|
|
|
29
30
|
args.extend(self.pip_install_arg.split())
|
|
30
31
|
|
|
31
32
|
logger.info(f"Pip 包管理器: pip {' '.join(args)}")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
try:
|
|
34
|
+
process = await asyncio.create_subprocess_exec(
|
|
35
|
+
sys.executable,
|
|
36
|
+
"-m",
|
|
37
|
+
"pip",
|
|
38
|
+
*args,
|
|
39
|
+
stdout=asyncio.subprocess.PIPE,
|
|
40
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
assert process.stdout is not None
|
|
44
|
+
async for line in process.stdout:
|
|
45
|
+
logger.info(line.decode().strip())
|
|
46
|
+
|
|
47
|
+
await process.wait()
|
|
48
|
+
|
|
49
|
+
if process.returncode != 0:
|
|
50
|
+
raise Exception(f"安装失败,错误码:{process.returncode}")
|
|
51
|
+
except FileNotFoundError:
|
|
52
|
+
# 没有 pip
|
|
53
|
+
from pip import main as pip_main
|
|
54
|
+
|
|
55
|
+
result_code = await asyncio.to_thread(pip_main, args)
|
|
56
|
+
|
|
57
|
+
# 清除 pip.main 导致的多余的 logging handlers
|
|
58
|
+
for handler in logging.root.handlers[:]:
|
|
59
|
+
logging.root.removeHandler(handler)
|
|
60
|
+
|
|
61
|
+
if result_code != 0:
|
|
62
|
+
raise Exception(f"安装失败,错误码:{result_code}")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SessionLockManager:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self._locks: dict[str, asyncio.Lock] = defaultdict(asyncio.Lock)
|
|
9
|
+
self._lock_count: dict[str, int] = defaultdict(int)
|
|
10
|
+
self._access_lock = asyncio.Lock()
|
|
11
|
+
|
|
12
|
+
@asynccontextmanager
|
|
13
|
+
async def acquire_lock(self, session_id: str):
|
|
14
|
+
async with self._access_lock:
|
|
15
|
+
lock = self._locks[session_id]
|
|
16
|
+
self._lock_count[session_id] += 1
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
async with lock:
|
|
20
|
+
yield
|
|
21
|
+
finally:
|
|
22
|
+
async with self._access_lock:
|
|
23
|
+
self._lock_count[session_id] -= 1
|
|
24
|
+
if self._lock_count[session_id] == 0:
|
|
25
|
+
self._locks.pop(session_id, None)
|
|
26
|
+
self._lock_count.pop(session_id, None)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
session_lock_manager = SessionLockManager()
|