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.
This commit is contained in:
邹永赫
2026-02-10 15:43:43 +09:00
parent 7e9d8599f1
commit 82c43f5b32
16 changed files with 99 additions and 70 deletions

View File

@@ -141,10 +141,10 @@
</template>
<script setup lang="ts">
import { inject, ref } from 'vue';
import { ref } from 'vue';
import { useI18n, useModuleI18n } from '@/i18n/composables';
import type { Session } from '@/composables/useSessions';
import { askForConfirmation, type ConfirmDialogHandler } from '@/utils/confirmDialog';
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';
@@ -184,7 +184,7 @@ const emit = defineEmits<{
const { t } = useI18n();
const { tm } = useModuleI18n('features/chat');
const confirmDialog = inject<ConfirmDialogHandler | undefined>('$confirm', undefined);
const confirmDialog = useConfirmDialog();
const sidebarCollapsed = ref(true);
const showProviderConfigDialog = ref(false);

View File

@@ -42,9 +42,9 @@
</template>
<script setup lang="ts">
import { inject, ref } from 'vue';
import { ref } from 'vue';
import { useModuleI18n } from '@/i18n/composables';
import { askForConfirmation, type ConfirmDialogHandler } from '@/utils/confirmDialog';
import { askForConfirmation, useConfirmDialog } from '@/utils/confirmDialog';
export interface Project {
project_id: string;
@@ -73,7 +73,7 @@ const emit = defineEmits<{
const { tm } = useModuleI18n('features/chat');
const confirmDialog = inject<ConfirmDialogHandler | undefined>('$confirm', undefined);
const confirmDialog = useConfirmDialog();
const expanded = ref(props.initialExpanded);

View File

@@ -45,10 +45,9 @@
</template>
<script setup lang="ts">
import { inject } from 'vue';
import { useModuleI18n } from '@/i18n/composables';
import type { Project } from '@/components/chat/ProjectList.vue';
import { askForConfirmation, type ConfirmDialogHandler } from '@/utils/confirmDialog';
import { askForConfirmation, useConfirmDialog } from '@/utils/confirmDialog';
interface Session {
session_id: string;
@@ -71,7 +70,7 @@ const emit = defineEmits<{
const { tm } = useModuleI18n('features/chat');
const confirmDialog = inject<ConfirmDialogHandler | undefined>('$confirm', undefined);
const confirmDialog = useConfirmDialog();
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleString();

View File

@@ -218,7 +218,7 @@ 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, resolveConfirmDialog } from '@/utils/confirmDialog';
import { askForConfirmation as askForConfirmationDialog } from '@/utils/confirmDialog';
export default {
name: 'McpServersSection',
@@ -386,7 +386,7 @@ export default {
async deleteServer(server) {
const serverName = server.name || server;
const message = this.tm('dialogs.confirmDelete', { name: serverName });
if (!(await askForConfirmationDialog(message, resolveConfirmDialog(this.$confirm)))) {
if (!(await askForConfirmationDialog(message, this.$confirm))) {
return;
}

View File

@@ -367,15 +367,15 @@
</template>
<script setup>
import { ref, computed, inject, watch } from 'vue'
import { ref, computed, watch } from 'vue'
import axios from 'axios'
import { useI18n } from '@/i18n/composables'
import { askForConfirmation, resolveConfirmDialog } from '@/utils/confirmDialog'
import { askForConfirmation, useConfirmDialog } from '@/utils/confirmDialog'
import WaitingForRestart from './WaitingForRestart.vue'
const { t } = useI18n()
const confirmDialog = resolveConfirmDialog(inject('$confirm', undefined))
const confirmDialog = useConfirmDialog()
const isOpen = ref(false)
const activeTab = ref('export')

View File

@@ -307,7 +307,7 @@
<script>
import axios from 'axios';
import { useModuleI18n } from '@/i18n/composables';
import { askForConfirmation as askForConfirmationDialog, resolveConfirmDialog } from '@/utils/confirmDialog';
import { askForConfirmation as askForConfirmationDialog } from '@/utils/confirmDialog';
export default {
name: 'PersonaForm',
@@ -598,7 +598,12 @@ export default {
async deletePersona() {
if (!this.editingPersona) return;
if (!(await askForConfirmationDialog(this.tm('messages.deleteConfirm', { id: this.editingPersona.persona_id }), resolveConfirmDialog(this.$confirm)))) {
if (
!(await askForConfirmationDialog(
this.tm('messages.deleteConfirm', { id: this.editingPersona.persona_id }),
this.$confirm,
))
) {
return;
}

View File

@@ -1,7 +1,7 @@
import { ref, computed, inject, onMounted, nextTick, watch } from 'vue'
import { ref, computed, onMounted, nextTick, watch } from 'vue'
import axios from 'axios'
import { getProviderIcon } from '@/utils/providerUtils'
import { askForConfirmation as askForConfirmationDialog, type ConfirmDialogHandler } from '@/utils/confirmDialog'
import { askForConfirmation as askForConfirmationDialog, useConfirmDialog } from '@/utils/confirmDialog'
export interface UseProviderSourcesOptions {
defaultTab?: string
@@ -38,7 +38,7 @@ export function resolveDefaultTab(value?: string) {
export function useProviderSources(options: UseProviderSourcesOptions) {
const { tm, showMessage } = options
const confirmDialog = inject<ConfirmDialogHandler | undefined>('$confirm', undefined)
const confirmDialog = useConfirmDialog()
async function askForConfirmation(message: string) {
return askForConfirmationDialog(message, confirmDialog)

View File

@@ -1,3 +1,5 @@
import { inject } from 'vue'
export type ConfirmDialogOptions = {
title?: string
message?: string
@@ -13,10 +15,16 @@ export function resolveConfirmDialog(candidate: unknown): ConfirmDialogHandler |
return undefined
}
export function useConfirmDialog(): ConfirmDialogHandler | undefined {
return resolveConfirmDialog(inject('$confirm', undefined))
}
export async function askForConfirmation(
message: string,
confirmDialog?: ConfirmDialogHandler
candidate?: unknown
): Promise<boolean> {
const confirmDialog = resolveConfirmDialog(candidate)
if (confirmDialog) {
try {
return await confirmDialog({ message })

View File

@@ -190,7 +190,7 @@ 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, resolveConfirmDialog } from '@/utils/confirmDialog';
import { askForConfirmation as askForConfirmationDialog } from '@/utils/confirmDialog';
export default {
name: 'ConfigPage',
@@ -476,7 +476,7 @@ export default {
},
async confirmDeleteConfig(config) {
const message = this.tm('configManagement.confirmDelete').replace('{name}', config.name);
if (await askForConfirmationDialog(message, resolveConfirmDialog(this.$confirm))) {
if (await askForConfirmationDialog(message, this.$confirm)) {
this.deleteConfig(config.id);
}
},

View File

@@ -333,7 +333,7 @@ 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, resolveConfirmDialog } from '@/utils/confirmDialog';
import { askForConfirmation as askForConfirmationDialog } from '@/utils/confirmDialog';
export default {
name: 'ConversationPage',
@@ -747,7 +747,7 @@ export default {
// 关闭对话历史对话框
async closeHistoryDialog() {
if (this.isEditingHistory) {
if (await askForConfirmationDialog(this.tm('dialogs.view.confirmClose'), resolveConfirmDialog(this.$confirm))) {
if (await askForConfirmationDialog(this.tm('dialogs.view.confirmClose'), this.$confirm)) {
this.dialogView = false;
}
} else {

View File

@@ -197,7 +197,7 @@ 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, resolveConfirmDialog } from '@/utils/confirmDialog';
import { askForConfirmation as askForConfirmationDialog } from '@/utils/confirmDialog';
export default {
name: 'PlatformPage',
@@ -454,7 +454,7 @@ export default {
async deletePlatform(platform) {
const message = `${this.messages.deleteConfirm} ${platform.id}?`;
if (!(await askForConfirmationDialog(message, resolveConfirmDialog(this.$confirm)))) {
if (!(await askForConfirmationDialog(message, this.$confirm))) {
return;
}

View File

@@ -522,7 +522,7 @@
<script>
import axios from 'axios'
import { useI18n, useModuleI18n } from '@/i18n/composables'
import { askForConfirmation as askForConfirmationDialog, resolveConfirmDialog } from '@/utils/confirmDialog'
import { askForConfirmation as askForConfirmationDialog } from '@/utils/confirmDialog'
export default {
name: 'SessionManagementPage',
@@ -1505,7 +1505,7 @@ export default {
async deleteGroup(group) {
const message = `确定要删除分组 "${group.name}" 吗?`
if (!(await askForConfirmationDialog(message, resolveConfirmDialog(this.$confirm)))) return
if (!(await askForConfirmationDialog(message, this.$confirm))) return
try {
const response = await axios.post('/api/session/group/delete', { id: group.id })

View File

@@ -240,16 +240,16 @@
</template>
<script setup lang="ts">
import { ref, computed, inject, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import axios from 'axios'
import { useModuleI18n } from '@/i18n/composables'
import { askForConfirmation, type ConfirmDialogHandler } from '@/utils/confirmDialog'
import { askForConfirmation, useConfirmDialog } from '@/utils/confirmDialog'
const { tm: t } = useModuleI18n('features/knowledge-base/document')
const route = useRoute()
const confirmDialog = inject<ConfirmDialogHandler | undefined>('$confirm', undefined)
const confirmDialog = useConfirmDialog()
const kbId = ref(route.params.kbId as string)
const docId = ref(route.params.docId as string)

View File

@@ -260,7 +260,7 @@ 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, resolveConfirmDialog } from '@/utils/confirmDialog';
import { askForConfirmation as askForConfirmationDialog } from '@/utils/confirmDialog';
import type { Folder, FolderTreeNode } from '@/components/folder/types';
@@ -421,8 +421,12 @@ export default defineComponent({
},
async confirmDeletePersona(persona: Persona) {
const confirmDialog = resolveConfirmDialog((this as any).$confirm);
if (!(await askForConfirmationDialog(this.tm('messages.deleteConfirm', { id: persona.persona_id }), confirmDialog))) {
if (
!(await askForConfirmationDialog(
this.tm('messages.deleteConfirm', { id: persona.persona_id }),
(this as any).$confirm,
))
) {
return;
}

View File

@@ -94,7 +94,7 @@ async def check_dashboard_files(webui_dir: str | None = None):
if __name__ == "__main__":
runtime_bootstrap.flush_bootstrap_records(logger)
runtime_bootstrap.initialize_runtime_bootstrap(logger)
parser = argparse.ArgumentParser(description="AstrBot")
parser.add_argument(

View File

@@ -1,67 +1,80 @@
import ssl
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
import aiohttp.connector as aiohttp_connector
import certifi
@dataclass
class BootstrapRecord:
level: str
message: str
_BOOTSTRAP_RECORDS: list[BootstrapRecord] = []
_BOOTSTRAP_RECORDS: list[tuple[str, str]] = []
_TLS_BOOTSTRAP_DONE = False
def _record(level: str, message: str) -> None:
_BOOTSTRAP_RECORDS.append(BootstrapRecord(level=level, message=message))
_BOOTSTRAP_RECORDS.append((level, message))
def flush_bootstrap_records(log_obj: Any) -> None:
if not _BOOTSTRAP_RECORDS:
return
level_methods: dict[str, Callable[[str], Any]] = {
"info": getattr(log_obj, "info", None),
"warning": getattr(log_obj, "warning", None),
"error": getattr(log_obj, "error", None),
}
fallback = getattr(log_obj, "info", None)
for record in _BOOTSTRAP_RECORDS:
logger_method = level_methods.get(record.level, fallback)
for level, message in _BOOTSTRAP_RECORDS:
logger_method = getattr(log_obj, level, None) or getattr(log_obj, "info", None)
if callable(logger_method):
logger_method(record.message)
logger_method(message)
_BOOTSTRAP_RECORDS.clear()
def configure_runtime_ca_bundle() -> None:
def _try_patch_aiohttp_ssl_context(ssl_context: ssl.SSLContext) -> bool:
attr_name = "_SSL_CONTEXT_VERIFIED"
if not hasattr(aiohttp_connector, attr_name):
_record(
"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):
_record(
"warning",
"aiohttp connector exposes _SSL_CONTEXT_VERIFIED with unexpected type; skipped patch.",
)
return False
setattr(aiohttp_connector, attr_name, ssl_context)
_record(
"info",
"Configured aiohttp verified SSL context with system+certifi trust chain.",
)
return True
def configure_runtime_ca_bundle() -> bool:
global _TLS_BOOTSTRAP_DONE
if _TLS_BOOTSTRAP_DONE:
return True
try:
_record("info", "Bootstrapping runtime CA bundle.")
ssl_context = ssl.create_default_context()
ssl_context.load_verify_locations(cafile=certifi.where())
if hasattr(aiohttp_connector, "_SSL_CONTEXT_VERIFIED"):
aiohttp_connector._SSL_CONTEXT_VERIFIED = ssl_context
_record(
"info",
"Configured aiohttp verified SSL context with system+certifi trust chain.",
)
else:
_record(
"warning",
"aiohttp connector does not expose _SSL_CONTEXT_VERIFIED; skipped patch.",
)
_TLS_BOOTSTRAP_DONE = _try_patch_aiohttp_ssl_context(ssl_context)
return _TLS_BOOTSTRAP_DONE
except Exception as exc:
_record(
"error",
f"Failed to configure runtime CA bundle for aiohttp: {exc!r}",
)
return
return False
def initialize_runtime_bootstrap(log_obj: Any | None = None) -> bool:
configured = configure_runtime_ca_bundle()
if log_obj is not None:
flush_bootstrap_records(log_obj)
return configured
configure_runtime_ca_bundle()