diff --git a/packages/napcat-common/src/helper.ts b/packages/napcat-common/src/helper.ts index c1809c0c..fb42b611 100644 --- a/packages/napcat-common/src/helper.ts +++ b/packages/napcat-common/src/helper.ts @@ -2,6 +2,7 @@ import path from 'node:path'; import fs from 'fs'; import os from 'node:os'; import { QQVersionConfigType, QQLevel } from './types'; +import { RequestUtil } from './request'; export async function solveProblem any> (func: T, ...args: Parameters): Promise | undefined> { return new Promise | undefined>((resolve) => { @@ -211,3 +212,80 @@ export function parseAppidFromMajor (nodeMajor: string): string | undefined { return undefined; } + +const baseUrl = 'https://github.com/NapNeko/NapCatQQ.git/info/refs?service=git-upload-pack'; +const urls = [ + 'https://j.1win.ggff.net/' + baseUrl, + 'https://git.yylx.win/' + baseUrl, + 'https://ghfile.geekertao.top/' + baseUrl, + 'https://gh-proxy.net/' + baseUrl, + 'https://ghm.078465.xyz/' + baseUrl, + 'https://gitproxy.127731.xyz/' + baseUrl, + 'https://jiashu.1win.eu.org/' + baseUrl, + baseUrl, +]; + +async function testUrl (url: string): Promise { + try { + await PromiseTimer(RequestUtil.HttpGetText(url), 5000); + return true; + } catch { + return false; + } +} + +async function findAvailableUrl (): Promise { + for (const url of urls) { + if (await testUrl(url)) { + return url; + } + } + return null; +} + +export async function getAllTags (): Promise { + const availableUrl = await findAvailableUrl(); + if (!availableUrl) { + throw new Error('No available URL for fetching tags'); + } + const raw = await RequestUtil.HttpGetText(availableUrl); + return raw + .split('\n') + .map(line => { + const match = line.match(/refs\/tags\/(.+)$/); + return match ? match[1] : null; + }) + .filter(tag => tag !== null && !tag!.endsWith('^{}')) as string[]; +} + + +export async function getLatestTag (): Promise { + const tags = await getAllTags(); + + tags.sort((a, b) => compareVersion(a, b)); + + const latest = tags.at(-1); + if (!latest) { + throw new Error('No tags found'); + } + return latest; +} + + +function compareVersion (a: string, b: string): number { + const normalize = (v: string) => + v.replace(/^v/, '') // 去掉开头的 v + .split('.') + .map(n => parseInt(n) || 0); + + const pa = normalize(a); + const pb = normalize(b); + const len = Math.max(pa.length, pb.length); + + for (let i = 0; i < len; i++) { + const na = pa[i] || 0; + const nb = pb[i] || 0; + if (na !== nb) return na - nb; + } + return 0; +} diff --git a/packages/napcat-webui-backend/src/api/BaseInfo.ts b/packages/napcat-webui-backend/src/api/BaseInfo.ts index 0cabb170..11e5bfd6 100644 --- a/packages/napcat-webui-backend/src/api/BaseInfo.ts +++ b/packages/napcat-webui-backend/src/api/BaseInfo.ts @@ -3,12 +3,22 @@ import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data'; import { sendSuccess } from '@/napcat-webui-backend/src/utils/response'; import { WebUiConfig } from '@/napcat-webui-backend/index'; +import { getLatestTag } from 'napcat-common/src/helper'; export const GetNapCatVersion: RequestHandler = (_, res) => { const data = WebUiDataRuntime.GetNapCatVersion(); sendSuccess(res, { version: data }); }; +export const getLatestTagHandler: RequestHandler = async (_, res) => { + try { + const latestTag = await getLatestTag(); + sendSuccess(res, latestTag); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch latest tag' }); + } +}; + export const QQVersionHandler: RequestHandler = (_, res) => { const data = WebUiDataRuntime.getQQVersion(); sendSuccess(res, data); diff --git a/packages/napcat-webui-backend/src/router/Base.ts b/packages/napcat-webui-backend/src/router/Base.ts index e1935fea..42469ee0 100644 --- a/packages/napcat-webui-backend/src/router/Base.ts +++ b/packages/napcat-webui-backend/src/router/Base.ts @@ -1,5 +1,5 @@ import { Router } from 'express'; -import { GetThemeConfigHandler, GetNapCatVersion, QQVersionHandler, SetThemeConfigHandler } from '../api/BaseInfo'; +import { GetThemeConfigHandler, GetNapCatVersion, QQVersionHandler, SetThemeConfigHandler, getLatestTagHandler } from '../api/BaseInfo'; import { StatusRealTimeHandler } from '@/napcat-webui-backend/src/api/Status'; import { GetProxyHandler } from '../api/Proxy'; @@ -7,6 +7,7 @@ const router = Router(); // router: 获取nc的package.json信息 router.get('/QQVersion', QQVersionHandler); router.get('/GetNapCatVersion', GetNapCatVersion); +router.get('/getLatestTag', getLatestTagHandler); router.get('/GetSysStatusRealTime', StatusRealTimeHandler); router.get('/proxy', GetProxyHandler); router.get('/Theme', GetThemeConfigHandler); diff --git a/packages/napcat-webui-frontend/src/components/system_info.tsx b/packages/napcat-webui-frontend/src/components/system_info.tsx index 29d414ce..a1964c12 100644 --- a/packages/napcat-webui-frontend/src/components/system_info.tsx +++ b/packages/napcat-webui-frontend/src/components/system_info.tsx @@ -1,13 +1,19 @@ import { Card, CardBody, CardHeader } from '@heroui/card'; +import { Button } from '@heroui/button'; +import { Chip } from '@heroui/chip'; import { Spinner } from '@heroui/spinner'; +import { Tooltip } from '@heroui/tooltip'; import { useRequest } from 'ahooks'; -import { FaCircleInfo, FaQq } from 'react-icons/fa6'; +import { FaCircleInfo, FaInfo, FaQq } from 'react-icons/fa6'; import { IoLogoChrome, IoLogoOctocat } from 'react-icons/io'; import { RiMacFill } from 'react-icons/ri'; +import { useState } from 'react'; +import toast from 'react-hot-toast'; import WebUIManager from '@/controllers/webui_manager'; +import useDialog from '@/hooks/use-dialog'; export interface SystemInfoItemProps { @@ -186,6 +192,75 @@ export interface NewVersionTipProps { // ); // }; +const NewVersionTip = (props: NewVersionTipProps) => { + const { currentVersion } = props; + const dialog = useDialog(); + const { data: latestVersion, error } = useRequest(WebUIManager.getLatestTag); + const [updating, setUpdating] = useState(false); + + if (error || !latestVersion || !currentVersion || latestVersion === currentVersion) { + return null; + } + + return ( + + + + ); +}; + const NapCatVersion = () => { const { data: packageData, @@ -212,6 +287,7 @@ const NapCatVersion = () => { currentVersion ) } + endContent={} /> ); }; diff --git a/packages/napcat-webui-frontend/src/contexts/dialog.tsx b/packages/napcat-webui-frontend/src/contexts/dialog.tsx index 1c7e7f0e..a76754d3 100644 --- a/packages/napcat-webui-frontend/src/contexts/dialog.tsx +++ b/packages/napcat-webui-frontend/src/contexts/dialog.tsx @@ -6,30 +6,30 @@ import type { ModalProps } from '@/components/modal'; export interface AlertProps extends Omit { - onConfirm?: () => void + onConfirm?: () => void; } export interface ConfirmProps extends ModalProps { - onConfirm?: () => void - onCancel?: () => void + onConfirm?: () => void; + onCancel?: () => void; } export interface ModalItem extends ModalProps { - id: number + id: number; } export interface DialogContextProps { - alert: (config: AlertProps) => void - confirm: (config: ConfirmProps) => void + alert: (config: AlertProps) => void; + confirm: (config: ConfirmProps) => void; } export interface DialogProviderProps { - children: React.ReactNode + children: React.ReactNode; } export const DialogContext = React.createContext({ - alert: () => {}, - confirm: () => {}, + alert: () => { }, + confirm: () => { }, }); const DialogProvider: React.FC = ({ children }) => { diff --git a/packages/napcat-webui-frontend/src/controllers/webui_manager.ts b/packages/napcat-webui-frontend/src/controllers/webui_manager.ts index 18812fdc..5a8100cb 100644 --- a/packages/napcat-webui-frontend/src/controllers/webui_manager.ts +++ b/packages/napcat-webui-frontend/src/controllers/webui_manager.ts @@ -48,6 +48,12 @@ export default class WebUIManager { return data.data; } + public static async getLatestTag () { + const { data } = + await serverRequest.get>('/base/getLatestTag'); + return data.data; + } + public static async UpdateNapCat () { const { data } = await serverRequest.post>( '/UpdateNapCat/update',