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:
@@ -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())
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
import 'vue'
|
||||
|
||||
import type { ConfirmDialogHandler } from '@/utils/confirmDialog'
|
||||
|
||||
declare module 'vue' {
|
||||
interface ComponentCustomProperties {
|
||||
$confirm?: ConfirmDialogHandler
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user