feat: Add the ability to get the latest version

This commit is contained in:
shiyu
2025-08-27 15:37:23 +08:00
parent 2f2e1db536
commit fd02a4bbe5
7 changed files with 159 additions and 29 deletions
+36 -3
View File
@@ -1,9 +1,10 @@
import httpx
import time
from fastapi import APIRouter, Depends, Form from fastapi import APIRouter, Depends, Form
from typing import Annotated from typing import Annotated
from services.config import ConfigCenter from services.config import ConfigCenter
from services.auth import get_current_active_user, User, has_users from services.auth import get_current_active_user, User, has_users
from api.response import success from api.response import success
router = APIRouter(prefix="/api/config", tags=["config"]) router = APIRouter(prefix="/api/config", tags=["config"])
@@ -37,9 +38,41 @@ async def get_all_config(
@router.get("/status") @router.get("/status")
async def get_system_status(): async def get_system_status():
system_info = { system_info = {
"version": "1.0.0", "version": "v1.0.0",
"title": await ConfigCenter.get("APP_NAME", "Foxel"), "title": await ConfigCenter.get("APP_NAME", "Foxel"),
"logo": await ConfigCenter.get("APP_LOGO", "/logo.svg"), "logo": await ConfigCenter.get("APP_LOGO", "/logo.svg"),
"is_initialized": await has_users() "is_initialized": await has_users()
} }
return success(system_info) return success(system_info)
latest_version_cache = {
"timestamp": 0,
"data": None
}
@router.get("/latest-version")
async def get_latest_version():
current_time = time.time()
if current_time - latest_version_cache["timestamp"] < 3600 and latest_version_cache["data"]:
return success(latest_version_cache["data"])
try:
async with httpx.AsyncClient(timeout=10.0, proxy="http://127.0.0.1:7897") as client:
resp = await client.get(
"https://api.github.com/repos/DrizzleTime/Foxel/releases/latest",
follow_redirects=True,
)
resp.raise_for_status()
data = resp.json()
version_info = {
"latest_version": data.get("tag_name"),
"body": data.get("body")
}
latest_version_cache["timestamp"] = current_time
latest_version_cache["data"] = version_info
return success(version_info)
except httpx.RequestError as e:
if latest_version_cache["data"]:
return success(latest_version_cache["data"])
return success({"latest_version": None, "body": None})
+1 -1
View File
@@ -28,4 +28,4 @@ class AutomationTaskRead(AutomationTaskBase):
id: int id: int
class Config: class Config:
orm_mode = True from_attributes = True
+1 -1
View File
@@ -17,7 +17,7 @@ class LoggingMiddleware(BaseHTTPMiddleware):
try: try:
if token_str and token_str.startswith("Bearer "): if token_str and token_str.startswith("Bearer "):
token = token_str.split(" ")[1] token = token_str.split(" ")[1]
payload = jwt.decode(token, ConfigCenter.get_secret_key("SECRET_KEY"), algorithms=[ALGORITHM]) payload = jwt.decode(token, await ConfigCenter.get_secret_key("SECRET_KEY"), algorithms=[ALGORITHM])
username = payload.get("sub") username = payload.get("sub")
if username: if username:
user_account = await UserAccount.get_or_none(username=username) user_account = await UserAccount.get_or_none(username=username)
+4 -1
View File
@@ -11,6 +11,7 @@
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-markdown": "^10.1.0",
"react-router": "^7.8.0", "react-router": "^7.8.0",
}, },
"devDependencies": { "devDependencies": {
@@ -751,7 +752,7 @@
"react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
"react-markdown": ["react-markdown@9.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw=="], "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
@@ -899,6 +900,8 @@
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"@uiw/react-markdown-preview/react-markdown": ["react-markdown@9.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw=="],
"@uiw/react-markdown-preview/rehype-prism-plus": ["rehype-prism-plus@2.0.0", "", { "dependencies": { "hast-util-to-string": "^3.0.0", "parse-numeric-range": "^1.3.0", "refractor": "^4.8.0", "rehype-parse": "^9.0.0", "unist-util-filter": "^5.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-FeM/9V2N7EvDZVdR2dqhAzlw5YI49m9Tgn7ZrYJeYHIahM6gcXpH0K1y2gNnKanZCydOMluJvX2cB9z3lhY8XQ=="], "@uiw/react-markdown-preview/rehype-prism-plus": ["rehype-prism-plus@2.0.0", "", { "dependencies": { "hast-util-to-string": "^3.0.0", "parse-numeric-range": "^1.3.0", "refractor": "^4.8.0", "rehype-parse": "^9.0.0", "unist-util-filter": "^5.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-FeM/9V2N7EvDZVdR2dqhAzlw5YI49m9Tgn7ZrYJeYHIahM6gcXpH0K1y2gNnKanZCydOMluJvX2cB9z3lhY8XQ=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
+1
View File
@@ -17,6 +17,7 @@
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-markdown": "^10.1.0",
"react-router": "^7.8.0" "react-router": "^7.8.0"
}, },
"devDependencies": { "devDependencies": {
+4
View File
@@ -25,3 +25,7 @@ export interface SystemStatus {
export async function status() { export async function status() {
return request<SystemStatus>('/config/status'); return request<SystemStatus>('/config/status');
} }
export async function getLatestVersion() {
return request<{ latest_version: string | null, body: string | null }>('/config/latest-version');
}
+112 -23
View File
@@ -1,11 +1,19 @@
import { Layout, Menu, theme, Button, Modal } from 'antd'; import { Layout, Menu, theme, Button, Modal, Tag, Tooltip } from 'antd';
import { navGroups } from './nav.ts'; import { navGroups } from './nav.ts';
import type { NavItem, NavGroup } from './nav.ts'; import type { NavItem, NavGroup } from './nav.ts';
import { memo, useState } from 'react'; import { memo, useEffect, useState } from 'react';
import { useSystemStatus } from '../contexts/SystemContext.tsx'; import { useSystemStatus } from '../contexts/SystemContext.tsx';
import { GithubOutlined, MenuFoldOutlined, SendOutlined, WechatOutlined } from '@ant-design/icons'; import {
CheckCircleOutlined,
GithubOutlined,
MenuFoldOutlined,
SendOutlined,
WechatOutlined,
WarningOutlined
} from '@ant-design/icons';
import '../styles/sider-menu.css'; import '../styles/sider-menu.css';
import { getLatestVersion } from '../api/config.ts';
import ReactMarkdown from 'react-markdown';
const { Sider } = Layout; const { Sider } = Layout;
export interface SideNavProps { export interface SideNavProps {
@@ -19,6 +27,28 @@ const SideNav = memo(function SideNav({ collapsed, activeKey, onChange, onToggle
const status = useSystemStatus(); const status = useSystemStatus();
const { token } = theme.useToken(); const { token } = theme.useToken();
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [isVersionModalOpen, setIsVersionModalOpen] = useState(false);
const [latestVersion, setLatestVersion] = useState<{
version: string;
body: string;
} | null>(null);
useEffect(() => {
getLatestVersion().then(resp => {
if (resp.latest_version && resp.body) {
setLatestVersion({
version: resp.latest_version,
body: resp.body
});
}
});
}, []);
const showVersionModal = () => {
setIsVersionModalOpen(true);
};
const hasUpdate = latestVersion && latestVersion.version !== status?.version;
return ( return (
<> <>
<Sider <Sider
@@ -105,29 +135,72 @@ const SideNav = memo(function SideNav({ collapsed, activeKey, onChange, onToggle
width: '100%', width: '100%',
padding: '12px 8px', padding: '12px 8px',
display: 'flex', display: 'flex',
justifyContent: 'center', flexDirection: 'column',
gap: 8, alignItems: 'center',
gap: 12,
flexShrink: 0, flexShrink: 0,
borderTop: `1px solid ${token.colorBorderSecondary}` borderTop: `1px solid ${token.colorBorderSecondary}`
}} }}
> >
<Button <div style={{
shape="circle" fontSize: 12,
icon={<GithubOutlined />} color: token.colorTextSecondary,
href="https://github.com/DrizzleTime/Foxel" textAlign: 'center',
target="_blank" height: 22,
/> display: 'flex',
<Button alignItems: 'center',
shape="circle" justifyContent: 'center',
icon={<WechatOutlined />} cursor: 'pointer'
onClick={() => setIsModalOpen(true)} }} onClick={showVersionModal}>
/> {hasUpdate ? (
<Button <Tooltip title={`发现新版本: ${latestVersion?.version}`} placement={collapsed ? 'right' : 'top'}>
shape="circle" <a href="https://github.com/DrizzleTime/Foxel/releases" target="_blank" rel="noopener noreferrer"
icon={<SendOutlined />} style={{ textDecoration: 'none' }}>
href="https://t.me/+thDsBfyqJxZkNTU1" {collapsed ? (
target="_blank" <Tag icon={<WarningOutlined />} color="warning" style={{ marginInlineEnd: 0 }} />
/> ) : (
<Tag icon={<WarningOutlined />} color="warning">
{status?.version} - [{latestVersion?.version}]
</Tag>
)}
</a>
</Tooltip>
) : (
latestVersion ? (
<Tooltip title={`当前为最新版: ${status?.version}`} placement={collapsed ? 'right' : 'top'}>
{collapsed ? (
<Tag icon={<CheckCircleOutlined />} color="success" style={{ marginInlineEnd: 0 }} />
) : (
<Tag icon={<CheckCircleOutlined />} color="success">
</Tag>
)}
</Tooltip>
) : (
collapsed ? null : <Tag>{status?.version}</Tag>
)
)}
</div>
<div style={{ display: 'flex', flexDirection: collapsed ? 'column' : 'row', gap: 8 }}>
<Button
shape="circle"
icon={<GithubOutlined />}
href="https://github.com/DrizzleTime/Foxel"
target="_blank"
/>
<Button
shape="circle"
icon={<WechatOutlined />}
onClick={() => setIsModalOpen(true)}
/>
<Button
shape="circle"
icon={<SendOutlined />}
href="https://t.me/+thDsBfyqJxZkNTU1"
target="_blank"
/>
</div>
</div> </div>
</Sider> </Sider>
<Modal <Modal
@@ -147,6 +220,22 @@ const SideNav = memo(function SideNav({ collapsed, activeKey, onChange, onToggle
</div> </div>
</div> </div>
</Modal> </Modal>
<Modal
open={isVersionModalOpen}
onCancel={() => setIsVersionModalOpen(false)}
title="版本信息"
footer={null}
>
<div>
<p>: {status?.version}</p>
{latestVersion && (
<div>
<p>: {latestVersion.version}</p>
<ReactMarkdown>{latestVersion.body}</ReactMarkdown>
</div>
)}
</div>
</Modal>
</> </>
); );
}); });