添加自动更新功能

This commit is contained in:
2025-12-15 14:34:08 +08:00
parent 809c735498
commit 0d1397debe
26 changed files with 1021 additions and 52 deletions

View File

@@ -0,0 +1,27 @@
import { api } from './client'
export async function fetchUpdateStatus() {
const { data } = await api.get('/update/status')
return data
}
export async function fetchUpdateResult() {
const { data } = await api.get('/update/result')
return data
}
export async function fetchUpdateLog(params = {}) {
const { data } = await api.get('/update/log', { params })
return data
}
export async function requestUpdateCheck() {
const { data } = await api.post('/update/check', {})
return data
}
export async function requestUpdateRun() {
const { data } = await api.post('/update/run', {})
return data
}

View File

@@ -1,9 +1,10 @@
<script setup>
import { computed, onMounted, ref } from 'vue'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { fetchSystemConfig, updateSystemConfig, executeScheduleNow } from '../api/system'
import { fetchProxyConfig, testProxy, updateProxyConfig } from '../api/proxy'
import { fetchUpdateLog, fetchUpdateResult, fetchUpdateStatus, requestUpdateCheck, requestUpdateRun } from '../api/update'
const loading = ref(false)
@@ -28,6 +29,16 @@ const autoApproveEnabled = ref(false)
const autoApproveHourlyLimit = ref(10)
const autoApproveVipDays = ref(7)
// 自动更新
const updateLoading = ref(false)
const updateActionLoading = ref(false)
const updateStatus = ref(null)
const updateStatusError = ref('')
const updateResult = ref(null)
const updateLog = ref('')
const updateLogTruncated = ref(false)
let updatePollTimer = null
const weekdaysOptions = [
{ label: '周一', value: '1' },
{ label: '周二', value: '2' },
@@ -59,6 +70,59 @@ function normalizeBrowseType(value) {
return '应读'
}
function shortCommit(value) {
const text = String(value || '').trim()
if (!text) return '-'
return text.length > 12 ? `${text.slice(0, 12)}` : text
}
async function loadUpdateInfo({ withLog = true } = {}) {
updateLoading.value = true
updateStatusError.value = ''
try {
const [statusRes, resultRes] = await Promise.all([fetchUpdateStatus(), fetchUpdateResult()])
if (statusRes?.ok) {
updateStatus.value = statusRes.data || null
} else {
updateStatus.value = null
updateStatusError.value = statusRes?.error || '未发现更新状态Update-Agent 可能未运行)'
}
updateResult.value = resultRes?.ok ? resultRes.data : null
const jobId = updateResult.value?.job_id
if (withLog && jobId) {
const logRes = await fetchUpdateLog({ job_id: jobId, max_bytes: 200000 })
updateLog.value = logRes?.log || ''
updateLogTruncated.value = !!logRes?.truncated
} else {
updateLog.value = ''
updateLogTruncated.value = false
}
} catch {
// handled by interceptor
} finally {
updateLoading.value = false
}
}
function startUpdatePolling() {
if (updatePollTimer) return
updatePollTimer = setInterval(async () => {
if (updateResult.value?.status === 'running') {
await loadUpdateInfo()
}
}, 5000)
}
function stopUpdatePolling() {
if (updatePollTimer) {
clearInterval(updatePollTimer)
updatePollTimer = null
}
}
async function loadAll() {
loading.value = true
try {
@@ -85,6 +149,9 @@ async function loadAll() {
proxyEnabled.value = (proxy.proxy_enabled ?? 0) === 1
proxyApiUrl.value = proxy.proxy_api_url || ''
proxyExpireMinutes.value = proxy.proxy_expire_minutes ?? 3
await loadUpdateInfo({ withLog: false })
startUpdatePolling()
} catch {
// handled by interceptor
} finally {
@@ -233,7 +300,48 @@ async function saveAutoApprove() {
}
}
async function onCheckUpdate() {
updateActionLoading.value = true
try {
const res = await requestUpdateCheck()
ElMessage.success(res?.success ? '已触发检查更新' : '已提交检查请求')
setTimeout(() => loadUpdateInfo({ withLog: false }), 800)
} catch {
// handled by interceptor
} finally {
updateActionLoading.value = false
}
}
async function onRunUpdate() {
const status = updateStatus.value
const remote = status?.remote_commit ? shortCommit(status.remote_commit) : '-'
try {
await ElMessageBox.confirm(
`确定开始“一键更新”吗?\n\n目标版本: ${remote}\n\n更新将会重建并重启服务页面可能短暂不可用系统会先备份数据库。`,
'一键更新确认',
{ confirmButtonText: '开始更新', cancelButtonText: '取消', type: 'warning' },
)
} catch {
return
}
updateActionLoading.value = true
try {
const res = await requestUpdateRun()
ElMessage.success(res?.message || '已提交更新请求')
startUpdatePolling()
setTimeout(() => loadUpdateInfo(), 800)
} catch {
// handled by interceptor
} finally {
updateActionLoading.value = false
}
}
onMounted(loadAll)
onBeforeUnmount(stopUpdatePolling)
</script>
<template>
@@ -347,6 +455,84 @@ onMounted(loadAll)
<el-button type="primary" @click="saveAutoApprove">保存自动审核配置</el-button>
</el-card>
<el-card shadow="never" :body-style="{ padding: '16px' }" class="card" v-loading="updateLoading">
<h3 class="section-title">版本与更新</h3>
<el-alert
v-if="updateStatus?.update_available"
type="warning"
:closable="false"
title="检测到新版本:可以在此页面点击“一键更新”升级并自动重启服务。"
style="margin-bottom: 10px"
/>
<el-alert
v-if="updateStatusError"
type="info"
:closable="false"
:title="updateStatusError"
style="margin-bottom: 10px"
/>
<el-descriptions border :column="1" size="small" style="margin-bottom: 10px">
<el-descriptions-item label="本地版本(commit)">
{{ shortCommit(updateStatus?.local_commit) }}
</el-descriptions-item>
<el-descriptions-item label="远端版本(commit)">
{{ shortCommit(updateStatus?.remote_commit) }}
</el-descriptions-item>
<el-descriptions-item label="是否有更新">
<el-tag v-if="updateStatus?.update_available" type="danger"></el-tag>
<el-tag v-else type="success"></el-tag>
</el-descriptions-item>
<el-descriptions-item label="工作区修改">
<el-tag v-if="updateStatus?.dirty" type="warning">有未提交修改</el-tag>
<el-tag v-else type="info">干净</el-tag>
</el-descriptions-item>
<el-descriptions-item label="最近检查时间">
{{ updateStatus?.checked_at || '-' }}
</el-descriptions-item>
<el-descriptions-item v-if="updateStatus?.error" label="检查错误">
{{ updateStatus?.error }}
</el-descriptions-item>
</el-descriptions>
<div class="row-actions">
<el-button @click="loadUpdateInfo" :disabled="updateActionLoading">刷新更新信息</el-button>
<el-button @click="onCheckUpdate" :loading="updateActionLoading">检查更新</el-button>
<el-button type="danger" @click="onRunUpdate" :loading="updateActionLoading" :disabled="!updateStatus?.update_available">
一键更新
</el-button>
</div>
<el-divider content-position="left">最近一次更新结果</el-divider>
<el-descriptions v-if="updateResult" border :column="1" size="small" style="margin-bottom: 10px">
<el-descriptions-item label="job_id">{{ updateResult.job_id }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag v-if="updateResult.status === 'running'" type="warning">运行中</el-tag>
<el-tag v-else-if="updateResult.status === 'success'" type="success">成功</el-tag>
<el-tag v-else type="danger">失败</el-tag>
</el-descriptions-item>
<el-descriptions-item label="阶段">{{ updateResult.stage || '-' }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ updateResult.started_at || '-' }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ updateResult.finished_at || '-' }}</el-descriptions-item>
<el-descriptions-item label="耗时(秒)">{{ updateResult.duration_seconds ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="更新前(commit)">{{ shortCommit(updateResult.from_commit) }}</el-descriptions-item>
<el-descriptions-item label="更新后(commit)">{{ shortCommit(updateResult.to_commit) }}</el-descriptions-item>
<el-descriptions-item label="健康检查">
<span v-if="updateResult.health_ok === true">通过{{ updateResult.health_message }}</span>
<span v-else-if="updateResult.health_ok === false">失败{{ updateResult.health_message }}</span>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item v-if="updateResult.error" label="错误">{{ updateResult.error }}</el-descriptions-item>
</el-descriptions>
<div v-else class="help">暂无更新记录</div>
<el-divider content-position="left">更新日志</el-divider>
<div class="help" v-if="updateLogTruncated">日志过长仅展示末尾内容</div>
<el-input v-model="updateLog" type="textarea" :rows="10" readonly placeholder="暂无日志" />
</el-card>
</div>
</template>