Add backend and frontend support for NapCat auto-update

Introduces backend API and router for updating NapCat, including update logic and pending update application on startup. Adds frontend integration with update button and request handling. Refactors system info component to remove legacy new version tip. Updates types and runtime to track working environment for update selection. Implements lazy loading for pty in unixTerminal to avoid early initialization.
This commit is contained in:
手瓜一十雪
2025-11-19 21:05:08 +08:00
parent 907861102b
commit acdd56eb5e
11 changed files with 641 additions and 164 deletions

View File

@@ -1,30 +1,20 @@
import { Button } from '@heroui/button';
import { Card, CardBody, CardHeader } from '@heroui/card';
import { Chip } from '@heroui/chip';
import { Spinner } from '@heroui/spinner';
import { Tooltip } from '@heroui/tooltip';
import { useRequest } from 'ahooks';
import { useEffect } from 'react';
import { BsStars } from 'react-icons/bs';
import { FaCircleInfo, FaInfo, FaQq } from 'react-icons/fa6';
import { FaCircleInfo, FaQq } from 'react-icons/fa6';
import { IoLogoChrome, IoLogoOctocat } from 'react-icons/io';
import { RiMacFill } from 'react-icons/ri';
import useDialog from '@/hooks/use-dialog';
import { request } from '@/utils/request';
import { compareVersion } from '@/utils/version';
import WebUIManager from '@/controllers/webui_manager';
import { GithubRelease } from '@/types/github';
import TailwindMarkdown from './tailwind_markdown';
export interface SystemInfoItemProps {
title: string
icon?: React.ReactNode
value?: React.ReactNode
endContent?: React.ReactNode
title: string;
icon?: React.ReactNode;
value?: React.ReactNode;
endContent?: React.ReactNode;
}
const SystemInfoItem: React.FC<SystemInfoItemProps> = ({
@@ -44,157 +34,157 @@ const SystemInfoItem: React.FC<SystemInfoItemProps> = ({
};
export interface NewVersionTipProps {
currentVersion?: string
currentVersion?: string;
}
const NewVersionTip = (props: NewVersionTipProps) => {
const { currentVersion } = props;
const dialog = useDialog();
const { data: releaseData, error } = useRequest(() =>
request.get<GithubRelease[]>(
'https://api.github.com/repos/NapNeko/NapCatQQ/releases'
)
);
// const NewVersionTip = (props: NewVersionTipProps) => {
// const { currentVersion } = props;
// const dialog = useDialog();
// const { data: releaseData, error } = useRequest(() =>
// request.get<GithubRelease[]>(
// 'https://api.github.com/repos/NapNeko/NapCatQQ/releases'
// )
// );
if (error) {
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.alert({
title: '检查新版本失败',
content: error.message,
});
}}
>
<FaInfo />
</Button>
</Tooltip>
);
}
// if (error) {
// 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.alert({
// title: '检查新版本失败',
// content: error.message,
// });
// }}
// >
// <FaInfo />
// </Button>
// </Tooltip>
// );
// }
const latestVersion = releaseData?.data?.[0]?.tag_name;
// const latestVersion = releaseData?.data?.[0]?.tag_name;
if (!latestVersion || !currentVersion) {
return null;
}
// if (!latestVersion || !currentVersion) {
// return null;
// }
if (compareVersion(latestVersion, currentVersion) <= 0) {
return null;
}
// if (compareVersion(latestVersion, currentVersion) <= 0) {
// return null;
// }
const middleVersions: GithubRelease[] = [];
// const middleVersions: GithubRelease[] = [];
for (let i = 0; i < releaseData.data.length; i++) {
const versionInfo = releaseData.data[i];
if (compareVersion(versionInfo.tag_name, currentVersion) > 0) {
middleVersions.push(versionInfo);
} else {
break;
}
}
// for (let i = 0; i < releaseData.data.length; i++) {
// const versionInfo = releaseData.data[i];
// if (compareVersion(versionInfo.tag_name, currentVersion) > 0) {
// middleVersions.push(versionInfo);
// } else {
// break;
// }
// }
const AISummaryComponent = () => {
const {
data: aiSummaryData,
loading: aiSummaryLoading,
error: aiSummaryError,
run: runAiSummary,
} = useRequest(
(version) =>
request.get<ServerResponse<string | null>>(
`https://release.nc.152710.xyz/?version=${version}`,
{
timeout: 30000,
}
),
{
manual: true,
}
);
// const AISummaryComponent = () => {
// const {
// data: aiSummaryData,
// loading: aiSummaryLoading,
// error: aiSummaryError,
// run: runAiSummary,
// } = useRequest(
// (version) =>
// request.get<ServerResponse<string | null>>(
// `https://release.nc.152710.xyz/?version=${version}`,
// {
// timeout: 30000,
// }
// ),
// {
// manual: true,
// }
// );
useEffect(() => {
runAiSummary(currentVersion);
}, [currentVersion, runAiSummary]);
// useEffect(() => {
// runAiSummary(currentVersion);
// }, [currentVersion, runAiSummary]);
if (aiSummaryLoading) {
return (
<div className='flex justify-center py-1'>
<Spinner size='sm' />
</div>
);
}
if (aiSummaryError) {
return <div className='text-center text-primary-500'>AI </div>;
}
return <span className='text-default-700'>{aiSummaryData?.data.data}</span>;
};
// if (aiSummaryLoading) {
// return (
// <div className='flex justify-center py-1'>
// <Spinner size='sm' />
// </div>
// );
// }
// if (aiSummaryError) {
// return <div className='text-center text-primary-500'>AI 摘要获取失败</div>;
// }
// return <span className='text-default-700'>{aiSummaryData?.data.data}</span>;
// };
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'>
v{currentVersion}
</Chip>
</div>
<div className='text-sm space-x-2'>
<span></span>
<Chip color='primary'>{latestVersion}</Chip>
</div>
<div className='p-2 rounded-md bg-content2 text-sm'>
<div className='text-primary-400 font-bold flex items-center gap-1 mb-1'>
<BsStars />
<span>AI总结</span>
</div>
<AISummaryComponent />
</div>
<div className='text-sm space-y-2 !mt-4'>
{middleVersions.map((versionInfo) => (
<div
key={versionInfo.tag_name}
className='p-4 bg-content1 rounded-md shadow-small'
>
<TailwindMarkdown content={versionInfo.body} />
</div>
))}
</div>
</div>
),
scrollBehavior: 'inside',
size: '3xl',
confirmText: '前往下载',
onConfirm () {
window.open(
'https://github.com/NapNeko/NapCatQQ/releases',
'_blank',
'noopener'
);
},
});
}}
>
<FaInfo />
</Button>
</Tooltip>
);
};
// 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'>
// v{currentVersion}
// </Chip>
// </div>
// <div className='text-sm space-x-2'>
// <span>最新版本</span>
// <Chip color='primary'>{latestVersion}</Chip>
// </div>
// <div className='p-2 rounded-md bg-content2 text-sm'>
// <div className='text-primary-400 font-bold flex items-center gap-1 mb-1'>
// <BsStars />
// <span>AI总结</span>
// </div>
// <AISummaryComponent />
// </div>
// <div className='text-sm space-y-2 !mt-4'>
// {middleVersions.map((versionInfo) => (
// <div
// key={versionInfo.tag_name}
// className='p-4 bg-content1 rounded-md shadow-small'
// >
// <TailwindMarkdown content={versionInfo.body} />
// </div>
// ))}
// </div>
// </div>
// ),
// scrollBehavior: 'inside',
// size: '3xl',
// confirmText: '前往下载',
// onConfirm () {
// window.open(
// 'https://github.com/NapNeko/NapCatQQ/releases',
// '_blank',
// 'noopener'
// );
// },
// });
// }}
// >
// <FaInfo />
// </Button>
// </Tooltip>
// );
// };
const NapCatVersion = () => {
const {
@@ -212,7 +202,7 @@ const NapCatVersion = () => {
value={
packageError
? (
`错误:${packageError.message}`
`错误:${packageError.message}`
)
: packageLoading
? (
@@ -222,13 +212,12 @@ const NapCatVersion = () => {
currentVersion
)
}
endContent={<NewVersionTip currentVersion={currentVersion} />}
/>
);
};
export interface SystemInfoProps {
archInfo?: string
archInfo?: string;
}
const SystemInfo: React.FC<SystemInfoProps> = (props) => {
const { archInfo } = props;
@@ -252,7 +241,7 @@ const SystemInfo: React.FC<SystemInfoProps> = (props) => {
value={
qqVersionError
? (
`错误:${qqVersionError.message}`
`错误:${qqVersionError.message}`
)
: qqVersionLoading
? (

View File

@@ -48,6 +48,15 @@ export default class WebUIManager {
return data.data;
}
public static async UpdateNapCat () {
const { data } = await serverRequest.post<ServerResponse<any>>(
'/UpdateNapCat/update',
{},
{ timeout: 60000 } // 1分钟超时
);
return data;
}
public static async getQQVersion () {
const { data } =
await serverRequest.get<ServerResponse<string>>('/base/QQVersion');

View File

@@ -1,4 +1,5 @@
import { Card, CardBody } from '@heroui/card';
import { Button } from '@heroui/button';
import { Image } from '@heroui/image';
import { Link } from '@heroui/link';
import { Skeleton } from '@heroui/skeleton';
@@ -7,6 +8,7 @@ import { useRequest } from 'ahooks';
import { useMemo } from 'react';
import { BsTelegram, BsTencentQq } from 'react-icons/bs';
import { IoDocument } from 'react-icons/io5';
import toast from 'react-hot-toast';
import HoverTiltedCard from '@/components/hover_titled_card';
import NapCatRepoInfo from '@/components/napcat_repo_info';
@@ -20,9 +22,44 @@ import WebUIManager from '@/controllers/webui_manager';
function VersionInfo () {
const { data, loading, error } = useRequest(WebUIManager.GetNapCatVersion);
// 更新NapCat
const { run: updateNapCat, loading: updating } = useRequest(
WebUIManager.UpdateNapCat,
{
manual: true,
onSuccess: (response) => {
console.log('UpdateNapCat onSuccess response:', response);
console.log('response.code:', response.code);
console.log('response.data:', response.data);
console.log('response.message:', response.message);
if (response.code === 0) {
const message = response.data?.message || '更新完成';
console.log('显示消息:', message);
toast.success(message, {
duration: 5000,
});
} else {
console.log('显示错误消息:', response.message || '更新失败');
toast.error(response.message || '更新失败');
}
},
onError: (error) => {
toast.error('更新失败: ' + error.message);
},
}
);
const handleUpdate = () => {
if (!updating) {
updateNapCat();
}
};
return (
<div className='flex items-center gap-2 text-2xl font-bold'>
<div className='flex items-center gap-2'>
<div className='flex items-center gap-4'>
<div className='flex items-center gap-2 text-2xl font-bold'>
<div className='text-primary-500 drop-shadow-md'>NapCat</div>
{error
? (
@@ -47,6 +84,16 @@ function VersionInfo () {
/>
)}
</div>
<Button
color="primary"
variant="solid"
size="sm"
isLoading={updating}
onPress={handleUpdate}
isDisabled={updating}
>
{updating ? '更新中...' : '更新'}
</Button>
</div>
);
}