添加自动更新功能
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user