feat: 实现系统动态更新功能和 Release 创建脚本

主要变更:

1. 动态更新功能
   - 新增 Python 更新服务 (docker/update-service.py)
   - 添加系统更新前端页面 (frontend/src/pages/SystemUpdate.tsx)
   - 配置 Nginx 代理更新服务 API
   - 更新 Docker 启动脚本支持多进程管理
   - 修复权限验证接口 (AuthController.verify)

2. Release 创建脚本
   - 新增 create-release.sh 脚本支持快速创建 GitHub Release
   - 支持自动拼接 -beta 后缀(pre-release)
   - 支持无交互模式(--yes 参数)
   - 添加详细的使用文档

3. GitHub Actions 增强
   - 添加更新包构建和上传流程
   - 支持 Pre-release 检测和过滤

4. 文档完善
   - 添加动态更新技术方案文档
   - 添加 Docker 版本号确定流程文档
   - 添加 Release 脚本使用说明
This commit is contained in:
WrBug
2026-01-21 03:34:16 +08:00
parent 662aa47de6
commit 26dd3bb387
25 changed files with 4957 additions and 118 deletions
+329
View File
@@ -0,0 +1,329 @@
import { useState, useEffect } from 'react'
import { Card, Button, Spin, Progress, Alert, Space, Typography, Divider, Tag, Modal, message } from 'antd'
import {
CloudUploadOutlined,
ReloadOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
InfoCircleOutlined
} from '@ant-design/icons'
import { apiClient } from '../services/api'
const { Title, Text, Paragraph } = Typography
interface UpdateInfo {
hasUpdate: boolean
currentVersion: string
latestVersion: string
latestTag: string
releaseNotes: string
publishedAt: string
prerelease: boolean
}
interface UpdateStatus {
updating: boolean
progress: number
message: string
error: string | null
}
const SystemUpdate: React.FC = () => {
const [currentVersion, setCurrentVersion] = useState('')
const [updateChecking, setUpdateChecking] = useState(false)
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null)
const [updateStatus, setUpdateStatus] = useState<UpdateStatus>({
updating: false,
progress: 0,
message: '就绪',
error: null
})
useEffect(() => {
fetchCurrentVersion()
fetchUpdateStatus()
}, [])
const fetchCurrentVersion = async () => {
try {
const response = await apiClient.get('/update/version')
if (response.data.code === 0 && response.data.data) {
setCurrentVersion(response.data.data.version)
}
} catch (error: any) {
console.error('获取版本失败:', error)
}
}
const fetchUpdateStatus = async () => {
try {
const response = await apiClient.get('/update/status')
if (response.data.code === 0 && response.data.data) {
setUpdateStatus({
updating: response.data.data.updating,
progress: response.data.data.progress || 0,
message: response.data.data.message || '就绪',
error: response.data.data.error || null
})
}
} catch (error: any) {
console.error('获取更新状态失败:', error)
}
}
const handleCheckUpdate = async () => {
setUpdateChecking(true)
setUpdateInfo(null)
try {
const response = await apiClient.get('/update/check')
const data = response.data
if (data.code === 0 && data.data) {
setUpdateInfo(data.data)
if (data.data.hasUpdate) {
message.success(`发现新版本: ${data.data.latestVersion}`)
} else {
message.info('当前已是最新版本')
}
} else {
message.error(data.message || '检查更新失败')
}
} catch (error: any) {
message.error(error.message || '检查更新失败')
} finally {
setUpdateChecking(false)
}
}
const handleExecuteUpdate = () => {
Modal.confirm({
title: '确认更新',
icon: <ExclamationCircleOutlined />,
content: (
<div>
<p> <strong>{updateInfo?.latestVersion}</strong> </p>
<p>30-60</p>
<p></p>
</div>
),
okText: '立即更新',
okType: 'primary',
cancelText: '取消',
onOk: async () => {
try {
const response = await apiClient.post('/update/execute', {})
const data = response.data
if (data.code === 0) {
message.success('更新已启动,请稍候...')
// 开始轮询更新状态
const pollInterval = setInterval(async () => {
try {
const statusResponse = await apiClient.get('/update/status')
const statusData = statusResponse.data
if (statusData.code === 0 && statusData.data) {
setUpdateStatus({
updating: statusData.data.updating,
progress: statusData.data.progress || 0,
message: statusData.data.message || '',
error: statusData.data.error || null
})
// 更新完成
if (!statusData.data.updating) {
clearInterval(pollInterval)
if (statusData.data.error) {
message.error(`更新失败: ${statusData.data.error}`)
} else if (statusData.data.progress === 100) {
message.success('更新成功!页面将在3秒后刷新...')
setTimeout(() => window.location.reload(), 3000)
}
}
}
} catch (error) {
console.error('获取更新状态失败:', error)
}
}, 2000) // 每2秒轮询一次
// 5分钟后停止轮询
setTimeout(() => clearInterval(pollInterval), 5 * 60 * 1000)
} else if (data.code === 403) {
message.error('需要管理员权限才能执行更新')
} else {
message.error(data.message || '启动更新失败')
}
} catch (error: any) {
message.error(error.message || '启动更新失败')
}
}
})
}
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString('zh-CN')
}
return (
<Card
title={
<Space>
<CloudUploadOutlined />
<span></span>
</Space>
}
style={{ marginBottom: '16px' }}
>
<Space direction="vertical" style={{ width: '100%' }} size="large">
{/* 当前版本信息 */}
<div>
<Title level={5}></Title>
<Space>
<Tag color="blue" style={{ fontSize: '14px', padding: '4px 12px' }}>
v{currentVersion || 'unknown'}
</Tag>
</Space>
</div>
<Divider />
{/* 更新状态 */}
{updateStatus.updating && (
<Alert
message="系统正在更新"
description={
<div>
<p>{updateStatus.message}</p>
<Progress
percent={updateStatus.progress}
status="active"
strokeColor={{ '0%': '#108ee9', '100%': '#87d068' }}
/>
</div>
}
type="info"
showIcon
icon={<Spin />}
/>
)}
{updateStatus.error && (
<Alert
message="更新失败"
description={updateStatus.error}
type="error"
showIcon
closable
onClose={() => setUpdateStatus(prev => ({ ...prev, error: null }))}
/>
)}
{/* 检查更新 */}
{!updateStatus.updating && (
<Space>
<Button
type="primary"
icon={<ReloadOutlined />}
onClick={handleCheckUpdate}
loading={updateChecking}
>
</Button>
{updateInfo && !updateInfo.hasUpdate && (
<Alert
message="当前已是最新版本"
type="success"
showIcon
icon={<CheckCircleOutlined />}
/>
)}
</Space>
)}
{/* 更新信息 */}
{updateInfo && updateInfo.hasUpdate && !updateStatus.updating && (
<Alert
message={
<Space>
<span>:</span>
<Tag color="green" style={{ fontSize: '14px' }}>
v{updateInfo.latestVersion}
</Tag>
{updateInfo.prerelease && (
<Tag color="orange">Pre-release</Tag>
)}
</Space>
}
description={
<div style={{ marginTop: '12px' }}>
<Paragraph>
<Text strong></Text>
<Text type="secondary">{formatDate(updateInfo.publishedAt)}</Text>
</Paragraph>
{updateInfo.releaseNotes && (
<div>
<Text strong></Text>
<div style={{
marginTop: '8px',
padding: '12px',
background: '#f5f5f5',
borderRadius: '4px',
maxHeight: '200px',
overflowY: 'auto'
}}>
<pre style={{
margin: 0,
whiteSpace: 'pre-wrap',
fontFamily: 'inherit'
}}>
{updateInfo.releaseNotes}
</pre>
</div>
</div>
)}
<div style={{ marginTop: '16px' }}>
<Button
type="primary"
icon={<CloudUploadOutlined />}
onClick={handleExecuteUpdate}
>
</Button>
</div>
</div>
}
type="warning"
showIcon
icon={<InfoCircleOutlined />}
/>
)}
{/* 使用提示 */}
{!updateStatus.updating && (
<Alert
message="使用说明"
description={
<ul style={{ marginBottom: 0, paddingLeft: '20px' }}>
<li>"检查更新"</li>
<li>30-60</li>
<li></li>
<li></li>
</ul>
}
type="info"
showIcon
/>
)}
</Space>
</Card>
)
}
export default SystemUpdate