feat: 添加 WebUI 迁移助手以及相关迁移方法 (#2477)

This commit is contained in:
Soulter
2025-08-17 23:24:30 +08:00
committed by GitHub
parent 1df49d1d6f
commit e95ad4049b
14 changed files with 415 additions and 28 deletions
-6
View File
@@ -34,7 +34,6 @@ from astrbot.core.platform_message_history_mgr import PlatformMessageHistoryMana
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
from astrbot.core.star.star_handler import star_handlers_registry, EventType
from astrbot.core.star.star_handler import star_map
from astrbot.core.db.migration.helper import do_migration_v4
class AstrBotCoreLifecycle:
@@ -72,11 +71,6 @@ class AstrBotCoreLifecycle:
await self.db.initialize()
try:
await do_migration_v4(self.db, {}, self.astrbot_config)
except Exception as e:
logger.error(f"迁移到 v4.0.0 新版本数据格式失败: {e}")
# 初始化 AstrBot 配置管理器
self.astrbot_config_mgr = AstrBotConfigManager(
default_config=self.astrbot_config, sp=sp
+4 -4
View File
@@ -43,14 +43,14 @@ async def do_migration_v4(
# 执行会话表迁移
await migration_conversation_table(db_helper, platform_id_map)
# 执行平台统计表迁移
await migration_platform_table(db_helper, platform_id_map)
# 执行人格数据迁移
await migration_persona_data(db_helper, astrbot_config)
# 执行 WebChat 数据迁移
await migration_webchat_data(db_helper, platform_id_map)
# 执行人格数据迁移
await migration_persona_data(db_helper, astrbot_config)
# 执行平台统计表迁移
await migration_platform_table(db_helper, platform_id_map)
# 标记迁移完成
await db_helper.insert_preference_or_update("migration_done_v4", "true")
+3
View File
@@ -7,8 +7,11 @@ from astrbot import logger
DEFAULT_PERSONALITY = Personality(
prompt="You are a helpful and friendly assistant.",
name="default",
begin_dialogs=[],
mood_imitation_dialogs=[],
tools=None,
_begin_dialogs_processed=[],
_mood_imitation_dialogs_processed="",
)
+4
View File
@@ -11,6 +11,7 @@ from astrbot.core.db import BaseDatabase
from astrbot.core.config import VERSION
from astrbot.core.utils.io import get_dashboard_version
from astrbot.core import DEMO_MODE
from astrbot.core.db.migration.helper import check_migration_needed_v4
class StatRoute(Route):
@@ -59,6 +60,8 @@ class StatRoute(Route):
)
async def get_version(self):
need_migration = await check_migration_needed_v4(self.core_lifecycle.db)
return (
Response()
.ok(
@@ -66,6 +69,7 @@ class StatRoute(Route):
"version": VERSION,
"dashboard_version": await get_dashboard_version(),
"change_pwd_hint": self.is_default_cred(),
"need_migration": need_migration,
}
)
.__dict__
+17
View File
@@ -7,6 +7,7 @@ from astrbot.core import logger, pip_installer
from astrbot.core.utils.io import download_dashboard, get_dashboard_version
from astrbot.core.config.default import VERSION
from astrbot.core import DEMO_MODE
from astrbot.core.db.migration.helper import do_migration_v4, check_migration_needed_v4
class UpdateRoute(Route):
@@ -23,11 +24,27 @@ class UpdateRoute(Route):
"/update/do": ("POST", self.update_project),
"/update/dashboard": ("POST", self.update_dashboard),
"/update/pip-install": ("POST", self.install_pip_package),
"/update/migration": ("POST", self.do_migration),
}
self.astrbot_updator = astrbot_updator
self.core_lifecycle = core_lifecycle
self.register_routes()
async def do_migration(self):
need_migration = await check_migration_needed_v4(self.core_lifecycle.db)
if not need_migration:
return Response().ok(None, "不需要进行迁移。").__dict__
try:
data = await request.json
pim = data.get("platform_id_map", {})
await do_migration_v4(
self.core_lifecycle.db, pim, self.core_lifecycle.astrbot_config
)
return Response().ok(None, "迁移成功。").__dict__
except Exception as e:
logger.error(f"迁移失败: {traceback.format_exc()}")
return Response().error(f"迁移失败: {str(e)}").__dict__
async def check_update(self):
type_ = request.args.get("type", None)
@@ -0,0 +1,275 @@
<template>
<v-dialog v-model="isOpen" persistent max-width="600" max-height="80vh" scrollable>
<v-card>
<v-card-title>
{{ t('features.migration.dialog.title') }}
</v-card-title>
<v-card-text class="pa-6">
<p class="mb-4">{{ t('features.migration.dialog.warning') }}</p>
<div v-if="migrationCompleted" class="text-center py-8">
<v-icon size="64" color="success" class="mb-4">mdi-check-circle</v-icon>
<h3 class="mb-4">{{ t('features.migration.dialog.completed') }}</h3>
{{ migrationResult?.message || t('features.migration.dialog.success') }}
</div>
<div v-else-if="migrating" class="migration-in-progress">
<div class="text-center py-4">
<v-progress-circular indeterminate color="primary" class="mb-4"></v-progress-circular>
<h3 class="mb-4">{{ t('features.migration.dialog.migrating') }}</h3>
<p class="mb-4">{{ t('features.migration.dialog.migratingSubtitle') }}</p>
</div>
<div class="console-container">
<ConsoleDisplayer ref="consoleDisplayer" :showLevelBtns="false" style="height: 300px;" />
</div>
</div>
<div v-else-if="loading" class="text-center py-8">
<v-progress-circular indeterminate color="primary" class="mb-4"></v-progress-circular>
<p>{{ t('features.migration.dialog.loading') }}</p>
</div>
<div v-else-if="error" class="text-center py-4">
<v-alert type="error" variant="tonal" class="mb-4">
<template v-slot:prepend>
<v-icon>mdi-alert</v-icon>
</template>
{{ error }}
</v-alert>
<v-btn color="primary" @click="loadPlatforms">
{{ t('features.migration.dialog.retry') }}
</v-btn>
</div>
<div v-else>
<div v-if="platformGroups.length === 0" class="text-center py-4">
<v-alert type="info" variant="tonal">
<template v-slot:prepend>
<v-icon>mdi-information</v-icon>
</template>
{{ t('features.migration.dialog.noPlatforms') }}
</v-alert>
</div>
<div v-else>
<div v-for="group in platformGroups" :key="group.type" class="mb-6">
<v-card variant="outlined" v-if="group.platforms.length > 1">
<v-card-subtitle class="py-2">
{{ group.type }}
</v-card-subtitle>
<v-divider></v-divider>
<v-card-text style="padding: 16px;">
<small>请选择该平台类型下您主要使用的平台适配器</small>
<v-radio-group v-model="selectedPlatforms[group.type]" :key="group.type"
hide-details>
<v-radio v-for="platform in group.platforms" :key="platform.id"
:value="platform.id" :label="getPlatformLabel(platform)" color="primary"
class="mb-1"></v-radio>
</v-radio-group>
</v-card-text>
</v-card>
</div>
</div>
</div>
</v-card-text>
<v-card-actions class="px-6 py-4">
<v-spacer></v-spacer>
<template v-if="migrationCompleted">
<v-btn color="primary" variant="elevated" @click="handleClose">
{{ t('core.common.confirm') }}
</v-btn>
</template>
<template v-else>
<v-btn color="grey" variant="text" @click="handleCancel" :disabled="migrating">
{{ t('core.common.cancel') }}
</v-btn>
<v-btn color="primary" variant="elevated" @click="handleMigration" :disabled="!canMigrate || migrating"
:loading="migrating">
{{ t('features.migration.dialog.startMigration') }}
</v-btn>
</template>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import axios from 'axios'
import { useI18n } from '@/i18n/composables'
import ConsoleDisplayer from './ConsoleDisplayer.vue'
const { t } = useI18n()
const isOpen = ref(false)
const loading = ref(false)
const error = ref('')
const migrating = ref(false)
const migrationCompleted = ref(false)
const migrationResult = ref(null)
const platforms = ref([])
const selectedPlatforms = ref({})
let resolvePromise = null
// 计算属性:将平台按类型分组
const platformGroups = computed(() => {
const groups = {}
platforms.value.forEach(platform => {
const type = platform.platform_type || platform.type
if (!groups[type]) {
groups[type] = {
type,
platforms: []
}
}
groups[type].platforms.push(platform)
})
return Object.values(groups)
})
// 计算属性:检查是否可以开始迁移
const canMigrate = computed(() => {
return platformGroups.value.every(group => selectedPlatforms.value[group.type])
})
// 监听 isOpen 变化,当对话框打开时加载平台列表
watch(isOpen, (newVal) => {
if (newVal) {
loadPlatforms()
} else {
// 重置状态
platforms.value = []
selectedPlatforms.value = {}
error.value = ''
migrating.value = false
migrationCompleted.value = false
migrationResult.value = null
}
})
// 加载平台列表
const loadPlatforms = async () => {
loading.value = true
error.value = ''
try {
const response = await axios.get('/api/config/platform/list')
if (response.data.status === 'ok') {
platforms.value = response.data.data.platforms || []
// 为每个平台类型初始化默认选择(选择第一个)
platformGroups.value.forEach(group => {
if (group.platforms.length > 0) {
selectedPlatforms.value[group.type] = group.platforms[0].id
}
})
} else {
error.value = response.data.message || t('features.migration.dialog.loadError')
}
} catch (err) {
console.error('Failed to load platforms:', err)
error.value = t('features.migration.dialog.loadError')
} finally {
loading.value = false
}
}
// 执行迁移
const handleMigration = async () => {
migrating.value = true
try {
// 构建 platform_id_map
const platformIdMap = {}
Object.entries(selectedPlatforms.value).forEach(([type, platformId]) => {
const selectedPlatform = platforms.value.find(p => p.id === platformId)
if (selectedPlatform) {
platformIdMap[type] = {
platform_id: platformId,
platform_type: type
}
}
})
console.log('Migration platform_id_map:', platformIdMap)
const response = await axios.post('/api/update/migration', {
platform_id_map: platformIdMap
})
if (response.data.status === 'ok') {
migrationCompleted.value = true
migrationResult.value = {
success: true,
message: response.data.message || t('features.migration.dialog.success')
}
} else {
throw new Error(response.data.message || t('features.migration.dialog.migrationError'))
}
} catch (err) {
console.error('Migration failed:', err)
error.value = err.message || t('features.migration.dialog.migrationError')
} finally {
migrating.value = false
}
}
// 取消操作
const handleCancel = () => {
isOpen.value = false
if (resolvePromise) {
resolvePromise({ success: false, cancelled: true })
}
}
// 关闭已完成的迁移对话框
const handleClose = () => {
isOpen.value = false
if (resolvePromise) {
resolvePromise(migrationResult.value)
}
}
// 获取平台显示标签
const getPlatformLabel = (platform) => {
const name = platform.name || platform.id || 'Unknown'
return `${name}`
}
// 打开对话框的方法
const open = () => {
isOpen.value = true
return new Promise((resolve) => {
resolvePromise = resolve
})
}
defineExpose({ open })
</script>
<style scoped>
.v-radio-group {
max-height: 200px;
overflow-y: auto;
}
.migration-in-progress {
min-height: 400px;
}
.console-container {
border: 1px solid var(--v-theme-border);
border-radius: 8px;
margin-top: 16px;
overflow: hidden;
}
</style>
+1
View File
@@ -53,6 +53,7 @@ export class I18nLoader {
{ name: 'features/alkaid/knowledge-base', path: 'features/alkaid/knowledge-base.json' },
{ name: 'features/alkaid/memory', path: 'features/alkaid/memory.json' },
{ name: 'features/persona', path: 'features/persona.json' },
{ name: 'features/migration', path: 'features/migration.json' },
// 消息模块
{ name: 'messages/errors', path: 'messages/errors.json' },
@@ -0,0 +1,16 @@
{
"dialog": {
"title": "Database Migration Assistant",
"warning": "A database migration is required.",
"loading": "Loading platform list...",
"loadError": "Failed to load platform list, please retry",
"noPlatforms": "No available platform configurations found",
"retry": "Retry",
"startMigration": "Start Migration",
"migrating": "Migrating...",
"migratingSubtitle": "Please wait patiently, do not close this window during migration",
"migrationError": "Migration failed",
"success": "Migration completed successfully!",
"completed": "Migration Completed"
}
}
@@ -13,6 +13,11 @@
"title": "Restart",
"subtitle": "Restart AstrBot",
"button": "Restart"
},
"migration": {
"title": "Data Migration to v4.0.0",
"subtitle": "If you encounter data compatibility issues, you can manually start the database migration assistant",
"button": "Start Migration Assistant"
}
}
}
@@ -0,0 +1,16 @@
{
"dialog": {
"title": "数据迁移助手",
"warning": "👋 欢迎升级到 v4.0.0。我们在新版本对数据格式进行了优化,检测到需要进行数据库迁移。",
"loading": "正在加载平台列表...",
"loadError": "加载平台列表失败,请重试",
"noPlatforms": "未找到可用的平台配置",
"retry": "重试",
"startMigration": "开始迁移",
"migrating": "正在迁移中...",
"migratingSubtitle": "请耐心等待,迁移过程中请勿关闭此窗口",
"migrationError": "迁移失败",
"success": "迁移成功完成!",
"completed": "迁移已完成"
}
}
@@ -13,6 +13,11 @@
"title": "重启",
"subtitle": "重启 AstrBot",
"button": "重启"
},
"migration": {
"title": "数据迁移到 v4.0.0 格式",
"subtitle": "如果您遇到数据兼容性问题,可以手动启动数据库迁移助手",
"button": "启动迁移助手"
}
}
}
+6 -2
View File
@@ -26,6 +26,7 @@ import zhCNAlkaidIndex from './locales/zh-CN/features/alkaid/index.json';
import zhCNAlkaidKnowledgeBase from './locales/zh-CN/features/alkaid/knowledge-base.json';
import zhCNAlkaidMemory from './locales/zh-CN/features/alkaid/memory.json';
import zhCNPersona from './locales/zh-CN/features/persona.json';
import zhCNMigration from './locales/zh-CN/features/migration.json';
import zhCNErrors from './locales/zh-CN/messages/errors.json';
import zhCNSuccess from './locales/zh-CN/messages/success.json';
@@ -56,6 +57,7 @@ import enUSAlkaidIndex from './locales/en-US/features/alkaid/index.json';
import enUSAlkaidKnowledgeBase from './locales/en-US/features/alkaid/knowledge-base.json';
import enUSAlkaidMemory from './locales/en-US/features/alkaid/memory.json';
import enUSPersona from './locales/en-US/features/persona.json';
import enUSMigration from './locales/en-US/features/migration.json';
import enUSErrors from './locales/en-US/messages/errors.json';
import enUSSuccess from './locales/en-US/messages/success.json';
@@ -91,7 +93,8 @@ export const translations = {
'knowledge-base': zhCNAlkaidKnowledgeBase,
memory: zhCNAlkaidMemory
},
persona: zhCNPersona
persona: zhCNPersona,
migration: zhCNMigration
},
messages: {
errors: zhCNErrors,
@@ -127,7 +130,8 @@ export const translations = {
'knowledge-base': enUSAlkaidKnowledgeBase,
memory: enUSAlkaidMemory
},
persona: enUSPersona
persona: enUSPersona,
migration: enUSMigration
},
messages: {
errors: enUSErrors,
+34
View File
@@ -1,9 +1,40 @@
<script setup lang="ts">
import { RouterView } from 'vue-router';
import { ref, onMounted } from 'vue';
import axios from 'axios';
import VerticalSidebarVue from './vertical-sidebar/VerticalSidebar.vue';
import VerticalHeaderVue from './vertical-header/VerticalHeader.vue';
import MigrationDialog from '@/components/shared/MigrationDialog.vue';
import { useCustomizerStore } from '@/stores/customizer';
const customizer = useCustomizerStore();
const migrationDialog = ref(null);
// 检查是否需要迁移
const checkMigration = async () => {
try {
const response = await axios.get('/api/stat/version');
if (response.data.status === 'ok' && response.data.data.need_migration) {
// 需要迁移,显示迁移对话框
if (migrationDialog.value) {
const result = await migrationDialog.value.open();
if (result.success) {
// 迁移成功,可以显示成功消息
console.log('Migration completed successfully:', result.message);
// 可以考虑刷新页面或显示成功通知
window.location.reload();
}
}
}
} catch (error) {
console.error('Failed to check migration status:', error);
}
};
onMounted(() => {
// 页面加载时检查是否需要迁移
setTimeout(checkMigration, 1000); // 延迟1秒执行,确保页面完全加载
});
</script>
<template>
@@ -20,6 +51,9 @@ const customizer = useCustomizerStore();
</div>
</v-container>
</v-main>
<!-- Migration Dialog -->
<MigrationDialog ref="migrationDialog" />
</v-app>
</v-locale-provider>
</template>
+29 -16
View File
@@ -14,35 +14,48 @@
<v-list-item :subtitle="tm('system.restart.subtitle')" :title="tm('system.restart.title')">
<v-btn style="margin-top: 16px;" color="error" @click="restartAstrBot">{{ tm('system.restart.button') }}</v-btn>
</v-list-item>
<v-list-item :subtitle="tm('system.migration.subtitle')" :title="tm('system.migration.title')">
<v-btn style="margin-top: 16px;" color="primary" @click="startMigration">{{ tm('system.migration.button') }}</v-btn>
</v-list-item>
</v-list>
</div>
<WaitingForRestart ref="wfr"></WaitingForRestart>
<MigrationDialog ref="migrationDialog"></MigrationDialog>
</template>
<script>
<script setup>
import { ref } from 'vue';
import axios from 'axios';
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
import ProxySelector from '@/components/shared/ProxySelector.vue';
import MigrationDialog from '@/components/shared/MigrationDialog.vue';
import { useModuleI18n } from '@/i18n/composables';
export default {
components: {
WaitingForRestart,
ProxySelector,
},
setup() {
const { tm } = useModuleI18n('features/settings');
return { tm };
},
methods: {
restartAstrBot() {
axios.post('/api/stat/restart-core').then(() => {
this.$refs.wfr.check();
})
const { tm } = useModuleI18n('features/settings');
const wfr = ref(null);
const migrationDialog = ref(null);
const restartAstrBot = () => {
axios.post('/api/stat/restart-core').then(() => {
wfr.value.check();
})
}
const startMigration = async () => {
if (migrationDialog.value) {
try {
const result = await migrationDialog.value.open();
if (result.success) {
console.log('Migration completed successfully:', result.message);
}
} catch (error) {
console.error('Migration dialog error:', error);
}
},
}
}
</script>