Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions astrbot/core/utils/http_ssl.py
Original file line number Diff line number Diff line change
@@ -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())
5 changes: 4 additions & 1 deletion astrbot/core/utils/llm_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import aiohttp

from astrbot.core import logger
from astrbot.core.utils.http_ssl import build_tls_connector


class LLMModalities(TypedDict):
Expand Down Expand Up @@ -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
Expand Down
107 changes: 107 additions & 0 deletions astrbot/core/utils/pip_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

logger = logging.getLogger("astrbot")

_DISTLIB_FINDER_PATCH_ATTEMPTED = False


def _get_pip_main():
try:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
12 changes: 6 additions & 6 deletions astrbot/core/utils/t2i/network_strategy.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions astrbot/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

24 changes: 24 additions & 0 deletions astrbot/utils/http_ssl_common.py
Original file line number Diff line number Diff line change
@@ -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
8 changes: 5 additions & 3 deletions dashboard/src/components/chat/ConversationSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);

Expand All @@ -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);
}
}
Expand Down Expand Up @@ -359,4 +362,3 @@ function handleDeleteConversation(session: Session) {
justify-content: center;
}
</style>

9 changes: 6 additions & 3 deletions dashboard/src/components/chat/ProjectList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@
</template>

<script setup lang="ts">
import { ref, watch } from 'vue';
import { ref } from 'vue';
import { useModuleI18n } from '@/i18n/composables';
import { askForConfirmation, useConfirmDialog } from '@/utils/confirmDialog';

export interface Project {
project_id: string;
Expand Down Expand Up @@ -72,6 +73,8 @@ const emit = defineEmits<{

const { tm } = useModuleI18n('features/chat');

const confirmDialog = useConfirmDialog();

const expanded = ref(props.initialExpanded);

// 从 localStorage 读取项目展开状态
Expand All @@ -85,9 +88,9 @@ function toggleExpanded() {
localStorage.setItem('projectsExpanded', JSON.stringify(expanded.value));
}

function handleDeleteProject(project: Project) {
async function handleDeleteProject(project: Project) {
const message = tm('project.confirmDelete', { title: project.title });
if (window.confirm(message)) {
if (await askForConfirmation(message, confirmDialog)) {
emit('deleteProject', project.project_id);
}
}
Expand Down
7 changes: 5 additions & 2 deletions dashboard/src/components/chat/ProjectView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
<script setup lang="ts">
import { useModuleI18n } from '@/i18n/composables';
import type { Project } from '@/components/chat/ProjectList.vue';
import { askForConfirmation, useConfirmDialog } from '@/utils/confirmDialog';

interface Session {
session_id: string;
Expand All @@ -69,14 +70,16 @@ const emit = defineEmits<{

const { tm } = useModuleI18n('features/chat');

const confirmDialog = useConfirmDialog();

function formatDate(dateString: string): string {
return new Date(dateString).toLocaleString();
}

function handleDeleteSession(session: Session) {
async function handleDeleteSession(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('deleteSession', session.session_id);
}
}
Expand Down
30 changes: 19 additions & 11 deletions dashboard/src/components/extension/McpServersSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ import axios from 'axios';
import { VueMonacoEditor } from '@guolao/vue-monaco-editor';
import ItemCard from '@/components/shared/ItemCard.vue';
import { useI18n, useModuleI18n } from '@/i18n/composables';
import {
askForConfirmation as askForConfirmationDialog,
useConfirmDialog
} from '@/utils/confirmDialog';

export default {
name: 'McpServersSection',
Expand All @@ -228,7 +232,8 @@ export default {
setup() {
const { t } = useI18n();
const { tm } = useModuleI18n('features/tooluse');
return { t, tm };
const confirmDialog = useConfirmDialog();
return { t, tm, confirmDialog };
},
data() {
return {
Expand Down Expand Up @@ -382,18 +387,21 @@ export default {
this.showError(this.tm('dialogs.addServer.errors.jsonParse', { error: e.message }));
}
},
deleteServer(server) {
async deleteServer(server) {
const serverName = server.name || server;
if (confirm(this.tm('dialogs.confirmDelete', { name: serverName }))) {
axios.post('/api/tools/mcp/delete', { name: serverName })
.then(response => {
this.getServers();
this.showSuccess(response.data.message || this.tm('messages.deleteSuccess'));
})
.catch(error => {
this.showError(this.tm('messages.deleteError', { error: error.response?.data?.message || error.message }));
});
const message = this.tm('dialogs.confirmDelete', { name: serverName });
if (!(await askForConfirmationDialog(message, this.confirmDialog))) {
return;
}

axios.post('/api/tools/mcp/delete', { name: serverName })
.then(response => {
this.getServers();
this.showSuccess(response.data.message || this.tm('messages.deleteSuccess'));
})
.catch(error => {
this.showError(this.tm('messages.deleteError', { error: error.response?.data?.message || error.message }));
});
},
editServer(server) {
const configCopy = { ...server };
Expand Down
Loading