mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-12-18 20:30:08 +08:00
Add latest version check and update prompt to UI
Introduces backend and frontend logic to fetch the latest NapCat version tag from multiple sources, exposes a new API endpoint, and adds a UI prompt to notify users of new versions with an update button. Also includes minor code style improvements in dialog context.
This commit is contained in:
parent
d525f9b03d
commit
173a165c4b
@ -2,6 +2,7 @@ import path from 'node:path';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import { QQVersionConfigType, QQLevel } from './types';
|
import { QQVersionConfigType, QQLevel } from './types';
|
||||||
|
import { RequestUtil } from './request';
|
||||||
|
|
||||||
export async function solveProblem<T extends (...arg: any[]) => any> (func: T, ...args: Parameters<T>): Promise<ReturnType<T> | undefined> {
|
export async function solveProblem<T extends (...arg: any[]) => any> (func: T, ...args: Parameters<T>): Promise<ReturnType<T> | undefined> {
|
||||||
return new Promise<ReturnType<T> | undefined>((resolve) => {
|
return new Promise<ReturnType<T> | undefined>((resolve) => {
|
||||||
@ -211,3 +212,80 @@ export function parseAppidFromMajor (nodeMajor: string): string | undefined {
|
|||||||
|
|
||||||
return 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<boolean> {
|
||||||
|
try {
|
||||||
|
await PromiseTimer(RequestUtil.HttpGetText(url), 5000);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findAvailableUrl (): Promise<string | null> {
|
||||||
|
for (const url of urls) {
|
||||||
|
if (await testUrl(url)) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllTags (): Promise<string[]> {
|
||||||
|
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<string> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -3,12 +3,22 @@ import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
|
|||||||
|
|
||||||
import { sendSuccess } from '@/napcat-webui-backend/src/utils/response';
|
import { sendSuccess } from '@/napcat-webui-backend/src/utils/response';
|
||||||
import { WebUiConfig } from '@/napcat-webui-backend/index';
|
import { WebUiConfig } from '@/napcat-webui-backend/index';
|
||||||
|
import { getLatestTag } from 'napcat-common/src/helper';
|
||||||
|
|
||||||
export const GetNapCatVersion: RequestHandler = (_, res) => {
|
export const GetNapCatVersion: RequestHandler = (_, res) => {
|
||||||
const data = WebUiDataRuntime.GetNapCatVersion();
|
const data = WebUiDataRuntime.GetNapCatVersion();
|
||||||
sendSuccess(res, { version: data });
|
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) => {
|
export const QQVersionHandler: RequestHandler = (_, res) => {
|
||||||
const data = WebUiDataRuntime.getQQVersion();
|
const data = WebUiDataRuntime.getQQVersion();
|
||||||
sendSuccess(res, data);
|
sendSuccess(res, data);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Router } from 'express';
|
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 { StatusRealTimeHandler } from '@/napcat-webui-backend/src/api/Status';
|
||||||
import { GetProxyHandler } from '../api/Proxy';
|
import { GetProxyHandler } from '../api/Proxy';
|
||||||
|
|
||||||
@ -7,6 +7,7 @@ const router = Router();
|
|||||||
// router: 获取nc的package.json信息
|
// router: 获取nc的package.json信息
|
||||||
router.get('/QQVersion', QQVersionHandler);
|
router.get('/QQVersion', QQVersionHandler);
|
||||||
router.get('/GetNapCatVersion', GetNapCatVersion);
|
router.get('/GetNapCatVersion', GetNapCatVersion);
|
||||||
|
router.get('/getLatestTag', getLatestTagHandler);
|
||||||
router.get('/GetSysStatusRealTime', StatusRealTimeHandler);
|
router.get('/GetSysStatusRealTime', StatusRealTimeHandler);
|
||||||
router.get('/proxy', GetProxyHandler);
|
router.get('/proxy', GetProxyHandler);
|
||||||
router.get('/Theme', GetThemeConfigHandler);
|
router.get('/Theme', GetThemeConfigHandler);
|
||||||
|
|||||||
@ -1,13 +1,19 @@
|
|||||||
import { Card, CardBody, CardHeader } from '@heroui/card';
|
import { Card, CardBody, CardHeader } from '@heroui/card';
|
||||||
|
import { Button } from '@heroui/button';
|
||||||
|
import { Chip } from '@heroui/chip';
|
||||||
import { Spinner } from '@heroui/spinner';
|
import { Spinner } from '@heroui/spinner';
|
||||||
|
import { Tooltip } from '@heroui/tooltip';
|
||||||
import { useRequest } from 'ahooks';
|
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 { IoLogoChrome, IoLogoOctocat } from 'react-icons/io';
|
||||||
import { RiMacFill } from 'react-icons/ri';
|
import { RiMacFill } from 'react-icons/ri';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import WebUIManager from '@/controllers/webui_manager';
|
import WebUIManager from '@/controllers/webui_manager';
|
||||||
|
import useDialog from '@/hooks/use-dialog';
|
||||||
|
|
||||||
|
|
||||||
export interface SystemInfoItemProps {
|
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 (
|
||||||
|
<Tooltip content='有新版本可用'>
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
radius='full'
|
||||||
|
color='primary'
|
||||||
|
variant='shadow'
|
||||||
|
className='!w-5 !h-5 !min-w-0 text-small shadow-md'
|
||||||
|
onPress={() => {
|
||||||
|
dialog.confirm({
|
||||||
|
title: '有新版本可用',
|
||||||
|
content: (
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<div className='text-sm space-x-2'>
|
||||||
|
<span>当前版本</span>
|
||||||
|
<Chip color='primary' variant='flat'>
|
||||||
|
{currentVersion}
|
||||||
|
</Chip>
|
||||||
|
</div>
|
||||||
|
<div className='text-sm space-x-2'>
|
||||||
|
<span>最新版本</span>
|
||||||
|
<Chip color='primary'>{latestVersion}</Chip>
|
||||||
|
</div>
|
||||||
|
{updating && (
|
||||||
|
<div className='flex justify-center'>
|
||||||
|
<Spinner size='sm' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
confirmText: updating ? '更新中...' : '更新',
|
||||||
|
onConfirm: async () => {
|
||||||
|
setUpdating(true);
|
||||||
|
toast('更新中,预计需要几分钟,请耐心等待', {
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await WebUIManager.UpdateNapCat();
|
||||||
|
toast.success('更新完成,重启生效', {
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update failed:', error);
|
||||||
|
toast.success('更新异常', {
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setUpdating(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaInfo />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const NapCatVersion = () => {
|
const NapCatVersion = () => {
|
||||||
const {
|
const {
|
||||||
data: packageData,
|
data: packageData,
|
||||||
@ -212,6 +287,7 @@ const NapCatVersion = () => {
|
|||||||
currentVersion
|
currentVersion
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
endContent={<NewVersionTip currentVersion={currentVersion} />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,30 +6,30 @@ import type { ModalProps } from '@/components/modal';
|
|||||||
|
|
||||||
export interface AlertProps
|
export interface AlertProps
|
||||||
extends Omit<ModalProps, 'onCancel' | 'showCancel' | 'cancelText'> {
|
extends Omit<ModalProps, 'onCancel' | 'showCancel' | 'cancelText'> {
|
||||||
onConfirm?: () => void
|
onConfirm?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfirmProps extends ModalProps {
|
export interface ConfirmProps extends ModalProps {
|
||||||
onConfirm?: () => void
|
onConfirm?: () => void;
|
||||||
onCancel?: () => void
|
onCancel?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModalItem extends ModalProps {
|
export interface ModalItem extends ModalProps {
|
||||||
id: number
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DialogContextProps {
|
export interface DialogContextProps {
|
||||||
alert: (config: AlertProps) => void
|
alert: (config: AlertProps) => void;
|
||||||
confirm: (config: ConfirmProps) => void
|
confirm: (config: ConfirmProps) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DialogProviderProps {
|
export interface DialogProviderProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DialogContext = React.createContext<DialogContextProps>({
|
export const DialogContext = React.createContext<DialogContextProps>({
|
||||||
alert: () => {},
|
alert: () => { },
|
||||||
confirm: () => {},
|
confirm: () => { },
|
||||||
});
|
});
|
||||||
|
|
||||||
const DialogProvider: React.FC<DialogProviderProps> = ({ children }) => {
|
const DialogProvider: React.FC<DialogProviderProps> = ({ children }) => {
|
||||||
|
|||||||
@ -48,6 +48,12 @@ export default class WebUIManager {
|
|||||||
return data.data;
|
return data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async getLatestTag () {
|
||||||
|
const { data } =
|
||||||
|
await serverRequest.get<ServerResponse<string>>('/base/getLatestTag');
|
||||||
|
return data.data;
|
||||||
|
}
|
||||||
|
|
||||||
public static async UpdateNapCat () {
|
public static async UpdateNapCat () {
|
||||||
const { data } = await serverRequest.post<ServerResponse<any>>(
|
const { data } = await serverRequest.post<ServerResponse<any>>(
|
||||||
'/UpdateNapCat/update',
|
'/UpdateNapCat/update',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user