mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-12-18 20:30:08 +08:00
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:
parent
c123b34d5f
commit
2cdc9bdc09
@ -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();
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
|
||||
388
packages/napcat-webui-backend/src/api/UpdateNapCat.ts
Normal file
388
packages/napcat-webui-backend/src/api/UpdateNapCat.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
},
|
||||
|
||||
13
packages/napcat-webui-backend/src/router/UpdateNapCat.ts
Normal file
13
packages/napcat-webui-backend/src/router/UpdateNapCat.ts
Normal 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 };
|
||||
@ -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 };
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
? (
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user