fix: stabilize packaged runtime pip/ssl behavior and mac font fallback (#5007)

* fix: patch pip distlib finder for frozen electron runtime

* fix: use certifi CA bundle for runtime SSL requests

* fix: configure certifi CA before core imports

* fix: improve mac font fallback for dashboard text

* fix: harden frozen pip patch and unify TLS connector

* refactor: centralize dashboard CJK font fallback stacks

* perf: reuse TLS context and avoid repeated frozen pip patch

* refactor: bootstrap TLS setup before core imports

* fix: use async confirm dialog for provider deletions

* fix: replace native confirm dialogs in dashboard

- Add shared confirm helper in dashboard/src/utils/confirmDialog.ts for async dialog usage with safe fallback.

- Migrate provider, chat, config, session, platform, persona, MCP, backup, and knowledge-base delete/close confirmations to use the shared helper.

- Remove scattered inline confirm handling to keep behavior consistent and avoid native blocking dialog focus/caret issues in Electron.

* fix: capture runtime bootstrap logs after logger init

- Add bootstrap record buffer in runtime_bootstrap for early TLS patch logs before logger is ready.

- Flush buffered bootstrap logs to astrbot logger at process startup in main.py.

- Include concrete exception details for TLS bootstrap failures to improve diagnosis.

* fix: harden runtime bootstrap and unify confirm handling

- Simplify bootstrap log buffering and add a public initialize hook for non-main startup paths.

- Guard aiohttp TLS patching with feature/type checks and keep graceful fallback when internals are unavailable.

- Standardize dashboard confirmation flow via shared confirm helpers across composition and options API components.

* refactor: simplify runtime tls bootstrap and tighten confirm typing

* refactor: align ssl helper namespace and confirm usage
This commit is contained in:
エイカク
2026-02-10 16:42:43 +09:00
committed by GitHub
parent 7a4d20d329
commit d35771f97d
27 changed files with 442 additions and 67 deletions
+33
View File
@@ -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())
+4 -1
View File
@@ -3,6 +3,7 @@ from typing import Literal, TypedDict
import aiohttp
from astrbot.core import logger
from astrbot.core.utils.http_ssl import build_tls_connector
class LLMModalities(TypedDict):
@@ -32,7 +33,9 @@ LLM_METADATAS: dict[str, LLMMetadata] = {}
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
+107
View File
@@ -11,6 +11,8 @@ from astrbot.core.utils.runtime_env import is_packaged_electron_runtime
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 @@ class PipInstaller:
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(
+6 -6
View File
@@ -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 @@ class NetworkRenderStrategy(RenderStrategy):
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 @@ class NetworkRenderStrategy(RenderStrategy):
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",
+1
View File
@@ -0,0 +1 @@
+24
View File
@@ -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
@@ -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;
}
</style>
@@ -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;
@@ -72,6 +73,8 @@ const emit = defineEmits<{
const { tm } = useModuleI18n('features/chat');
const confirmDialog = useConfirmDialog();
const expanded = ref(props.initialExpanded);
// 从 localStorage 读取项目展开状态
@@ -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);
}
}
@@ -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;
@@ -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);
}
}
@@ -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',
@@ -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 {
@@ -382,9 +387,13 @@ 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 }))) {
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();
@@ -393,7 +402,6 @@ export default {
.catch(error => {
this.showError(this.tm('messages.deleteError', { error: error.response?.data?.message || error.message }));
});
}
},
editServer(server) {
const configCopy = { ...server };
@@ -370,10 +370,13 @@
import { ref, computed, watch } from 'vue'
import axios from 'axios'
import { useI18n } from '@/i18n/composables'
import { askForConfirmation, useConfirmDialog } from '@/utils/confirmDialog'
import WaitingForRestart from './WaitingForRestart.vue'
const { t } = useI18n()
const confirmDialog = useConfirmDialog()
const isOpen = ref(false)
const activeTab = ref('export')
const wfr = ref(null)
@@ -844,7 +847,7 @@ const restoreFromList = async (filename) => {
// 删除备份
const deleteBackup = async (filename) => {
if (!confirm(t('features.settings.backup.list.confirmDelete'))) return
if (!(await askForConfirmation(t('features.settings.backup.list.confirmDelete'), confirmDialog))) return
try {
const response = await axios.post('/api/backup/delete', { filename })
@@ -269,8 +269,8 @@ export default {
}
}
span.style = style + 'display: block; font-size: 12px; font-family: Consolas, monospace; white-space: pre-wrap; margin-bottom: 2px;'
span.classList.add('fade-in')
span.style = style
span.classList.add('console-log-line', 'fade-in')
span.innerText = `${log}`;
ele.appendChild(span)
if (this.autoScroll) {
@@ -290,7 +290,15 @@ export default {
margin-left: 20px;
}
.fade-in {
:deep(.console-log-line) {
display: block;
margin-bottom: 2px;
font-family: SFMono-Regular, Menlo, Monaco, Consolas, var(--astrbot-font-cjk-mono), monospace;
font-size: 12px;
white-space: pre-wrap;
}
:deep(.fade-in) {
animation: fadeIn 0.3s;
}
@@ -307,6 +307,10 @@
<script>
import axios from 'axios';
import { useModuleI18n } from '@/i18n/composables';
import {
askForConfirmation as askForConfirmationDialog,
useConfirmDialog
} from '@/utils/confirmDialog';
export default {
name: 'PersonaForm',
@@ -331,7 +335,8 @@ export default {
emits: ['update:modelValue', 'saved', 'error', 'deleted'],
setup() {
const { tm } = useModuleI18n('features/persona');
return { tm };
const confirmDialog = useConfirmDialog();
return { tm, confirmDialog };
},
data() {
return {
@@ -597,7 +602,12 @@ export default {
async deletePersona() {
if (!this.editingPersona) return;
if (!confirm(this.tm('messages.deleteConfirm', { id: this.editingPersona.persona_id }))) {
if (
!(await askForConfirmationDialog(
this.tm('messages.deleteConfirm', { id: this.editingPersona.persona_id }),
this.confirmDialog,
))
) {
return;
}
@@ -1,6 +1,7 @@
import { ref, computed, onMounted, nextTick, watch } from 'vue'
import axios from 'axios'
import { getProviderIcon } from '@/utils/providerUtils'
import { askForConfirmation as askForConfirmationDialog, useConfirmDialog } from '@/utils/confirmDialog'
export interface UseProviderSourcesOptions {
defaultTab?: string
@@ -37,6 +38,12 @@ export function resolveDefaultTab(value?: string) {
export function useProviderSources(options: UseProviderSourcesOptions) {
const { tm, showMessage } = options
const confirmDialog = useConfirmDialog()
async function askForConfirmation(message: string) {
return askForConfirmationDialog(message, confirmDialog)
}
// ===== State =====
const config = ref<Record<string, any>>({})
const metadata = ref<Record<string, any>>({})
@@ -396,7 +403,10 @@ export function useProviderSources(options: UseProviderSourcesOptions) {
}
async function deleteProviderSource(source: any) {
if (!confirm(tm('providerSources.deleteConfirm', { id: source.id }))) return
const confirmed = await askForConfirmation(
tm('providerSources.deleteConfirm', { id: source.id })
)
if (!confirmed) return
try {
await axios.post('/api/config/provider_sources/delete', { id: source.id })
@@ -558,7 +568,8 @@ export function useProviderSources(options: UseProviderSourcesOptions) {
}
async function deleteProvider(provider: any) {
if (!confirm(tm('models.deleteConfirm', { id: provider.id }))) return
const confirmed = await askForConfirmation(tm('models.deleteConfirm', { id: provider.id }))
if (!confirmed) return
try {
await axios.post('/api/config/provider/delete', { id: provider.id })
+9 -1
View File
@@ -9,7 +9,15 @@ $color-pack: false;
// Global font size and border radius
$font-size-root: 1rem;
$border-radius-root: 8px;
$body-font-family: 'Roboto', sans-serif !default;
$cjk-sans-fallback: 'PingFang SC', 'Hiragino Sans GB', 'Noto Sans CJK SC', 'Microsoft YaHei' !default;
$cjk-mono-fallback: 'PingFang SC', 'PingFang TC', 'Hiragino Sans GB', 'Noto Sans CJK SC', 'Microsoft YaHei' !default;
:root {
--astrbot-font-cjk-sans: #{$cjk-sans-fallback};
--astrbot-font-cjk-mono: #{$cjk-mono-fallback};
}
$body-font-family: 'Roboto', $cjk-sans-fallback, sans-serif !default;
$heading-font-family: $body-font-family !default;
$btn-font-weight: 400 !default;
$btn-letter-spacing: 0 !default;
+3 -3
View File
@@ -85,15 +85,15 @@ $sizes: (
body {
.Poppins {
font-family: 'Poppins', sans-serif !important;
font-family: 'Poppins', $cjk-sans-fallback, sans-serif !important;
}
.Inter {
font-family: 'Inter', sans-serif !important;
font-family: 'Inter', $cjk-sans-fallback, sans-serif !important;
}
.Outfit {
font-family: 'Outfit', sans-serif !important;
font-family: 'Outfit', $cjk-sans-fallback, sans-serif !important;
}
}
+11
View File
@@ -0,0 +1,11 @@
import 'vue'
import type { ConfirmDialogHandler } from '@/utils/confirmDialog'
declare module 'vue' {
interface ComponentCustomProperties {
$confirm?: ConfirmDialogHandler
}
}
export {}
+31
View File
@@ -0,0 +1,31 @@
import { inject } from 'vue'
export type ConfirmDialogOptions = {
title?: string
message?: string
}
export type ConfirmDialogHandler = (options: ConfirmDialogOptions) => Promise<boolean>
export type ConfirmDialogCandidate = ConfirmDialogHandler | null | undefined
export function useConfirmDialog(): ConfirmDialogHandler | undefined {
return inject<ConfirmDialogHandler | undefined>('$confirm', undefined)
}
export async function askForConfirmation(
message: string,
candidate?: ConfirmDialogCandidate
): Promise<boolean> {
const confirmDialog = candidate ?? undefined
if (confirmDialog) {
try {
return await confirmDialog({ message })
} catch {
return false
}
}
return window.confirm(message)
}
+10 -3
View File
@@ -190,6 +190,10 @@ import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
import StandaloneChat from '@/components/chat/StandaloneChat.vue';
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
import { useI18n, useModuleI18n } from '@/i18n/composables';
import {
askForConfirmation as askForConfirmationDialog,
useConfirmDialog
} from '@/utils/confirmDialog';
export default {
name: 'ConfigPage',
@@ -208,10 +212,12 @@ export default {
setup() {
const { t } = useI18n();
const { tm } = useModuleI18n('features/config');
const confirmDialog = useConfirmDialog();
return {
t,
tm
tm,
confirmDialog
};
},
@@ -473,8 +479,9 @@ export default {
this.createNewConfig();
}
},
confirmDeleteConfig(config) {
if (confirm(this.tm('configManagement.confirmDelete').replace('{name}', config.name))) {
async confirmDeleteConfig(config) {
const message = this.tm('configManagement.confirmDelete').replace('{name}', config.name);
if (await askForConfirmationDialog(message, this.confirmDialog)) {
this.deleteConfig(config.id);
}
},
+9 -3
View File
@@ -333,6 +333,10 @@ import { useCommonStore } from '@/stores/common';
import { useCustomizerStore } from '@/stores/customizer';
import { useI18n, useModuleI18n } from '@/i18n/composables';
import MessageList from '@/components/chat/MessageList.vue';
import {
askForConfirmation as askForConfirmationDialog,
useConfirmDialog
} from '@/utils/confirmDialog';
export default {
name: 'ConversationPage',
@@ -345,12 +349,14 @@ export default {
const { t, locale } = useI18n();
const { tm } = useModuleI18n('features/conversation');
const customizerStore = useCustomizerStore();
const confirmDialog = useConfirmDialog();
return {
t,
tm,
locale,
customizerStore
customizerStore,
confirmDialog
};
},
@@ -744,9 +750,9 @@ export default {
},
//
closeHistoryDialog() {
async closeHistoryDialog() {
if (this.isEditingHistory) {
if (confirm(this.tm('dialogs.view.confirmClose'))) {
if (await askForConfirmationDialog(this.tm('dialogs.view.confirmClose'), this.confirmDialog)) {
this.dialogView = false;
}
} else {
+13 -4
View File
@@ -197,6 +197,10 @@ import AddNewPlatform from '@/components/platform/AddNewPlatform.vue';
import { useCommonStore } from '@/stores/common';
import { useI18n, useModuleI18n } from '@/i18n/composables';
import { getPlatformIcon, getTutorialLink } from '@/utils/platformUtils';
import {
askForConfirmation as askForConfirmationDialog,
useConfirmDialog
} from '@/utils/confirmDialog';
export default {
name: 'PlatformPage',
@@ -210,10 +214,12 @@ export default {
setup() {
const { t } = useI18n();
const { tm } = useModuleI18n('features/platform');
const confirmDialog = useConfirmDialog();
return {
t,
tm
tm,
confirmDialog
};
},
data() {
@@ -451,15 +457,18 @@ export default {
});
},
deletePlatform(platform) {
if (confirm(`${this.messages.deleteConfirm} ${platform.id}?`)) {
async deletePlatform(platform) {
const message = `${this.messages.deleteConfirm} ${platform.id}?`;
if (!(await askForConfirmationDialog(message, this.confirmDialog))) {
return;
}
axios.post('/api/config/platform/delete', { id: platform.id }).then((res) => {
this.getConfig();
this.showSuccess(res.data.message || this.messages.deleteSuccess);
}).catch((err) => {
this.showError(err.response?.data?.message || err.message);
});
}
},
platformStatusChange(platform) {
@@ -522,16 +522,22 @@
<script>
import axios from 'axios'
import { useI18n, useModuleI18n } from '@/i18n/composables'
import {
askForConfirmation as askForConfirmationDialog,
useConfirmDialog
} from '@/utils/confirmDialog'
export default {
name: 'SessionManagementPage',
setup() {
const { t } = useI18n()
const { tm } = useModuleI18n('features/session-management')
const confirmDialog = useConfirmDialog()
return {
t,
tm
tm,
confirmDialog
}
},
data() {
@@ -1503,7 +1509,8 @@ export default {
},
async deleteGroup(group) {
if (!confirm(`确定要删除分组 "${group.name}" 吗?`)) return
const message = `确定要删除分组 "${group.name}" 吗?`
if (!(await askForConfirmationDialog(message, this.confirmDialog))) return
try {
const response = await axios.post('/api/session/group/delete', { id: group.id })
@@ -244,10 +244,13 @@ import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import axios from 'axios'
import { useModuleI18n } from '@/i18n/composables'
import { askForConfirmation, useConfirmDialog } from '@/utils/confirmDialog'
const { tm: t } = useModuleI18n('features/knowledge-base/document')
const route = useRoute()
const confirmDialog = useConfirmDialog()
const kbId = ref(route.params.kbId as string)
const docId = ref(route.params.docId as string)
@@ -356,7 +359,7 @@ const viewChunk = (chunk: any) => {
//
const deleteChunk = async (chunk: any) => {
if (!confirm(t('chunks.deleteConfirm'))) return
if (!(await askForConfirmation(t('chunks.deleteConfirm'), confirmDialog))) return
try {
const response = await axios.post('/api/kb/chunk/delete', {
chunk_id: chunk.chunk_id,
+12 -2
View File
@@ -260,6 +260,10 @@ import PersonaCard from './PersonaCard.vue';
import PersonaForm from '@/components/shared/PersonaForm.vue';
import CreateFolderDialog from './CreateFolderDialog.vue';
import MoveToFolderDialog from './MoveToFolderDialog.vue';
import {
askForConfirmation as askForConfirmationDialog,
useConfirmDialog
} from '@/utils/confirmDialog';
import type { Folder, FolderTreeNode } from '@/components/folder/types';
@@ -294,7 +298,8 @@ export default defineComponent({
setup() {
const { t } = useI18n();
const { tm } = useModuleI18n('features/persona');
return { t, tm };
const confirmDialog = useConfirmDialog();
return { t, tm, confirmDialog };
},
data() {
return {
@@ -420,7 +425,12 @@ export default defineComponent({
},
async confirmDeletePersona(persona: Persona) {
if (!confirm(this.tm('messages.deleteConfirm', { id: persona.persona_id }))) {
if (
!(await askForConfirmationDialog(
this.tm('messages.deleteConfirm', { id: persona.persona_id }),
this.confirmDialog,
))
) {
return;
}
+10
View File
@@ -161,6 +161,16 @@ function createWindow() {
nodeIntegration: false,
sandbox: true,
preload: path.join(__dirname, 'preload.js'),
...(isMac
? {
defaultFontFamily: {
standard: 'PingFang SC',
sansSerif: 'PingFang SC',
serif: 'Songti SC',
monospace: 'SF Mono',
},
}
: {}),
},
});
+12 -5
View File
@@ -5,10 +5,14 @@ import os
import sys
from pathlib import Path
from astrbot.core import LogBroker, LogManager, db_helper, logger
from astrbot.core.config.default import VERSION
from astrbot.core.initial_loader import InitialLoader
from astrbot.core.utils.astrbot_path import (
import runtime_bootstrap
runtime_bootstrap.initialize_runtime_bootstrap()
from astrbot.core import LogBroker, LogManager, db_helper, logger # noqa: E402
from astrbot.core.config.default import VERSION # noqa: E402
from astrbot.core.initial_loader import InitialLoader # noqa: E402
from astrbot.core.utils.astrbot_path import ( # noqa: E402
get_astrbot_config_path,
get_astrbot_data_path,
get_astrbot_plugin_path,
@@ -16,7 +20,10 @@ from astrbot.core.utils.astrbot_path import (
get_astrbot_site_packages_path,
get_astrbot_temp_path,
)
from astrbot.core.utils.io import download_dashboard, get_dashboard_version
from astrbot.core.utils.io import ( # noqa: E402
download_dashboard,
get_dashboard_version,
)
# 将父目录添加到 sys.path
sys.path.append(Path(__file__).parent.as_posix())
+50
View File
@@ -0,0 +1,50 @@
import logging
import ssl
from typing import Any
import aiohttp.connector as aiohttp_connector
from astrbot.utils.http_ssl_common import build_ssl_context_with_certifi
logger = logging.getLogger(__name__)
def _try_patch_aiohttp_ssl_context(
ssl_context: ssl.SSLContext,
log_obj: Any | None = None,
) -> bool:
log = log_obj or logger
attr_name = "_SSL_CONTEXT_VERIFIED"
if not hasattr(aiohttp_connector, attr_name):
log.warning(
"aiohttp connector does not expose _SSL_CONTEXT_VERIFIED; skipped patch.",
)
return False
current_value = getattr(aiohttp_connector, attr_name, None)
if current_value is not None and not isinstance(current_value, ssl.SSLContext):
log.warning(
"aiohttp connector exposes _SSL_CONTEXT_VERIFIED with unexpected type; skipped patch.",
)
return False
setattr(aiohttp_connector, attr_name, ssl_context)
log.info("Configured aiohttp verified SSL context with system+certifi trust chain.")
return True
def configure_runtime_ca_bundle(log_obj: Any | None = None) -> bool:
log = log_obj or logger
try:
log.info("Bootstrapping runtime CA bundle.")
ssl_context = build_ssl_context_with_certifi(log_obj=log)
return _try_patch_aiohttp_ssl_context(ssl_context, log_obj=log)
except Exception as exc:
log.error("Failed to configure runtime CA bundle for aiohttp: %r", exc)
return False
def initialize_runtime_bootstrap(log_obj: Any | None = None) -> bool:
return configure_runtime_ca_bundle(log_obj=log_obj)