feat: 版本徽章新增近3个历史版本在线回退与手动回退指引

后端:
- UpdateService 新增 ListRollbackVersions/RollbackToVersion,回退目标强制
  限定为比当前版本更旧的近3个正式版本(排除当前/更新/预发布/草稿),
  越界返回 400 ROLLBACK_VERSION_NOT_ALLOWED;下载复用既有 HTTPS 域名
  白名单 + checksum 校验 + 原子换二进制管线
- GitHubReleaseClient 新增 FetchRecentReleases(releases 列表 API)
- 新增 GET /admin/system/rollback-versions;POST /admin/system/rollback
  支持可选 {"version"} body,无 body 保持原 .backup 本地回退行为不变
- 端点均在 /admin 组 adminAuth(IsAdmin+TokenVersion)保护内

前端:
- VersionBadge「已是最新版本」状态下新增版本回退面板:radio 卡片选择
  近3个版本,手动回退方式按部署形态 tab 切换(脚本部署 curl 命令 /
  Docker 镜像 tag 指引),支持一键复制;release 构建提供一键回退并
  复用重启流程;源码构建仅提示不支持在线回退
- 下拉根元素重置 whitespace-normal,修复 sidebar-brand 的
  white-space:nowrap 继承导致的长文本溢出裁切

测试:服务层过滤/排序/上限/非法目标拒绝、handler 双模式与鉴权错误映射、
GitHub client 列表拉取、前端 API 请求体行为共 16 个新增用例
This commit is contained in:
shaw
2026-07-09 22:30:04 +08:00
parent 1a3cc2a787
commit 1c2e6503c6
12 changed files with 1064 additions and 37 deletions
@@ -0,0 +1,55 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { get, post } = vi.hoisted(() => ({
get: vi.fn(),
post: vi.fn(),
}))
vi.mock('../client', () => ({
apiClient: {
get,
post,
},
}))
import { getRollbackVersions, rollback, type RollbackVersionInfo } from '@/api/admin/system'
describe('admin system rollback API', () => {
beforeEach(() => {
get.mockReset()
post.mockReset()
})
it('getRollbackVersions fetches the rollback version list', async () => {
const versions: RollbackVersionInfo[] = [
{
version: '0.1.146',
published_at: '2026-07-07T00:00:00Z',
html_url: 'https://github.com/Wei-Shaw/sub2api/releases/tag/v0.1.146'
}
]
get.mockResolvedValue({ data: { versions } })
const result = await getRollbackVersions()
expect(get).toHaveBeenCalledWith('/admin/system/rollback-versions')
expect(result.versions).toEqual(versions)
})
it('rollback posts the target version in the request body', async () => {
post.mockResolvedValue({ data: { message: 'ok', need_restart: true } })
const result = await rollback('0.1.146')
expect(post).toHaveBeenCalledWith('/admin/system/rollback', { version: '0.1.146' })
expect(result.need_restart).toBe(true)
})
it('rollback without a version posts no body (legacy backup rollback)', async () => {
post.mockResolvedValue({ data: { message: 'ok', need_restart: true } })
await rollback()
expect(post).toHaveBeenCalledWith('/admin/system/rollback', undefined)
})
})
+24 -3
View File
@@ -45,6 +45,22 @@ export interface UpdateResult {
need_restart: boolean
}
export interface RollbackVersionInfo {
version: string
published_at: string
html_url: string
}
/**
* Get versions available for rollback (up to 3 versions older than current)
*/
export async function getRollbackVersions(): Promise<{ versions: RollbackVersionInfo[] }> {
const { data } = await apiClient.get<{ versions: RollbackVersionInfo[] }>(
'/admin/system/rollback-versions'
)
return data
}
/**
* Perform system update
* Downloads and applies the latest version
@@ -55,10 +71,14 @@ export async function performUpdate(): Promise<UpdateResult> {
}
/**
* Rollback to previous version
* Rollback to a previous version
* @param version - Target version (e.g. "0.1.146"); omit to restore the local backup binary
*/
export async function rollback(): Promise<UpdateResult> {
const { data } = await apiClient.post<UpdateResult>('/admin/system/rollback')
export async function rollback(version?: string): Promise<UpdateResult> {
const { data } = await apiClient.post<UpdateResult>(
'/admin/system/rollback',
version ? { version } : undefined
)
return data
}
@@ -74,6 +94,7 @@ export const systemAPI = {
getVersion,
checkUpdates,
performUpdate,
getRollbackVersions,
rollback,
restartService
}