mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
feat: 添加在线更新功能
This commit is contained in:
@@ -373,6 +373,22 @@ export interface CheckUpdateResponse {
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export interface SystemUpdateCapabilityResponse {
|
||||
enabled: boolean
|
||||
command_env: string
|
||||
workdir_env: string
|
||||
command?: string | null
|
||||
workdir?: string | null
|
||||
detail?: string | null
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface ApplySystemUpdateResponse {
|
||||
message: string
|
||||
started: boolean
|
||||
need_restart: boolean
|
||||
}
|
||||
|
||||
// LDAP 配置响应
|
||||
export interface LdapConfigResponse {
|
||||
server_url: string | null
|
||||
@@ -981,6 +997,30 @@ export const adminApi = {
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 获取一键更新能力
|
||||
async getSystemUpdateCapability(): Promise<SystemUpdateCapabilityResponse> {
|
||||
const response = await apiClient.get<SystemUpdateCapabilityResponse>(
|
||||
'/api/admin/system/update-capability'
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 准备系统一键更新(拉取最新镜像)
|
||||
async prepareSystemUpdate(): Promise<ApplySystemUpdateResponse> {
|
||||
const response = await apiClient.post<ApplySystemUpdateResponse>(
|
||||
'/api/admin/system/prepare-update'
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 触发系统一键重启(重建 app 容器)
|
||||
async applySystemUpdate(): Promise<ApplySystemUpdateResponse> {
|
||||
const response = await apiClient.post<ApplySystemUpdateResponse>(
|
||||
'/api/admin/system/apply-update'
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// LDAP 配置相关
|
||||
// 获取 LDAP 配置
|
||||
async getLdapConfig(): Promise<LdapConfigResponse> {
|
||||
|
||||
@@ -55,6 +55,13 @@
|
||||
>
|
||||
新版本已发布,建议更新以获得最新功能和安全修复
|
||||
</p>
|
||||
|
||||
<p
|
||||
v-if="updatePhase === 'restart'"
|
||||
class="mt-1 text-xs text-primary"
|
||||
>
|
||||
更新包已下载,点击“立即重启”完成安装
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
@@ -62,16 +69,26 @@
|
||||
<Button
|
||||
variant="outline"
|
||||
class="flex-1"
|
||||
:disabled="updating"
|
||||
@click="handleLater"
|
||||
>
|
||||
稍后提醒
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="flex-1"
|
||||
:disabled="updating"
|
||||
@click="handleViewRelease"
|
||||
>
|
||||
查看更新
|
||||
</Button>
|
||||
<Button
|
||||
class="flex-1"
|
||||
:disabled="updating"
|
||||
@click="handleApplyUpdate"
|
||||
>
|
||||
{{ actionButtonLabel }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
@@ -93,13 +110,24 @@ const props = defineProps<{
|
||||
releaseUrl: string | null
|
||||
releaseNotes: string | null
|
||||
publishedAt: string | null
|
||||
updatePhase?: 'download' | 'restart'
|
||||
updating?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
applyUpdate: []
|
||||
}>()
|
||||
|
||||
const isOpen = ref(props.modelValue)
|
||||
const updating = computed(() => props.updating ?? false)
|
||||
const updatePhase = computed(() => props.updatePhase ?? 'download')
|
||||
const actionButtonLabel = computed(() => {
|
||||
if (updating.value) {
|
||||
return updatePhase.value === 'restart' ? '重启中...' : '下载中...'
|
||||
}
|
||||
return updatePhase.value === 'restart' ? '立即重启' : '立即更新'
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
isOpen.value = val
|
||||
@@ -157,4 +185,8 @@ function handleViewRelease() {
|
||||
}
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
function handleApplyUpdate() {
|
||||
emit('applyUpdate')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -92,6 +92,19 @@
|
||||
<ExternalLink class="mr-2 h-3.5 w-3.5" />
|
||||
查看更新
|
||||
</Button>
|
||||
<Button
|
||||
v-if="status?.has_update"
|
||||
size="sm"
|
||||
class="flex-1"
|
||||
:disabled="updating"
|
||||
@click="handleApplyUpdate"
|
||||
>
|
||||
<RefreshCw
|
||||
class="mr-2 h-3.5 w-3.5"
|
||||
:class="updating ? 'animate-spin' : ''"
|
||||
/>
|
||||
{{ actionButtonLabel }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,16 +123,21 @@ import { ExternalLink, Info, RefreshCw } from 'lucide-vue-next'
|
||||
const props = defineProps<{
|
||||
status: CheckUpdateResponse | null
|
||||
loading?: boolean
|
||||
updating?: boolean
|
||||
updatePhase?: 'download' | 'restart'
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
refresh: []
|
||||
openRelease: []
|
||||
applyUpdate: []
|
||||
}>()
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
const loading = computed(() => props.loading ?? false)
|
||||
const updating = computed(() => props.updating ?? false)
|
||||
const updatePhase = computed(() => props.updatePhase ?? 'download')
|
||||
const buttonClass = computed(() => {
|
||||
const classes = []
|
||||
|
||||
@@ -160,6 +178,12 @@ const buttonTitle = computed(() => {
|
||||
if (!props.status) return '版本信息'
|
||||
return `版本信息:${statusLabel.value}`
|
||||
})
|
||||
const actionButtonLabel = computed(() => {
|
||||
if (updating.value) {
|
||||
return updatePhase.value === 'restart' ? '重启中...' : '下载中...'
|
||||
}
|
||||
return updatePhase.value === 'restart' ? '立即重启' : '立即更新'
|
||||
})
|
||||
|
||||
function handleRefresh() {
|
||||
emit('refresh')
|
||||
@@ -169,4 +193,8 @@ function handleOpenRelease() {
|
||||
isOpen.value = false
|
||||
emit('openRelease')
|
||||
}
|
||||
|
||||
function handleApplyUpdate() {
|
||||
emit('applyUpdate')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -118,8 +118,11 @@
|
||||
v-if="isAdmin"
|
||||
:status="versionStatus"
|
||||
:loading="loadingVersionStatus"
|
||||
:updating="applyingSystemUpdate"
|
||||
:update-phase="systemUpdatePhase"
|
||||
@refresh="handleVersionRefresh"
|
||||
@open-release="openVersionReleasePage"
|
||||
@apply-update="handleApplySystemUpdate"
|
||||
/>
|
||||
<button
|
||||
class="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition"
|
||||
@@ -301,8 +304,11 @@
|
||||
v-if="isAdmin"
|
||||
:status="versionStatus"
|
||||
:loading="loadingVersionStatus"
|
||||
:updating="applyingSystemUpdate"
|
||||
:update-phase="systemUpdatePhase"
|
||||
@refresh="handleVersionRefresh"
|
||||
@open-release="openVersionReleasePage"
|
||||
@apply-update="handleApplySystemUpdate"
|
||||
/>
|
||||
<!-- Theme Toggle -->
|
||||
<button
|
||||
@@ -385,6 +391,9 @@
|
||||
:release-url="updateInfo.release_url"
|
||||
:release-notes="updateInfo.release_notes"
|
||||
:published-at="updateInfo.published_at"
|
||||
:updating="applyingSystemUpdate"
|
||||
:update-phase="systemUpdatePhase"
|
||||
@apply-update="handleApplySystemUpdate"
|
||||
/>
|
||||
</AppShell>
|
||||
</template>
|
||||
@@ -397,9 +406,11 @@ import { useAuthStore } from '@/stores/auth'
|
||||
import { useModuleStore } from '@/stores/modules'
|
||||
import { useDarkMode } from '@/composables/useDarkMode'
|
||||
import { useSiteInfo } from '@/composables/useSiteInfo'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { isDemoMode } from '@/config/demo'
|
||||
import { adminApi, type CheckUpdateResponse } from '@/api/admin'
|
||||
import { announcementApi, type Announcement } from '@/api/announcements'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import { Dialog } from '@/components/ui'
|
||||
import AppShell from '@/components/layout/AppShell.vue'
|
||||
@@ -449,12 +460,15 @@ import { BUILTIN_TOOL_BREADCRUMBS } from '@/config/builtin-tools'
|
||||
import { prefetchAdminNavigationTarget } from '@/utils/adminNavigationPrefetch'
|
||||
import { sanitizeMarkdown } from '@/utils/sanitize'
|
||||
|
||||
type SystemUpdatePhase = 'download' | 'restart'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
const moduleStore = useModuleStore()
|
||||
const { themeMode, toggleDarkMode } = useDarkMode()
|
||||
const { siteName, siteSubtitle } = useSiteInfo()
|
||||
const { success, error: showError } = useToast()
|
||||
const isDemo = computed(() => isDemoMode())
|
||||
const isAdmin = computed(() => authStore.user?.role === 'admin')
|
||||
|
||||
@@ -475,8 +489,52 @@ const showUpdateDialog = ref(false)
|
||||
const updateInfo = ref<CheckUpdateResponse | null>(null)
|
||||
const versionStatus = ref<CheckUpdateResponse | null>(null)
|
||||
const loadingVersionStatus = ref(false)
|
||||
const applyingSystemUpdate = ref(false)
|
||||
const systemUpdatePhase = ref<SystemUpdatePhase>(readStoredSystemUpdatePhase())
|
||||
const preparedUpdateVersion = ref<string | null>(
|
||||
readSessionStorageItem('aether_prepared_update_version')
|
||||
)
|
||||
let versionStatusLoadPromise: Promise<CheckUpdateResponse | null> | null = null
|
||||
|
||||
watch(systemUpdatePhase, (val) => {
|
||||
setSessionStorageItem('aether_update_phase', val)
|
||||
})
|
||||
watch(preparedUpdateVersion, (val) => {
|
||||
if (val) {
|
||||
setSessionStorageItem('aether_prepared_update_version', val)
|
||||
} else {
|
||||
removeSessionStorageItem('aether_prepared_update_version')
|
||||
}
|
||||
})
|
||||
|
||||
function readStoredSystemUpdatePhase(): SystemUpdatePhase {
|
||||
return readSessionStorageItem('aether_update_phase') === 'restart' ? 'restart' : 'download'
|
||||
}
|
||||
|
||||
function readSessionStorageItem(key: string): string | null {
|
||||
try {
|
||||
return sessionStorage.getItem(key)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function setSessionStorageItem(key: string, value: string) {
|
||||
try {
|
||||
sessionStorage.setItem(key, value)
|
||||
} catch {
|
||||
// Ignore storage failures; update state still lives in memory for this page.
|
||||
}
|
||||
}
|
||||
|
||||
function removeSessionStorageItem(key: string) {
|
||||
try {
|
||||
sessionStorage.removeItem(key)
|
||||
} catch {
|
||||
// Ignore storage failures; update state still lives in memory for this page.
|
||||
}
|
||||
}
|
||||
|
||||
// 路由变化时自动关闭移动端菜单
|
||||
watch(() => route.path, () => {
|
||||
mobileMenuOpen.value = false
|
||||
@@ -508,6 +566,7 @@ async function loadVersionStatus() {
|
||||
versionStatusLoadPromise = (async () => {
|
||||
try {
|
||||
versionStatus.value = await adminApi.checkUpdate()
|
||||
syncSystemUpdatePhase(versionStatus.value)
|
||||
return versionStatus.value
|
||||
} catch (error) {
|
||||
versionStatus.value = buildUpdateErrorStatus(versionStatus.value, error)
|
||||
@@ -521,6 +580,22 @@ async function loadVersionStatus() {
|
||||
return versionStatusLoadPromise
|
||||
}
|
||||
|
||||
function syncSystemUpdatePhase(status: CheckUpdateResponse | null) {
|
||||
if (!status?.has_update) {
|
||||
systemUpdatePhase.value = 'download'
|
||||
preparedUpdateVersion.value = null
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
systemUpdatePhase.value === 'restart' &&
|
||||
(!preparedUpdateVersion.value || preparedUpdateVersion.value !== status.latest_version)
|
||||
) {
|
||||
systemUpdatePhase.value = 'download'
|
||||
preparedUpdateVersion.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function handleVersionRefresh() {
|
||||
void loadVersionStatus()
|
||||
}
|
||||
@@ -531,6 +606,38 @@ function openVersionReleasePage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleApplySystemUpdate() {
|
||||
if (applyingSystemUpdate.value) return
|
||||
applyingSystemUpdate.value = true
|
||||
try {
|
||||
const capability = await adminApi.getSystemUpdateCapability()
|
||||
if (!capability.enabled) {
|
||||
showError(
|
||||
capability.detail || `一键更新未启用,请先在部署环境配置 ${capability.command_env}`,
|
||||
'无法启动更新'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (systemUpdatePhase.value === 'download') {
|
||||
const result = await adminApi.prepareSystemUpdate()
|
||||
preparedUpdateVersion.value = updateInfo.value?.latest_version || versionStatus.value?.latest_version || null
|
||||
systemUpdatePhase.value = 'restart'
|
||||
success(result.message || '更新包已下载完成,请点击“立即重启”完成安装')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await adminApi.applySystemUpdate()
|
||||
success(result.message || '一键重启已启动')
|
||||
showUpdateDialog.value = false
|
||||
} catch (err) {
|
||||
const fallback = systemUpdatePhase.value === 'download' ? '下载更新失败' : '启动重启失败'
|
||||
showError(parseApiError(err, fallback))
|
||||
} finally {
|
||||
applyingSystemUpdate.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function showDebugUpdateDialog() {
|
||||
const currentVersion = versionStatus.value?.current_version || __APP_VERSION__ || '0.7.0-rc28'
|
||||
updateInfo.value = {
|
||||
@@ -547,6 +654,8 @@ function showDebugUpdateDialog() {
|
||||
published_at: new Date().toISOString(),
|
||||
error: null,
|
||||
}
|
||||
systemUpdatePhase.value = 'download'
|
||||
preparedUpdateVersion.value = null
|
||||
showUpdateDialog.value = true
|
||||
}
|
||||
|
||||
@@ -568,6 +677,8 @@ function showDebugVersionStatus(hasUpdate = true) {
|
||||
published_at: hasUpdate ? new Date().toISOString() : null,
|
||||
error: null,
|
||||
}
|
||||
systemUpdatePhase.value = 'download'
|
||||
preparedUpdateVersion.value = null
|
||||
}
|
||||
|
||||
// 检查更新
|
||||
|
||||
Reference in New Issue
Block a user