diff --git a/astrbot/core/utils/http_ssl.py b/astrbot/core/utils/http_ssl.py new file mode 100644 index 000000000..ee0bbc0d0 --- /dev/null +++ b/astrbot/core/utils/http_ssl.py @@ -0,0 +1,33 @@ +import logging +import ssl +import threading + +import aiohttp + +from astrbot.utils.http_ssl_common import ( + build_ssl_context_with_certifi as _build_ssl_context, +) + +logger = logging.getLogger("astrbot") + +_SHARED_TLS_CONTEXT: ssl.SSLContext | None = None +_SHARED_TLS_CONTEXT_LOCK = threading.Lock() + + +def build_ssl_context_with_certifi() -> ssl.SSLContext: + """Build an SSL context from system trust store and add certifi CAs.""" + global _SHARED_TLS_CONTEXT + + if _SHARED_TLS_CONTEXT is not None: + return _SHARED_TLS_CONTEXT + + with _SHARED_TLS_CONTEXT_LOCK: + if _SHARED_TLS_CONTEXT is not None: + return _SHARED_TLS_CONTEXT + + _SHARED_TLS_CONTEXT = _build_ssl_context(log_obj=logger) + return _SHARED_TLS_CONTEXT + + +def build_tls_connector() -> aiohttp.TCPConnector: + return aiohttp.TCPConnector(ssl=build_ssl_context_with_certifi()) diff --git a/astrbot/core/utils/llm_metadata.py b/astrbot/core/utils/llm_metadata.py index 915d8d8f9..ef88e9490 100644 --- a/astrbot/core/utils/llm_metadata.py +++ b/astrbot/core/utils/llm_metadata.py @@ -3,6 +3,7 @@ import aiohttp from astrbot.core import logger +from astrbot.core.utils.http_ssl import build_tls_connector class LLMModalities(TypedDict): @@ -32,7 +33,9 @@ class LLMMetadata(TypedDict): async def update_llm_metadata() -> None: url = "https://models.dev/api.json" try: - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession( + trust_env=True, connector=build_tls_connector() + ) as session: async with session.get(url) as response: data = await response.json() global LLM_METADATAS diff --git a/astrbot/core/utils/pip_installer.py b/astrbot/core/utils/pip_installer.py index d9a84f3ed..6be259a1d 100644 --- a/astrbot/core/utils/pip_installer.py +++ b/astrbot/core/utils/pip_installer.py @@ -11,6 +11,8 @@ logger = logging.getLogger("astrbot") +_DISTLIB_FINDER_PATCH_ATTEMPTED = False + def _get_pip_main(): try: @@ -47,6 +49,110 @@ def _cleanup_added_root_handlers(original_handlers: list[logging.Handler]) -> No handler.close() +def _get_loader_for_package(package: object) -> object | None: + loader = getattr(package, "__loader__", None) + if loader is not None: + return loader + + spec = getattr(package, "__spec__", None) + if spec is None: + return None + return getattr(spec, "loader", None) + + +def _try_register_distlib_finder( + distlib_resources: object, + finder_registry: dict[type, object], + register_finder, + resource_finder: object, + loader: object, + package_name: str, +) -> bool: + loader_type = type(loader) + if loader_type in finder_registry: + return False + + try: + register_finder(loader_type, resource_finder) + except Exception as exc: + logger.warning( + "Failed to patch pip distlib finder for loader %s (%s): %s", + loader_type.__name__, + package_name, + exc, + ) + return False + + updated_registry = getattr(distlib_resources, "_finder_registry", finder_registry) + if isinstance(updated_registry, dict) and loader_type not in updated_registry: + logger.warning( + "Distlib finder patch did not take effect for loader %s (%s).", + loader_type.__name__, + package_name, + ) + return False + + logger.info( + "Patched pip distlib finder for frozen loader: %s (%s)", + loader_type.__name__, + package_name, + ) + return True + + +def _patch_distlib_finder_for_frozen_runtime() -> None: + global _DISTLIB_FINDER_PATCH_ATTEMPTED + + if not getattr(sys, "frozen", False): + return + if _DISTLIB_FINDER_PATCH_ATTEMPTED: + return + + _DISTLIB_FINDER_PATCH_ATTEMPTED = True + + try: + from pip._vendor.distlib import resources as distlib_resources + except Exception: + return + + finder_registry = getattr(distlib_resources, "_finder_registry", None) + register_finder = getattr(distlib_resources, "register_finder", None) + resource_finder = getattr(distlib_resources, "ResourceFinder", None) + + if not isinstance(finder_registry, dict): + logger.warning( + "Skip patching distlib finder because _finder_registry is unavailable." + ) + return + if not callable(register_finder) or resource_finder is None: + logger.warning( + "Skip patching distlib finder because register API is unavailable." + ) + return + + for package_name in ("pip._vendor.distlib", "pip._vendor"): + try: + package = importlib.import_module(package_name) + except Exception: + continue + + loader = _get_loader_for_package(package) + if loader is None: + continue + + if _try_register_distlib_finder( + distlib_resources, + finder_registry, + register_finder, + resource_finder, + loader, + package_name, + ): + finder_registry = getattr( + distlib_resources, "_finder_registry", finder_registry + ) + + class PipInstaller: def __init__(self, pip_install_arg: str, pypi_index_url: str | None = None) -> None: self.pip_install_arg = pip_install_arg @@ -88,6 +194,7 @@ async def install( async def _run_pip_in_process(self, args: list[str]) -> int: pip_main = _get_pip_main() + _patch_distlib_finder_for_frozen_runtime() original_handlers = list(logging.getLogger().handlers) result_code, output = await asyncio.to_thread( diff --git a/astrbot/core/utils/t2i/network_strategy.py b/astrbot/core/utils/t2i/network_strategy.py index 2abb22917..53d9441fa 100644 --- a/astrbot/core/utils/t2i/network_strategy.py +++ b/astrbot/core/utils/t2i/network_strategy.py @@ -1,12 +1,11 @@ import asyncio import logging import random -import ssl import aiohttp -import certifi from astrbot.core.config import VERSION +from astrbot.core.utils.http_ssl import build_tls_connector from astrbot.core.utils.io import download_image_by_url from astrbot.core.utils.t2i.template_manager import TemplateManager @@ -39,7 +38,10 @@ async def get_template(self, name: str = "base") -> str: async def get_official_endpoints(self) -> None: """获取官方的 t2i 端点列表。""" try: - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession( + trust_env=True, + connector=build_tls_connector(), + ) as session: async with session.get( "https://api.soulter.top/astrbot/t2i-endpoints", ) as resp: @@ -88,12 +90,10 @@ async def render_custom_template( for endpoint in endpoints: try: if return_url: - ssl_context = ssl.create_default_context(cafile=certifi.where()) - connector = aiohttp.TCPConnector(ssl=ssl_context) async with ( aiohttp.ClientSession( trust_env=True, - connector=connector, + connector=build_tls_connector(), ) as session, session.post( f"{endpoint}/generate", diff --git a/astrbot/utils/__init__.py b/astrbot/utils/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/astrbot/utils/__init__.py @@ -0,0 +1 @@ + diff --git a/astrbot/utils/http_ssl_common.py b/astrbot/utils/http_ssl_common.py new file mode 100644 index 000000000..b379fd36e --- /dev/null +++ b/astrbot/utils/http_ssl_common.py @@ -0,0 +1,24 @@ +import logging +import ssl +from typing import Any + +import certifi + +_LOGGER = logging.getLogger(__name__) + + +def build_ssl_context_with_certifi(log_obj: Any | None = None) -> ssl.SSLContext: + logger = log_obj or _LOGGER + + ssl_context = ssl.create_default_context() + try: + ssl_context.load_verify_locations(cafile=certifi.where()) + except Exception as exc: + if logger and hasattr(logger, "warning"): + logger.warning( + "Failed to load certifi CA bundle into SSL context; " + "falling back to system trust store only: %s", + exc, + ) + + return ssl_context diff --git a/dashboard/src/components/chat/ConversationSidebar.vue b/dashboard/src/components/chat/ConversationSidebar.vue index fe25ef34c..a728930d9 100644 --- a/dashboard/src/components/chat/ConversationSidebar.vue +++ b/dashboard/src/components/chat/ConversationSidebar.vue @@ -144,6 +144,7 @@ import { ref } from 'vue'; import { useI18n, useModuleI18n } from '@/i18n/composables'; import type { Session } from '@/composables/useSessions'; +import { askForConfirmation, useConfirmDialog } from '@/utils/confirmDialog'; import LanguageSwitcher from '@/components/shared/LanguageSwitcher.vue'; import StyledMenu from '@/components/shared/StyledMenu.vue'; import ProviderConfigDialog from '@/components/chat/ProviderConfigDialog.vue'; @@ -183,6 +184,8 @@ const emit = defineEmits<{ const { t } = useI18n(); const { tm } = useModuleI18n('features/chat'); +const confirmDialog = useConfirmDialog(); + const sidebarCollapsed = ref(true); const showProviderConfigDialog = ref(false); @@ -199,10 +202,10 @@ function toggleSidebar() { localStorage.setItem('sidebarCollapsed', JSON.stringify(sidebarCollapsed.value)); } -function handleDeleteConversation(session: Session) { +async function handleDeleteConversation(session: Session) { const sessionTitle = session.display_name || tm('conversation.newConversation'); const message = tm('conversation.confirmDelete', { name: sessionTitle }); - if (window.confirm(message)) { + if (await askForConfirmation(message, confirmDialog)) { emit('deleteConversation', session.session_id); } } @@ -359,4 +362,3 @@ function handleDeleteConversation(session: Session) { justify-content: center; } - diff --git a/dashboard/src/components/chat/ProjectList.vue b/dashboard/src/components/chat/ProjectList.vue index e9ac1f5de..34491c529 100644 --- a/dashboard/src/components/chat/ProjectList.vue +++ b/dashboard/src/components/chat/ProjectList.vue @@ -42,8 +42,9 @@