Add backend and frontend support for NapCat auto-update
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run

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 c123b34d5f
commit 2cdc9bdc09
11 changed files with 641 additions and 164 deletions

View File

@ -8,6 +8,8 @@ import { QQBasicInfoWrapper } from '@/napcat-core/helper/qq-basic-info';
import { InstanceContext, loadQQWrapper, NapCatCore, NapCatCoreWorkingEnv, NodeIKernelLoginListener, NodeIKernelLoginService, NodeIQQNTWrapperSession, SelfInfo, WrapperNodeApi } from '@/napcat-core';
import { proxiedListenerOf } from '@/napcat-core/helper/proxy-handler';
import { statusHelperSubscription } from '@/napcat-core/helper/status';
import { applyPendingUpdates } from '@/napcat-webui-backend/src/api/UpdateNapCat';
import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
// Framework ES入口文件
export async function getWebUiUrl () {
@ -32,6 +34,7 @@ export async function NCoreInitFramework (
});
const pathWrapper = new NapCatPathWrapper();
await applyPendingUpdates(pathWrapper);
const logger = new LogWrapper(pathWrapper.logsPath);
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion());
@ -73,6 +76,7 @@ export async function NCoreInitFramework (
await loaderObject.core.initCore();
// 启动WebUi
WebUiDataRuntime.setWorkingEnv(NapCatCoreWorkingEnv.Framework);
InitWebUi(logger, pathWrapper, logSubscription, statusHelperSubscription).then().catch(e => logger.logError(e));
// 初始化LLNC的Onebot实现
await new NapCatOneBot11Adapter(loaderObject.core, loaderObject.context, pathWrapper).InitOneBot();

View File

@ -12,7 +12,17 @@ import { ArgvOrCommandLine } from '@homebridge/node-pty-prebuilt-multiarch/src/t
import { assign } from '@homebridge/node-pty-prebuilt-multiarch/src/utils';
import { pty_loader } from './prebuild-loader';
import { fileURLToPath } from 'url';
export const pty = pty_loader();
// 懒加载pty避免在模块导入时立即执行pty_loader()
let _pty: any;
export const pty: any = new Proxy({}, {
get (_target, prop) {
if (!_pty) {
_pty = pty_loader();
}
return _pty[prop];
}
});
let helperPath: string;
helperPath = '../build/Release/spawn-helper';

View File

@ -35,6 +35,7 @@ import { logSubscription, LogWrapper } from '@/napcat-core/helper/log';
import { proxiedListenerOf } from '@/napcat-core/helper/proxy-handler';
import { QQBasicInfoWrapper } from '@/napcat-core/helper/qq-basic-info';
import { statusHelperSubscription } from '@/napcat-core/helper/status';
import { applyPendingUpdates } from '@/napcat-webui-backend/src/api/UpdateNapCat';
// NapCat Shell App ES 入口文件
async function handleUncaughtExceptions (logger: LogWrapper) {
process.on('uncaughtException', (err) => {
@ -318,6 +319,7 @@ export async function NCoreInitShell () {
const pathWrapper = new NapCatPathWrapper();
const logger = new LogWrapper(pathWrapper.logsPath);
handleUncaughtExceptions(logger);
await applyPendingUpdates(pathWrapper);
// 初始化 FFmpeg 服务
await FFmpegService.init(pathWrapper.binaryPath, logger);
@ -338,8 +340,8 @@ export async function NCoreInitShell () {
o3Service.addO3MiscListener(new NodeIO3MiscListener());
logger.log('[NapCat] [Core] NapCat.Core Version: ' + napCatVersion);
WebUiDataRuntime.setWorkingEnv(NapCatCoreWorkingEnv.Shell);
InitWebUi(logger, pathWrapper, logSubscription, statusHelperSubscription).then().catch(e => logger.logError(e));
const engine = wrapper.NodeIQQNTWrapperEngine.get();
const loginService = wrapper.NodeIKernelLoginService.get();
let session: NodeIQQNTWrapperSession;

View File

@ -0,0 +1,388 @@
import { RequestHandler } from 'express';
import { sendSuccess, sendError } from '@/napcat-webui-backend/src/utils/response';
import * as fs from 'fs';
import * as path from 'path';
import * as https from 'https';
import compressing from 'compressing';
import { webUiPathWrapper } from '../../index';
import { NapCatPathWrapper } from '@/napcat-common/src/path';
import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
import { NapCatCoreWorkingEnv } from '@/napcat-webui-backend/src/types';
interface Release {
tag_name: string;
assets: Array<{
name: string;
browser_download_url: string;
}>;
body?: string;
}
// 更新配置文件接口
interface UpdateConfig {
version: string;
updateTime: string;
files: Array<{
sourcePath: string;
targetPath: string;
backupPath?: string;
}>;
changelog?: string;
}
// 需要跳过更新的文件
const SKIP_UPDATE_FILES = [
'NapCatWinBootMain.exe',
'NapCatWinBootHook.dll'
];
/**
*
*/
function scanFilesRecursively (dirPath: string, basePath: string = dirPath): Array<{
sourcePath: string;
relativePath: string;
}> {
const files: Array<{
sourcePath: string;
relativePath: string;
}> = [];
const items = fs.readdirSync(dirPath);
for (const item of items) {
const fullPath = path.join(dirPath, item);
const relativePath = path.relative(basePath, fullPath);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
// 递归扫描子目录
files.push(...scanFilesRecursively(fullPath, basePath));
} else if (stat.isFile()) {
files.push({
sourcePath: fullPath,
relativePath: relativePath
});
}
}
return files;
}
// 镜像源列表参考ffmpeg下载实现
const mirrorUrls = [
'https://j.1win.ggff.net/',
'https://git.yylx.win/',
'https://ghfile.geekertao.top/',
'https://gh-proxy.net/',
'https://ghm.078465.xyz/',
'https://gitproxy.127731.xyz/',
'https://jiashu.1win.eu.org/',
'', // 原始URL
];
/**
* URL是否可用
*/
async function testUrl (url: string): Promise<boolean> {
return new Promise<boolean>((resolve) => {
const req = https.get(url, { timeout: 5000 }, (res) => {
const statusCode = res.statusCode || 0;
if (statusCode >= 200 && statusCode < 300) {
req.destroy();
resolve(true);
} else {
req.destroy();
resolve(false);
}
});
req.on('error', () => resolve(false));
req.on('timeout', () => {
req.destroy();
resolve(false);
});
});
}
/**
* URL
*/
function buildMirrorUrl (originalUrl: string, mirror: string): string {
if (!mirror) return originalUrl;
return mirror + originalUrl;
}
/**
* URL
*/
async function findAvailableUrl (originalUrl: string): Promise<string> {
console.log('Testing download URLs...');
// 先测试原始URL
if (await testUrl(originalUrl)) {
console.log('Using original URL:', originalUrl);
return originalUrl;
}
// 测试镜像源
for (const mirror of mirrorUrls) {
const mirrorUrl = buildMirrorUrl(originalUrl, mirror);
console.log('Testing mirror:', mirrorUrl);
if (await testUrl(mirrorUrl)) {
console.log('Using mirror URL:', mirrorUrl);
return mirrorUrl;
}
}
throw new Error('所有下载源都不可用');
}
/**
*
*/
async function downloadFile (url: string, dest: string): Promise<void> {
console.log('Starting download from:', url);
const file = fs.createWriteStream(dest);
return new Promise((resolve, reject) => {
const request = https.get(url, {
headers: { 'User-Agent': 'NapCat-WebUI' }
}, (res) => {
console.log('Response status:', res.statusCode);
console.log('Content-Type:', res.headers['content-type']);
if (res.statusCode === 302 || res.statusCode === 301) {
console.log('Following redirect to:', res.headers.location);
file.close();
fs.unlinkSync(dest);
downloadFile(res.headers.location!, dest).then(resolve).catch(reject);
return;
}
if (res.statusCode !== 200) {
file.close();
fs.unlinkSync(dest);
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
return;
}
res.pipe(file);
file.on('finish', () => {
file.close();
console.log('Download completed');
resolve();
});
});
request.on('error', (err) => {
console.error('Download error:', err);
file.close();
fs.unlink(dest, () => { });
reject(err);
});
});
}
export const UpdateNapCatHandler: RequestHandler = async (_req, res) => {
try {
// 获取最新release信息
const latestRelease = await getLatestRelease() as Release;
const ReleaseName = WebUiDataRuntime.getWorkingEnv() === NapCatCoreWorkingEnv.Framework ? 'NapCat.Framework.zip' : 'NapCat.Shell.zip';
const shellZipAsset = latestRelease.assets.find(asset => asset.name === ReleaseName);
if (!shellZipAsset) {
throw new Error(`未找到${ReleaseName}文件`);
}
// 创建临时目录
const tempDir = path.join(webUiPathWrapper.binaryPath, './temp');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
// 查找可用的下载URL
const downloadUrl = await findAvailableUrl(shellZipAsset.browser_download_url);
// 下载zip
const zipPath = path.join(tempDir, 'napcat-latest.zip');
console.log('[NapCat Update] Saving to:', zipPath);
await downloadFile(downloadUrl, zipPath);
// 检查文件大小
const stats = fs.statSync(zipPath);
console.log('[NapCat Update] Downloaded file size:', stats.size, 'bytes');
// 解压到临时目录
const extractPath = path.join(tempDir, 'napcat-extract');
console.log('[NapCat Update] Extracting to:', extractPath);
await compressing.zip.uncompress(zipPath, extractPath);
// 获取解压后的实际内容目录NapCat.Shell.zip直接包含文件无额外根目录
const sourcePath = extractPath;
// 执行更新操作
try {
// 扫描需要更新的文件
const allFiles = scanFilesRecursively(sourcePath);
const failedFiles: Array<{
sourcePath: string;
targetPath: string;
}> = [];
// 先尝试直接替换文件
for (const fileInfo of allFiles) {
const targetFilePath = path.join(webUiPathWrapper.binaryPath, fileInfo.relativePath);
// 跳过指定的文件
if (SKIP_UPDATE_FILES.includes(path.basename(fileInfo.relativePath))) {
console.log(`[NapCat Update] Skipping update for ${fileInfo.relativePath}`);
continue;
}
try {
// 确保目标目录存在
const targetDir = path.dirname(targetFilePath);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
// 尝试直接替换文件
if (fs.existsSync(targetFilePath)) {
fs.unlinkSync(targetFilePath); // 删除旧文件
}
fs.copyFileSync(fileInfo.sourcePath, targetFilePath);
} catch (error) {
// 如果替换失败,添加到失败列表
console.log(`[NapCat Update] Failed to update ${targetFilePath}, will retry on next startup:`, error);
failedFiles.push({
sourcePath: fileInfo.sourcePath,
targetPath: targetFilePath
});
}
}
// 如果有替换失败的文件,创建更新配置文件
if (failedFiles.length > 0) {
const updateConfig: UpdateConfig = {
version: latestRelease.tag_name,
updateTime: new Date().toISOString(),
files: failedFiles,
changelog: latestRelease.body || ''
};
// 保存更新配置文件
const configPath = path.join(webUiPathWrapper.configPath, 'napcat-update.json');
fs.writeFileSync(configPath, JSON.stringify(updateConfig, null, 2));
console.log(`[NapCat Update] Update config saved for ${failedFiles.length} failed files: ${configPath}`);
}
// 发送成功响应
const message = failedFiles.length > 0
? `更新完成,重启应用以应用剩余${failedFiles.length}个文件的更新`
: '更新完成';
sendSuccess(res, {
status: 'completed',
message,
newVersion: latestRelease.tag_name,
failedFilesCount: failedFiles.length
});
} catch (error) {
console.error('更新失败:', error);
sendError(res, '更新失败: ' + (error instanceof Error ? error.message : '未知错误'));
}
} catch (error: any) {
console.error('更新失败:', error);
sendError(res, '更新失败: ' + error.message);
}
};
async function getLatestRelease (): Promise<Release> {
return new Promise((resolve, reject) => {
https.get('https://api.github.com/repos/NapNeko/NapCatQQ/releases/latest', {
headers: { 'User-Agent': 'NapCat-WebUI' }
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const release = JSON.parse(data) as Release;
console.log('Release info:', {
tag_name: release.tag_name,
assets: release.assets?.map(a => ({ name: a.name, url: a.browser_download_url }))
});
resolve(release);
} catch (e) {
reject(e);
}
});
}).on('error', reject);
});
}
/**
*
*/
export async function applyPendingUpdates (webUiPathWrapper: NapCatPathWrapper): Promise<void> {
const configPath = path.join(webUiPathWrapper.configPath, 'napcat-update.json');
if (!fs.existsSync(configPath)) {
console.log('No pending updates found');
return;
}
try {
console.log('[NapCat Update] Applying pending updates...');
const updateConfig: UpdateConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
const remainingFiles: Array<{
sourcePath: string;
targetPath: string;
}> = [];
for (const file of updateConfig.files) {
try {
// 检查源文件是否存在
if (!fs.existsSync(file.sourcePath)) {
console.warn(`[NapCat Update] Source file not found: ${file.sourcePath}`);
continue;
}
// 确保目标目录存在
const targetDir = path.dirname(file.targetPath);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
// 尝试替换文件
if (fs.existsSync(file.targetPath)) {
fs.unlinkSync(file.targetPath); // 删除旧文件
}
fs.copyFileSync(file.sourcePath, file.targetPath);
console.log(`[NapCat Update] Updated ${path.basename(file.targetPath)} on startup`);
} catch (error) {
console.error(`[NapCat Update] Failed to update ${file.targetPath} on startup:`, error);
// 如果仍然失败,保留在列表中
remainingFiles.push(file);
}
}
// 如果还有失败的文件,更新配置文件
if (remainingFiles.length > 0) {
const updatedConfig: UpdateConfig = {
...updateConfig,
files: remainingFiles
};
fs.writeFileSync(configPath, JSON.stringify(updatedConfig, null, 2));
console.log(`${remainingFiles.length} files still pending update`);
} else {
// 所有文件都成功更新,删除配置文件
fs.unlinkSync(configPath);
console.log('[NapCat Update] All pending updates applied successfully');
}
} catch (error) {
console.error('[NapCat Update] Failed to apply pending updates:', error);
}
}

View File

@ -1,8 +1,9 @@
import store from 'napcat-common/src/store';
import { napCatVersion } from 'napcat-common/src/version';
import type { LoginRuntimeType } from '../types';
import { NapCatCoreWorkingEnv, type LoginRuntimeType } from '../types';
const LoginRuntime: LoginRuntimeType = {
workingEnv: NapCatCoreWorkingEnv.Unknown,
LoginCurrentTime: Date.now(),
LoginCurrentRate: 0,
QQLoginStatus: false, // 已实现 但太傻了 得去那边注册个回调刷新
@ -36,6 +37,12 @@ const LoginRuntime: LoginRuntimeType = {
},
};
export const WebUiDataRuntime = {
setWorkingEnv (env: NapCatCoreWorkingEnv): void {
LoginRuntime.workingEnv = env;
},
getWorkingEnv (): NapCatCoreWorkingEnv {
return LoginRuntime.workingEnv;
},
setWebUiTokenChangeCallback (func: (token: string) => Promise<void>): void {
LoginRuntime.onWebUiTokenChange = func;
},

View File

@ -0,0 +1,13 @@
/**
* @file UpdateNapCat路由
*/
import { Router } from 'express';
import { UpdateNapCatHandler } from '@/napcat-webui-backend/src/api/UpdateNapCat';
const router = Router();
// POST /api/UpdateNapCat/update - 更新NapCat
router.post('/update', UpdateNapCatHandler);
export { router as UpdateNapCatRouter };

View File

@ -14,6 +14,7 @@ import { LogRouter } from '@/napcat-webui-backend/src/router/Log';
import { BaseRouter } from '@/napcat-webui-backend/src/router/Base';
import { FileRouter } from './File';
import { WebUIConfigRouter } from './WebUIConfig';
import { UpdateNapCatRouter } from './UpdateNapCat';
const router = Router();
@ -38,5 +39,7 @@ router.use('/Log', LogRouter);
router.use('/File', FileRouter);
// router:WebUI配置相关路由
router.use('/WebUIConfig', WebUIConfigRouter);
// router:更新NapCat相关路由
router.use('/UpdateNapCat', UpdateNapCatRouter);
export { router as ALLRouter };

View File

@ -30,8 +30,13 @@ export interface WebUiCredentialJson {
Data: WebUiCredentialInnerJson;
Hmac: string;
}
export enum NapCatCoreWorkingEnv {
Unknown = 0,
Shell = 1,
Framework = 2,
}
export interface LoginRuntimeType {
workingEnv: NapCatCoreWorkingEnv;
LoginCurrentTime: number;
LoginCurrentRate: number;
QQLoginStatus: boolean;

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>
);
}