mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-10 11:42:27 +08:00
feat: 添加版本更新检查功能
- 后端新增 /api/admin/system/check-update 接口,从 GitHub Tags 获取最新版本 - 前端新增 UpdateDialog 组件,管理员登录后自动检查更新并弹窗提示 - 同一会话内只检查一次,点击"稍后提醒"后 24 小时内不再提示 - CI 和 deploy.sh 自动生成 _version.py 版本文件
This commit is contained in:
23
.github/workflows/docker-publish.yml
vendored
23
.github/workflows/docker-publish.yml
vendored
@@ -146,10 +146,33 @@ jobs:
|
|||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=sha,prefix=
|
type=sha,prefix=
|
||||||
|
|
||||||
|
- name: Extract version from tag
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
# 从 tag 提取版本号,如 v0.2.5 -> 0.2.5
|
||||||
|
VERSION="${GITHUB_REF#refs/tags/v}"
|
||||||
|
if [ "$VERSION" = "$GITHUB_REF" ]; then
|
||||||
|
# 不是 tag 触发,使用 git describe
|
||||||
|
VERSION=$(git describe --tags --always | sed 's/^v//')
|
||||||
|
fi
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Extracted version: $VERSION"
|
||||||
|
|
||||||
- name: Update Dockerfile.app to use registry base image
|
- name: Update Dockerfile.app to use registry base image
|
||||||
run: |
|
run: |
|
||||||
sed -i "s|FROM aether-base:latest AS builder|FROM ${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}:latest AS builder|g" Dockerfile.app
|
sed -i "s|FROM aether-base:latest AS builder|FROM ${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}:latest AS builder|g" Dockerfile.app
|
||||||
|
|
||||||
|
- name: Generate version file
|
||||||
|
run: |
|
||||||
|
# 生成 _version.py 文件
|
||||||
|
cat > src/_version.py << EOF
|
||||||
|
# Auto-generated by CI
|
||||||
|
__version__ = '${{ steps.version.outputs.version }}'
|
||||||
|
__version_tuple__ = tuple(int(x) for x in '${{ steps.version.outputs.version }}'.split('.') if x.isdigit())
|
||||||
|
version = __version__
|
||||||
|
version_tuple = __version_tuple__
|
||||||
|
EOF
|
||||||
|
|
||||||
- name: Build and push app image
|
- name: Build and push app image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -224,3 +224,6 @@ extracted_*.ts
|
|||||||
.deps-hash
|
.deps-hash
|
||||||
.code-hash
|
.code-hash
|
||||||
.migration-hash
|
.migration-hash
|
||||||
|
|
||||||
|
# Version file (auto-generated by hatch-vcs)
|
||||||
|
src/_version.py
|
||||||
|
|||||||
19
deploy.sh
19
deploy.sh
@@ -88,9 +88,28 @@ build_base() {
|
|||||||
save_deps_hash
|
save_deps_hash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 生成版本文件
|
||||||
|
generate_version_file() {
|
||||||
|
# 从 git 获取版本号
|
||||||
|
local version
|
||||||
|
version=$(git describe --tags --always 2>/dev/null | sed 's/^v//')
|
||||||
|
if [ -z "$version" ]; then
|
||||||
|
version="unknown"
|
||||||
|
fi
|
||||||
|
echo ">>> Generating version file: $version"
|
||||||
|
cat > src/_version.py << EOF
|
||||||
|
# Auto-generated by deploy.sh - do not edit
|
||||||
|
__version__ = '$version'
|
||||||
|
__version_tuple__ = tuple(int(x) for x in '$version'.split('-')[0].split('.') if x.isdigit())
|
||||||
|
version = __version__
|
||||||
|
version_tuple = __version_tuple__
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
# 构建应用镜像
|
# 构建应用镜像
|
||||||
build_app() {
|
build_app() {
|
||||||
echo ">>> Building app image (code only)..."
|
echo ">>> Building app image (code only)..."
|
||||||
|
generate_version_file
|
||||||
docker build -f Dockerfile.app.local -t aether-app:latest .
|
docker build -f Dockerfile.app.local -t aether-app:latest .
|
||||||
save_code_hash
|
save_code_hash
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,6 +159,15 @@ export interface EmailTemplateResetResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查更新响应
|
||||||
|
export interface CheckUpdateResponse {
|
||||||
|
current_version: string
|
||||||
|
latest_version: string | null
|
||||||
|
has_update: boolean
|
||||||
|
release_url: string | null
|
||||||
|
error: string | null
|
||||||
|
}
|
||||||
|
|
||||||
// LDAP 配置响应
|
// LDAP 配置响应
|
||||||
export interface LdapConfigResponse {
|
export interface LdapConfigResponse {
|
||||||
server_url: string | null
|
server_url: string | null
|
||||||
@@ -526,6 +535,14 @@ export const adminApi = {
|
|||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 检查系统更新
|
||||||
|
async checkUpdate(): Promise<CheckUpdateResponse> {
|
||||||
|
const response = await apiClient.get<CheckUpdateResponse>(
|
||||||
|
'/api/admin/system/check-update'
|
||||||
|
)
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
|
||||||
// LDAP 配置相关
|
// LDAP 配置相关
|
||||||
// 获取 LDAP 配置
|
// 获取 LDAP 配置
|
||||||
async getLdapConfig(): Promise<LdapConfigResponse> {
|
async getLdapConfig(): Promise<LdapConfigResponse> {
|
||||||
|
|||||||
112
frontend/src/components/common/UpdateDialog.vue
Normal file
112
frontend/src/components/common/UpdateDialog.vue
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model="isOpen"
|
||||||
|
size="md"
|
||||||
|
title=""
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center text-center py-2">
|
||||||
|
<!-- Logo -->
|
||||||
|
<HeaderLogo
|
||||||
|
size="h-16 w-16"
|
||||||
|
class-name="text-primary"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<h2 class="text-xl font-semibold text-foreground mt-4 mb-2">
|
||||||
|
发现新版本
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Version Info -->
|
||||||
|
<div class="flex items-center gap-3 mb-4">
|
||||||
|
<span class="px-3 py-1.5 rounded-lg bg-muted text-sm font-mono text-muted-foreground">
|
||||||
|
v{{ currentVersion }}
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
class="h-4 w-4 text-muted-foreground"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13 7l5 5m0 0l-5 5m5-5H6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="px-3 py-1.5 rounded-lg bg-primary/10 text-sm font-mono font-medium text-primary">
|
||||||
|
v{{ latestVersion }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<p class="text-sm text-muted-foreground max-w-xs">
|
||||||
|
新版本已发布,建议更新以获得最新功能和安全修复
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex w-full gap-3">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
class="flex-1"
|
||||||
|
@click="handleLater"
|
||||||
|
>
|
||||||
|
稍后提醒
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
class="flex-1"
|
||||||
|
@click="handleViewRelease"
|
||||||
|
>
|
||||||
|
查看更新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { Dialog } from '@/components/ui'
|
||||||
|
import Button from '@/components/ui/button.vue'
|
||||||
|
import HeaderLogo from '@/components/HeaderLogo.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
currentVersion: string
|
||||||
|
latestVersion: string
|
||||||
|
releaseUrl: string | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: boolean]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isOpen = ref(props.modelValue)
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
isOpen.value = val
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(isOpen, (val) => {
|
||||||
|
emit('update:modelValue', val)
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleLater() {
|
||||||
|
// 记录忽略的版本,24小时内不再提醒
|
||||||
|
const ignoreKey = 'aether_update_ignore'
|
||||||
|
const ignoreData = {
|
||||||
|
version: props.latestVersion,
|
||||||
|
until: Date.now() + 24 * 60 * 60 * 1000 // 24小时
|
||||||
|
}
|
||||||
|
localStorage.setItem(ignoreKey, JSON.stringify(ignoreData))
|
||||||
|
isOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleViewRelease() {
|
||||||
|
if (props.releaseUrl) {
|
||||||
|
window.open(props.releaseUrl, '_blank')
|
||||||
|
}
|
||||||
|
isOpen.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -295,6 +295,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<RouterView />
|
<RouterView />
|
||||||
|
|
||||||
|
<!-- 更新提示弹窗 -->
|
||||||
|
<UpdateDialog
|
||||||
|
v-if="updateInfo"
|
||||||
|
v-model="showUpdateDialog"
|
||||||
|
:current-version="updateInfo.current_version"
|
||||||
|
:latest-version="updateInfo.latest_version || ''"
|
||||||
|
:release-url="updateInfo.release_url"
|
||||||
|
/>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -304,10 +313,12 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useDarkMode } from '@/composables/useDarkMode'
|
import { useDarkMode } from '@/composables/useDarkMode'
|
||||||
import { isDemoMode } from '@/config/demo'
|
import { isDemoMode } from '@/config/demo'
|
||||||
|
import { adminApi, type CheckUpdateResponse } from '@/api/admin'
|
||||||
import Button from '@/components/ui/button.vue'
|
import Button from '@/components/ui/button.vue'
|
||||||
import AppShell from '@/components/layout/AppShell.vue'
|
import AppShell from '@/components/layout/AppShell.vue'
|
||||||
import SidebarNav from '@/components/layout/SidebarNav.vue'
|
import SidebarNav from '@/components/layout/SidebarNav.vue'
|
||||||
import HeaderLogo from '@/components/HeaderLogo.vue'
|
import HeaderLogo from '@/components/HeaderLogo.vue'
|
||||||
|
import UpdateDialog from '@/components/common/UpdateDialog.vue'
|
||||||
import {
|
import {
|
||||||
Home,
|
Home,
|
||||||
Users,
|
Users,
|
||||||
@@ -345,17 +356,67 @@ const showAuthError = ref(false)
|
|||||||
const mobileMenuOpen = ref(false)
|
const mobileMenuOpen = ref(false)
|
||||||
let authCheckInterval: number | null = null
|
let authCheckInterval: number | null = null
|
||||||
|
|
||||||
|
// 更新检查相关
|
||||||
|
const showUpdateDialog = ref(false)
|
||||||
|
const updateInfo = ref<CheckUpdateResponse | null>(null)
|
||||||
|
|
||||||
// 路由变化时自动关闭移动端菜单
|
// 路由变化时自动关闭移动端菜单
|
||||||
watch(() => route.path, () => {
|
watch(() => route.path, () => {
|
||||||
mobileMenuOpen.value = false
|
mobileMenuOpen.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 检查是否应该显示更新提示
|
||||||
|
function shouldShowUpdatePrompt(latestVersion: string): boolean {
|
||||||
|
const ignoreKey = 'aether_update_ignore'
|
||||||
|
const ignoreData = localStorage.getItem(ignoreKey)
|
||||||
|
if (!ignoreData) return true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { version, until } = JSON.parse(ignoreData)
|
||||||
|
// 如果忽略的是同一版本且未过期,则不显示
|
||||||
|
if (version === latestVersion && Date.now() < until) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 解析失败,显示提示
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查更新
|
||||||
|
async function checkForUpdate() {
|
||||||
|
// 只有管理员才检查更新
|
||||||
|
if (authStore.user?.role !== 'admin') return
|
||||||
|
|
||||||
|
// 同一会话内只检查一次
|
||||||
|
const sessionKey = 'aether_update_checked'
|
||||||
|
if (sessionStorage.getItem(sessionKey)) return
|
||||||
|
sessionStorage.setItem(sessionKey, '1')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await adminApi.checkUpdate()
|
||||||
|
if (result.has_update && result.latest_version) {
|
||||||
|
if (shouldShowUpdatePrompt(result.latest_version)) {
|
||||||
|
updateInfo.value = result
|
||||||
|
showUpdateDialog.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 静默失败,不影响用户体验
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
authCheckInterval = setInterval(() => {
|
authCheckInterval = setInterval(() => {
|
||||||
if (authStore.user && !authStore.token) {
|
if (authStore.user && !authStore.token) {
|
||||||
showAuthError.value = true
|
showAuthError.value = true
|
||||||
}
|
}
|
||||||
}, 5000)
|
}, 5000)
|
||||||
|
|
||||||
|
// 延迟检查更新,避免影响页面加载
|
||||||
|
setTimeout(() => {
|
||||||
|
checkForUpdate()
|
||||||
|
}, 2000)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
# file generated by setuptools-scm
|
|
||||||
# don't change, don't track in version control
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"__version__",
|
|
||||||
"__version_tuple__",
|
|
||||||
"version",
|
|
||||||
"version_tuple",
|
|
||||||
"__commit_id__",
|
|
||||||
"commit_id",
|
|
||||||
]
|
|
||||||
|
|
||||||
TYPE_CHECKING = False
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from typing import Tuple
|
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
||||||
COMMIT_ID = Union[str, None]
|
|
||||||
else:
|
|
||||||
VERSION_TUPLE = object
|
|
||||||
COMMIT_ID = object
|
|
||||||
|
|
||||||
version: str
|
|
||||||
__version__: str
|
|
||||||
__version_tuple__: VERSION_TUPLE
|
|
||||||
version_tuple: VERSION_TUPLE
|
|
||||||
commit_id: COMMIT_ID
|
|
||||||
__commit_id__: COMMIT_ID
|
|
||||||
|
|
||||||
__version__ = version = '0.2.5'
|
|
||||||
__version_tuple__ = version_tuple = (0, 2, 5)
|
|
||||||
|
|
||||||
__commit_id__ = commit_id = None
|
|
||||||
@@ -42,6 +42,42 @@ def _get_version_from_git() -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_current_version() -> str:
|
||||||
|
"""获取当前版本号"""
|
||||||
|
version = _get_version_from_git()
|
||||||
|
if version:
|
||||||
|
return version
|
||||||
|
try:
|
||||||
|
from src._version import __version__
|
||||||
|
|
||||||
|
return __version__
|
||||||
|
except ImportError:
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_version(version_str: str) -> tuple:
|
||||||
|
"""解析版本号为可比较的元组,支持 3-4 段版本号
|
||||||
|
|
||||||
|
例如:
|
||||||
|
- '0.2.5' -> (0, 2, 5, 0)
|
||||||
|
- '0.2.5.1' -> (0, 2, 5, 1)
|
||||||
|
- 'v0.2.5-4-g1234567' -> (0, 2, 5, 0)
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
version_str = version_str.lstrip("v")
|
||||||
|
main_version = re.split(r"[-+]", version_str)[0]
|
||||||
|
try:
|
||||||
|
parts = main_version.split(".")
|
||||||
|
# 标准化为 4 段,便于比较
|
||||||
|
int_parts = [int(p) for p in parts]
|
||||||
|
while len(int_parts) < 4:
|
||||||
|
int_parts.append(0)
|
||||||
|
return tuple(int_parts[:4])
|
||||||
|
except ValueError:
|
||||||
|
return (0, 0, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/version")
|
@router.get("/version")
|
||||||
async def get_system_version():
|
async def get_system_version():
|
||||||
"""
|
"""
|
||||||
@@ -52,18 +88,111 @@ async def get_system_version():
|
|||||||
**返回字段**:
|
**返回字段**:
|
||||||
- `version`: 版本号字符串
|
- `version`: 版本号字符串
|
||||||
"""
|
"""
|
||||||
# 优先从 git 获取
|
return {"version": _get_current_version()}
|
||||||
version = _get_version_from_git()
|
|
||||||
if version:
|
|
||||||
return {"version": version}
|
@router.get("/check-update")
|
||||||
|
async def check_update():
|
||||||
|
"""
|
||||||
|
检查系统更新
|
||||||
|
|
||||||
|
从 GitHub Tags 获取最新版本并与当前版本对比。
|
||||||
|
|
||||||
|
**返回字段**:
|
||||||
|
- `current_version`: 当前版本号
|
||||||
|
- `latest_version`: 最新版本号
|
||||||
|
- `has_update`: 是否有更新可用
|
||||||
|
- `release_url`: 最新版本的 GitHub 页面链接
|
||||||
|
"""
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from src.clients.http_client import HTTPClientPool
|
||||||
|
|
||||||
|
current_version = _get_current_version()
|
||||||
|
github_repo = "Aethersailor/Aether"
|
||||||
|
github_tags_url = f"https://api.github.com/repos/{github_repo}/tags"
|
||||||
|
|
||||||
# 回退到静态版本文件
|
|
||||||
try:
|
try:
|
||||||
from src._version import __version__
|
async with HTTPClientPool.get_temp_client(
|
||||||
|
timeout=httpx.Timeout(connect=5.0, read=10.0, write=5.0, pool=5.0)
|
||||||
|
) as client:
|
||||||
|
response = await client.get(
|
||||||
|
github_tags_url,
|
||||||
|
headers={
|
||||||
|
"Accept": "application/vnd.github.v3+json",
|
||||||
|
"User-Agent": f"Aether/{current_version}",
|
||||||
|
},
|
||||||
|
params={"per_page": 10},
|
||||||
|
)
|
||||||
|
|
||||||
return {"version": __version__}
|
if response.status_code != 200:
|
||||||
except ImportError:
|
return {
|
||||||
return {"version": "unknown"}
|
"current_version": current_version,
|
||||||
|
"latest_version": None,
|
||||||
|
"has_update": False,
|
||||||
|
"release_url": None,
|
||||||
|
"error": f"GitHub API 返回错误: {response.status_code}",
|
||||||
|
}
|
||||||
|
|
||||||
|
tags = response.json()
|
||||||
|
if not tags:
|
||||||
|
return {
|
||||||
|
"current_version": current_version,
|
||||||
|
"latest_version": None,
|
||||||
|
"has_update": False,
|
||||||
|
"release_url": None,
|
||||||
|
"error": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 找到最新的版本 tag(按版本号排序,而非时间)
|
||||||
|
version_tags = []
|
||||||
|
for tag in tags:
|
||||||
|
tag_name = tag.get("name", "")
|
||||||
|
if tag_name.startswith("v") or tag_name[0].isdigit():
|
||||||
|
version_tags.append((tag_name, _parse_version(tag_name)))
|
||||||
|
|
||||||
|
if not version_tags:
|
||||||
|
return {
|
||||||
|
"current_version": current_version,
|
||||||
|
"latest_version": None,
|
||||||
|
"has_update": False,
|
||||||
|
"release_url": None,
|
||||||
|
"error": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 按版本号排序,取最大的
|
||||||
|
version_tags.sort(key=lambda x: x[1], reverse=True)
|
||||||
|
latest_tag = version_tags[0][0]
|
||||||
|
latest_version = latest_tag.lstrip("v")
|
||||||
|
|
||||||
|
current_tuple = _parse_version(current_version)
|
||||||
|
latest_tuple = _parse_version(latest_version)
|
||||||
|
has_update = latest_tuple > current_tuple
|
||||||
|
|
||||||
|
return {
|
||||||
|
"current_version": current_version,
|
||||||
|
"latest_version": latest_version,
|
||||||
|
"has_update": has_update,
|
||||||
|
"release_url": f"https://github.com/{github_repo}/releases/tag/{latest_tag}",
|
||||||
|
"error": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
except httpx.TimeoutException:
|
||||||
|
return {
|
||||||
|
"current_version": current_version,
|
||||||
|
"latest_version": None,
|
||||||
|
"has_update": False,
|
||||||
|
"release_url": None,
|
||||||
|
"error": "检查更新超时",
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"current_version": current_version,
|
||||||
|
"latest_version": None,
|
||||||
|
"has_update": False,
|
||||||
|
"release_url": None,
|
||||||
|
"error": f"检查更新失败: {str(e)}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
pipeline = ApiRequestPipeline()
|
pipeline = ApiRequestPipeline()
|
||||||
|
|||||||
Reference in New Issue
Block a user